From 4700e83f15d9a61112169602361d06c667a472ce Mon Sep 17 00:00:00 2001 From: INS201 Date: Mon, 25 May 2026 12:18:23 +0530 Subject: [PATCH 01/45] docs(campaigns): Campaigns REST API public docs Adds the Campaigns product to the docs site: - Product landing (campaigns.mdx) with capabilities, common use cases, and server-only positioning. - Conceptual pages for Sequences and Analytics under campaigns/. - REST API docs at rest-api/campaigns-apis/, covering 5 endpoints: GET /channels, GET /channels/availability, GET /templates, GET /templates/{templateId}, DELETE /notification-feed/{feedItemId}. Includes an OpenAPI 3.0 spec (campaigns-apis.json), an overview with response/error envelope and variable-resolution precedence, and a Setup & Authentication page. - Campaigns card on the home page (index.mdx). - docs.json registers campaigns-apis.json and the Campaigns product navigation (Campaigns and APIs tabs). --- campaigns-apis.json | 362 ++++++++++++++++++ campaigns.mdx | 39 ++ campaigns/analytics.mdx | 60 +++ campaigns/sequences.mdx | 56 +++ docs.json | 80 +++- index.mdx | 9 + .../analytics/campaign-metrics.mdx | 20 + .../analytics/channel-metrics.mdx | 14 + .../analytics/overview-metrics.mdx | 22 ++ .../analytics/template-metrics.mdx | 20 + .../campaigns-apis/analytics/user-metrics.mdx | 43 +++ .../channels/check-availability.mdx | 8 + .../campaigns-apis/channels/list-channels.mdx | 8 + .../notification-feed/delete-feed-item.mdx | 8 + rest-api/campaigns-apis/overview.mdx | 108 ++++++ .../sequences/sequence-metrics.mdx | 20 + .../setup-and-authentication.mdx | 58 +++ .../campaigns-apis/templates/get-template.mdx | 8 + .../templates/list-templates.mdx | 8 + 19 files changed, 950 insertions(+), 1 deletion(-) create mode 100644 campaigns-apis.json create mode 100644 campaigns.mdx create mode 100644 campaigns/analytics.mdx create mode 100644 campaigns/sequences.mdx create mode 100644 rest-api/campaigns-apis/analytics/campaign-metrics.mdx create mode 100644 rest-api/campaigns-apis/analytics/channel-metrics.mdx create mode 100644 rest-api/campaigns-apis/analytics/overview-metrics.mdx create mode 100644 rest-api/campaigns-apis/analytics/template-metrics.mdx create mode 100644 rest-api/campaigns-apis/analytics/user-metrics.mdx create mode 100644 rest-api/campaigns-apis/channels/check-availability.mdx create mode 100644 rest-api/campaigns-apis/channels/list-channels.mdx create mode 100644 rest-api/campaigns-apis/notification-feed/delete-feed-item.mdx create mode 100644 rest-api/campaigns-apis/overview.mdx create mode 100644 rest-api/campaigns-apis/sequences/sequence-metrics.mdx create mode 100644 rest-api/campaigns-apis/setup-and-authentication.mdx create mode 100644 rest-api/campaigns-apis/templates/get-template.mdx create mode 100644 rest-api/campaigns-apis/templates/list-templates.mdx diff --git a/campaigns-apis.json b/campaigns-apis.json new file mode 100644 index 000000000..6e2a24429 --- /dev/null +++ b/campaigns-apis.json @@ -0,0 +1,362 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Campaigns API", + "description": "Server-side REST API for CometChat Campaigns. Send transactional in-app and push notifications, manage templates and channels, and administer the notification feed. For backend integrators and the CometChat Dashboard. SDK-facing endpoints for end-user clients are documented separately.", + "version": "3.0" + }, + "servers": [ + { + "url": "https://{appId}.api-{region}.cometchat.io/v3/campaigns", + "variables": { + "appId": { + "default": "appId", + "description": "(Required) App ID" + }, + "region": { + "enum": ["us", "eu", "in"], + "default": "us", + "description": "Select Region" + } + } + } + ], + "security": [ + { + "apiKey": [] + } + ], + "tags": [ + { + "name": "Notification Feed", + "description": "Admin operations on feed items produced by sends and campaigns." + }, + { + "name": "Channels", + "description": "Read channel instances and per-type availability." + }, + { + "name": "Templates", + "description": "Read templates and their pinned versions. Templates are the atomic content primitive, and every notification flows through one." + } + ], + "paths": { + "/notification-feed/{feedItemId}": { + "parameters": [ + { "$ref": "#/components/parameters/feedItemId" } + ], + "delete": { + "tags": ["Notification Feed"], + "summary": "Delete feed item", + "description": "Soft-delete a feed item. The row remains until retention purge hard-deletes it.", + "operationId": "delete-feed-item", + "responses": { + "200": { + "description": "Deleted.", + "content": { + "application/json": { + "example": { "data": { "id": "feed-cl9xyz123", "deletedAt": "2026-05-04T10:35:00.000Z" } } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "404": { "$ref": "#/components/responses/NotFound" } + } + } + }, + "/channels": { + "get": { + "tags": ["Channels"], + "summary": "List channels", + "description": "List channel instances configured for this app. Paginated.", + "operationId": "list-channels", + "parameters": [ + { + "name": "type", + "in": "query", + "description": "Filter by channel type.", + "schema": { "type": "string", "enum": ["in_app", "push"] } + }, + { + "name": "enabled", + "in": "query", + "description": "Filter by enabled state.", + "schema": { "type": "boolean" } + }, + { + "name": "limit", + "in": "query", + "schema": { "type": "integer", "default": 20 } + } + ], + "responses": { + "200": { + "description": "Paginated channel list.", + "content": { + "application/json": { + "example": { + "data": [ + { + "id": "ch-cl9abc111", + "appId": "app_123", + "name": "Default Feed", + "channelId": "default-feed", + "type": "in_app", + "enabled": true, + "metadata": {}, + "templateCount": 4, + "createdAt": "2026-04-01T00:00:00.000Z", + "updatedAt": "2026-04-01T00:00:00.000Z" + } + ], + "meta": { "current": { "limit": 20, "count": 1 } } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/channels/availability": { + "get": { + "tags": ["Channels"], + "summary": "Check availability", + "description": "Return per-type counts and configured limits. Used by the Dashboard to gate channel creation.", + "operationId": "check-channel-availability", + "responses": { + "200": { + "description": "Availability by channel type.", + "content": { + "application/json": { + "example": { + "data": { + "in_app": { "count": 2, "limit": 5, "available": 3 }, + "push": { "count": 1, "limit": 5, "available": 4 } + } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/templates": { + "get": { + "tags": ["Templates"], + "summary": "List templates", + "description": "List templates for this app. Paginated.", + "operationId": "list-templates", + "parameters": [ + { + "name": "category", + "in": "query", + "description": "Filter by category name.", + "schema": { "type": "string" } + }, + { + "name": "status", + "in": "query", + "schema": { "type": "string", "enum": ["draft", "approved", "archived"] } + }, + { + "name": "search", + "in": "query", + "description": "Substring match on name or templateId.", + "schema": { "type": "string" } + }, + { + "name": "tags", + "in": "query", + "description": "Comma-separated tag list. Pair with `tagMatch`.", + "schema": { "type": "string" } + }, + { + "name": "tagMatch", + "in": "query", + "schema": { "type": "string", "enum": ["any", "all"], "default": "any" } + }, + { + "name": "limit", + "in": "query", + "schema": { "type": "integer", "default": 20 } + } + ], + "responses": { + "200": { + "description": "Paginated template list.", + "content": { + "application/json": { + "example": { + "data": [ + { + "id": "tmpl-cl9def789", + "templateId": "order_update", + "appId": "app_123", + "name": "Order Update", + "category": "Updates", + "label": "Orders", + "tags": ["transactional", "orders"], + "status": "approved", + "currentVersion": 1, + "variableSchema": [ + { "key": "user_name", "name": "User Name", "type": "string", "required": true }, + { "key": "order_id", "name": "Order ID", "type": "string", "required": true } + ], + "config": { "sequenceEnabled": false }, + "createdAt": "2026-04-15T12:00:00.000Z", + "updatedAt": "2026-04-15T12:00:00.000Z" + } + ], + "meta": { "current": { "limit": 20, "count": 1 } } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/templates/{templateId}": { + "get": { + "tags": ["Templates"], + "summary": "Get template", + "description": "Fetch a single template by CUID or `templateId` slug. Includes all versions and the current version's channels and variable schema.", + "operationId": "get-template", + "parameters": [ + { + "name": "templateId", + "in": "path", + "required": true, + "description": "Template CUID or `templateId` slug.", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "Template.", + "content": { + "application/json": { + "example": { + "data": { + "id": "tmpl-cl9def789", + "templateId": "order_update", + "appId": "app_123", + "name": "Order Update", + "category": "Updates", + "label": "Orders", + "tags": ["transactional", "orders"], + "status": "approved", + "currentVersion": 1, + "versions": [ + { + "version": 1, + "channels": [ + { + "channelType": "in_app", + "channelId": "default-feed", + "dataType": "ui_template", + "categoryFilterEnabled": true, + "templateLabelEnabled": true, + "messageRetentionHours": 720, + "content": { + "title": "Order #{{order_id}} update", + "body": "Hi {{user_name}}, your order is on the way." + } + } + ], + "variableSchema": [ + { "key": "user_name", "name": "User Name", "type": "string", "required": true }, + { "key": "order_id", "name": "Order ID", "type": "string", "required": true } + ], + "createdAt": "2026-04-15T12:00:00.000Z" + } + ], + "variableSchema": [ + { "key": "user_name", "name": "User Name", "type": "string", "required": true }, + { "key": "order_id", "name": "Order ID", "type": "string", "required": true } + ], + "config": { "sequenceEnabled": false }, + "createdAt": "2026-04-15T12:00:00.000Z", + "updatedAt": "2026-04-15T12:00:00.000Z" + } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "404": { "$ref": "#/components/responses/NotFound" } + } + } + } + }, + "components": { + "parameters": { + "feedItemId": { + "name": "feedItemId", + "in": "path", + "required": true, + "description": "(Required) Feed item ID.", + "schema": { "type": "string" } + } + }, + "securitySchemes": { + "apiKey": { + "type": "apiKey", + "description": "API Key with fullAccess scope (REST API Key from the Dashboard).", + "name": "apikey", + "in": "header" + } + }, + "responses": { + "BadRequest": { + "description": "400 Bad Request. Validation or contract failure. See the error envelope on the Campaigns API overview.", + "content": { + "application/json": { + "example": { + "error": { + "code": "ERR_VALIDATION", + "message": "receivers must contain at least one userId", + "devMessage": "receivers must contain at least one userId", + "details": ["receivers must contain at least one userId"], + "source": "campaigns-service" + } + } + } + } + }, + "Unauthorized": { + "description": "401 Unauthorized. Missing or invalid app credentials.", + "content": { + "application/json": { + "example": { + "error": { + "code": "ERR_UNAUTHORIZED", + "message": "apikey header is required", + "devMessage": "apikey header is required", + "source": "campaigns-service" + } + } + } + } + }, + "NotFound": { + "description": "404 Not Found. Resource does not exist.", + "content": { + "application/json": { + "example": { + "error": { + "code": "ERR_FEED_ITEM_NOT_FOUND", + "message": "Feed item not found", + "devMessage": "Feed item not found: feed-cl9xyz123", + "details": { "feedItemId": "feed-cl9xyz123" }, + "source": "campaigns-service" + } + } + } + } + } + } + } +} diff --git a/campaigns.mdx b/campaigns.mdx new file mode 100644 index 000000000..e3b28da6a --- /dev/null +++ b/campaigns.mdx @@ -0,0 +1,39 @@ +--- +title: "Campaigns" +description: "Send transactional notifications, run scheduled campaigns, and manage the per-user in-app feed across in-app and push channels." +--- + +The CometChat **Campaigns** product is a managed notification surface for sending the right message on the right channel at the right time. It pairs a content layer (templates, variables, categories, tags) with a delivery layer (real-time sends, scheduled campaigns, CSV-driven batches) and a per-user in-app notification feed that your application reads with the SDK. + +### What you can do + +- **Send transactional notifications.** Deliver a templated message to one or many users in real time, with variable resolution applied per recipient. +- **Organise content with templates.** Author reusable templates with typed variables, categories, and tags. Every notification flows through one. +- **Run scheduled and batch campaigns.** Send to a handful of users in real time or to tens of thousands through inline batch and CSV-driven flows. +- **Track delivery and engagement.** Capture delivered, read, viewed, clicked, and interacted signals so your analytics and unread counts stay accurate. +- **Administer the in-app feed.** Soft-delete feed items, audit per-user delivery, and manage retention. + +### Common use cases + +#### Transactional alerts + +Order shipped, payment receipt, password reset, security alert. One templated message per event, dispatched in real time, with per-recipient variable substitution (`{{order_id}}`, `{{user_name}}`). + +#### Marketing campaigns + +Product launches, promotional offers, re-engagement nudges. Schedule the send, upload a recipient list, and let the campaign worker fan out across in-app and push. + +#### Operational messages + +Maintenance windows, policy updates, account changes. Same template authoring flow, typically dispatched to a broad audience and routed through the in-app feed so users can revisit them. + +### Who this is for + +This section documents the **server-side REST API**. It's the surface your back-end, the CometChat Dashboard, and your ops tools call to manage templates, channels, and sends. It is not meant for direct use from a browser or mobile client. + +End-user clients (a mobile app reading its own feed, an in-app bell widget) should integrate with the **Campaigns SDK** instead, which exposes the feed read, unread count, mark-delivered/read, and engagement-tracking surfaces in a way that's safe for client-side use. + +### Start here + +- [REST API Overview](/rest-api/campaigns-apis/overview). Endpoint reference, response envelope, error codes. +- [Setup & Authentication](/rest-api/campaigns-apis/setup-and-authentication). Credentials, headers, base URL. diff --git a/campaigns/analytics.mdx b/campaigns/analytics.mdx new file mode 100644 index 000000000..9b36d2567 --- /dev/null +++ b/campaigns/analytics.mdx @@ -0,0 +1,60 @@ +--- +title: "Analytics" +description: "Track notification delivery and engagement metrics across campaigns, templates, channels, and users." +--- + +The Campaigns analytics surface gives you visibility into how notifications perform after they leave the system. Metrics are available in the Dashboard and through the analytics API, broken down by three primary dimensions. + +### Dimensions + +#### Campaigns + +Track delivery and engagement at the campaign level. Useful for comparing the performance of different sends, A/B testing subject lines, or evaluating whether a scheduled campaign hit its target audience. + +#### Templates + +Measure how a specific template performs across all sends that use it. Since every notification flows through a template, this is the most granular content-level view. Compare open rates between an "order shipped" template and a "weekly digest" template to understand which content resonates. + +#### Channels + +Evaluate per-channel effectiveness. If your app uses both in-app and push, the channel dimension shows which channel drives more engagement and where delivery failures concentrate. + +#### Tags + +Group notifications by any free-form label set on the template (`transactional`, `launch`, `re-engagement`, team or campaign cohort). Tags cut across templates and channels, so they're the right dimension when you want a marketing-style rollup without forcing one template per cohort. + +### Metrics tracked + +| Metric | Description | +| ------------ | --------------------------------------------------------------------------- | +| Requested | Notifications dispatched to the delivery layer for this channel. | +| Delivered | Confirmed delivered to the user (device acknowledgement or feed write). | +| Viewed | User scrolled the item into view or opened the notification. | +| Clicked | User tapped a CTA or link within the notification. | +| Interacted | User performed a custom interaction (e.g. dismissed, snoozed, replied). | +| Failed | Delivery failed (provider error, invalid token, user not reachable). | + +### Per-user insights + +You can also query engagement at the individual user level. This returns aggregate counts for a specific user over a date range: + +- Total viewed, clicked, and interacted counts. +- Last engagement timestamp. + +This is useful for building user-level health scores, identifying disengaged users, or powering re-engagement logic in your backend. + +### Filtering + +All analytics queries support: + +- **Date range.** Specify `startDate` and `endDate` to scope the window. +- **Period.** Choose `hourly` or `daily` granularity for time-series data. + +### Multi-device deduplication + +When a user receives or interacts with a notification on multiple devices, the same event is deduplicated and counted once. For example, if a push notification is delivered to both a phone and a tablet, it counts as a single delivery in the analytics. + +### Where to access + +- **Dashboard.** Navigate to Campaigns > Analytics for visual charts and drill-downs. +- **API.** Programmatic access to the same data via the analytics endpoints is part of the admin API surface (rolling out separately). For now, use the Dashboard for visual reports. diff --git a/campaigns/sequences.mdx b/campaigns/sequences.mdx new file mode 100644 index 000000000..2d03402c0 --- /dev/null +++ b/campaigns/sequences.mdx @@ -0,0 +1,56 @@ +--- +title: "Sequences" +description: "Control the order and timing of notifications across channels with channel sequencing." +--- + +Sequences let you define the order in which notification channels fire and the conditions that stop the sequence early. Instead of broadcasting the same message across all channels simultaneously, you deliver in a prioritised chain where each step waits for a result before deciding whether to proceed. + +### Why use sequences + +- **Reduce cost.** Start with low-cost channels (in-app feed) and only escalate to higher-cost channels (push) when the user has not engaged. +- **Improve deliverability.** Target the channel most likely to reach the user based on their prior engagement pattern. +- **Avoid notification fatigue.** Stop the chain as soon as the user has seen or acted on the message, rather than hitting them on every channel. + +### How it works + +Sequences are configured at the template level. When you create or edit a template in the Dashboard: + +1. Add two or more channels to the template. +2. Enable the **Sequence** toggle in the template settings (`config.sequenceEnabled = true`). +3. Arrange the channels in the desired delivery order. The first channel fires immediately at send time. +4. For each subsequent step, configure: + - **Stop condition.** The engagement signal that halts the sequence: `delivered`, `viewed`, or `clicked`. + - **Wait window.** How long to wait for the stop condition before moving to the next step: 5, 10, 30, 60, 240, or 1440 minutes. + +If the stop condition is met within the wait window, the remaining steps are skipped. If the window expires without the condition being met, the next channel fires. + +### Example + +A template with two channels configured as a sequence: + +| Step | Channel | Stop condition | Wait | +| ---- | -------- | -------------- | ------ | +| 1 | In-app | delivered | 30 min | +| 2 | Push | (terminal) | n/a | + +At send time, the in-app notification is dispatched immediately. If the item is marked delivered within 30 minutes, the sequence stops and push is never sent. If 30 minutes pass without a delivery signal, push fires as a fallback. + +### Metrics + +When a template uses sequencing, per-step delivery metrics are available through the Dashboard and the analytics API. Which metrics are reported depends on the channel: + +| Channel | Requested | Delivered | Viewed | Clicked | +| -------- | :-------: | :-------: | :----: | :-----: | +| In-app | ✓ | ✓ | ✓ | ✓ | +| Push | ✓ | ✓ | n/a | ✓ | + +`Viewed` is in-app only. Push notifications do not emit a viewed signal because there is no in-feed render to scroll into view; use `Clicked` to gauge engagement on the push step. + +These metrics help you evaluate whether your sequence order and wait windows are tuned correctly, and where users are dropping off. + +### Limitations + +- Sequences require at least two channels on the template. +- The stop condition applies per step, not globally. A `clicked` on step 1 stops the chain, but a `delivered` on step 1 only stops if `delivered` is the configured condition for that step. +- Wait windows are fixed increments (5, 10, 30, 60, 240, 1440 minutes). Custom durations are not supported. +- Sequences are not available for CSV-batch campaigns in the current release. They apply to transactional sends and scheduled campaigns. diff --git a/docs.json b/docs.json index a014a3746..5feb4a252 100644 --- a/docs.json +++ b/docs.json @@ -33,7 +33,8 @@ "management-apis.json", "data-import-apis.json", "calls.json", - "ai-agent-service.json" + "ai-agent-service.json", + "campaigns-apis.json" ], "navigation": { "products": [ @@ -6470,6 +6471,83 @@ } ] }, + { + "product": "Campaigns", + "tabs": [ + { + "tab": "Campaigns", + "pages": [ + "campaigns", + { + "group": "Features", + "icon": "puzzle-piece", + "pages": [ + "campaigns/sequences", + "campaigns/analytics" + ] + } + ] + }, + { + "tab": "APIs", + "groups": [ + { + "group": "Campaigns API", + "pages": [ + "rest-api/campaigns-apis/overview", + "rest-api/campaigns-apis/setup-and-authentication", + { + "group": "Templates", + "expanded": false, + "icon": "file-lines", + "pages": [ + "rest-api/campaigns-apis/templates/list-templates", + "rest-api/campaigns-apis/templates/get-template" + ] + }, + { + "group": "Channels", + "expanded": false, + "icon": "tower-broadcast", + "pages": [ + "rest-api/campaigns-apis/channels/list-channels", + "rest-api/campaigns-apis/channels/check-availability" + ] + }, + { + "group": "Notification Feed", + "expanded": false, + "icon": "bell", + "pages": [ + "rest-api/campaigns-apis/notification-feed/delete-feed-item" + ] + }, + { + "group": "Sequences", + "expanded": false, + "icon": "arrow-progress", + "pages": [ + "rest-api/campaigns-apis/sequences/sequence-metrics" + ] + }, + { + "group": "Analytics", + "expanded": false, + "icon": "chart-line", + "pages": [ + "rest-api/campaigns-apis/analytics/overview-metrics", + "rest-api/campaigns-apis/analytics/campaign-metrics", + "rest-api/campaigns-apis/analytics/template-metrics", + "rest-api/campaigns-apis/analytics/channel-metrics", + "rest-api/campaigns-apis/analytics/user-metrics" + ] + } + ] + } + ] + } + ] + }, { "product": "Insights", "tabs": [ diff --git a/index.mdx b/index.mdx index b161b7282..0f73e9119 100644 --- a/index.mdx +++ b/index.mdx @@ -52,6 +52,15 @@ canonical: "https://cometchat.com/docs" Automate conversations using AI-powered chatbot technology. +} + iconType="solid" + href="/campaigns" +> + Send transactional notifications and run campaigns with an in-app feed and push. + + diff --git a/rest-api/campaigns-apis/analytics/campaign-metrics.mdx b/rest-api/campaigns-apis/analytics/campaign-metrics.mdx new file mode 100644 index 000000000..5bd0d9df4 --- /dev/null +++ b/rest-api/campaigns-apis/analytics/campaign-metrics.mdx @@ -0,0 +1,20 @@ +--- +title: "Campaign Metrics" +description: "Delivery and engagement drilldown for a specific campaign." +--- + +Returns delivery and engagement metrics scoped to a single campaign. + +### Endpoint + +``` +GET /analytics/campaigns/{campaignId} +``` + +### Path parameters + +| Parameter | Type | Required | Description | +| ------------ | ------ | -------- | -------------------- | +| `campaignId` | string | Yes | The campaign's CUID. | + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/analytics/channel-metrics.mdx b/rest-api/campaigns-apis/analytics/channel-metrics.mdx new file mode 100644 index 000000000..175a17cbd --- /dev/null +++ b/rest-api/campaigns-apis/analytics/channel-metrics.mdx @@ -0,0 +1,14 @@ +--- +title: "Channel Metrics" +description: "Delivery and engagement breakdown by channel type." +--- + +Returns delivery and engagement metrics broken down by channel. + +### Endpoint + +``` +GET /analytics/channels +``` + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/analytics/overview-metrics.mdx b/rest-api/campaigns-apis/analytics/overview-metrics.mdx new file mode 100644 index 000000000..269c2f8a4 --- /dev/null +++ b/rest-api/campaigns-apis/analytics/overview-metrics.mdx @@ -0,0 +1,22 @@ +--- +title: "Overview Metrics" +description: "Aggregate delivery and engagement counts across all campaigns." +--- + +Returns aggregate notification metrics for the app over a date range. + +### Endpoint + +``` +GET /analytics/overview +``` + +### Query parameters + +| Parameter | Type | Required | Description | +| ----------- | ------ | -------- | -------------------------------------------------- | +| `period` | string | No | Granularity: `hourly` or `daily`. | +| `startDate` | string | No | Start of the window (ISO-8601 date or unix seconds). | +| `endDate` | string | No | End of the window (ISO-8601 date or unix seconds). | + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/analytics/template-metrics.mdx b/rest-api/campaigns-apis/analytics/template-metrics.mdx new file mode 100644 index 000000000..9af1f21e4 --- /dev/null +++ b/rest-api/campaigns-apis/analytics/template-metrics.mdx @@ -0,0 +1,20 @@ +--- +title: "Template Metrics" +description: "Delivery and engagement drilldown for a specific template." +--- + +Returns delivery and engagement metrics across all sends that used a specific template. + +### Endpoint + +``` +GET /analytics/templates/{templateId} +``` + +### Path parameters + +| Parameter | Type | Required | Description | +| ------------ | ------ | -------- | ----------------------------------- | +| `templateId` | string | Yes | Template CUID or `templateId` slug. | + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/analytics/user-metrics.mdx b/rest-api/campaigns-apis/analytics/user-metrics.mdx new file mode 100644 index 000000000..841957f0a --- /dev/null +++ b/rest-api/campaigns-apis/analytics/user-metrics.mdx @@ -0,0 +1,43 @@ +--- +title: "User Metrics" +description: "Per-user engagement insights." +--- + +Returns aggregate engagement counts and last engagement timestamp for a specific user. + +### Endpoint + +``` +GET /analytics/users/{userId} +``` + +### Path parameters + +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | --------------- | +| `userId` | string | Yes | The user's UID. | + +### Query parameters + +| Parameter | Type | Required | Description | +| ----------- | ------ | -------- | ----------------------------------- | +| `startDate` | string | No | Start of the window. | +| `endDate` | string | No | End of the window. | + +### Response + +```json +{ + "data": { + "userId": "user_42", + "viewed": 47, + "clicked": 12, + "interacted": 3, + "lastEngagement": 1730301234 + } +} +``` + +This response shape is from the source-of-truth handoff. `lastEngagement` is unix seconds, or null if the user has never engaged. + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/channels/check-availability.mdx b/rest-api/campaigns-apis/channels/check-availability.mdx new file mode 100644 index 000000000..c800ba034 --- /dev/null +++ b/rest-api/campaigns-apis/channels/check-availability.mdx @@ -0,0 +1,8 @@ +--- +openapi: get /channels/availability +description: "Return per-type channel counts and configured limits." +--- + +Used by the Dashboard to gate channel creation against per-type quotas. + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/channels/list-channels.mdx b/rest-api/campaigns-apis/channels/list-channels.mdx new file mode 100644 index 000000000..db26ca1ab --- /dev/null +++ b/rest-api/campaigns-apis/channels/list-channels.mdx @@ -0,0 +1,8 @@ +--- +openapi: get /channels +description: "List channel instances configured for this app." +--- + +Paginated. Filter by `type` or `enabled` to narrow the result set. + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/notification-feed/delete-feed-item.mdx b/rest-api/campaigns-apis/notification-feed/delete-feed-item.mdx new file mode 100644 index 000000000..15dc7048e --- /dev/null +++ b/rest-api/campaigns-apis/notification-feed/delete-feed-item.mdx @@ -0,0 +1,8 @@ +--- +openapi: delete /notification-feed/{feedItemId} +description: "Soft-delete a feed item." +--- + +Sets `deletedAt`. The row remains until the retention purge hard-deletes it. + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/overview.mdx b/rest-api/campaigns-apis/overview.mdx new file mode 100644 index 000000000..d513f9898 --- /dev/null +++ b/rest-api/campaigns-apis/overview.mdx @@ -0,0 +1,108 @@ +--- +title: "Overview" +description: "REST surface for the Campaigns product. Send transactional notifications, manage templates and channels, and administer the in-app feed." +--- + +The Campaigns REST API is the server-side surface of the product. It covers transactional sends, template and channel look-ups, and admin operations on the notification feed. All endpoints share a single response envelope, a single error envelope, and a single authentication model. + +### Base URL + +``` +https://{appId}.api-{region}.cometchat.io/v3/campaigns +``` + +Regions: `us`, `eu`, `in`. Replace `{appId}` with your application ID. See [Setup & Authentication](/rest-api/campaigns-apis/setup-and-authentication) for credentials and headers. + +### Response envelope + +#### Success + +Single-entity responses wrap the resource under `data`: + +```json +{ "data": { "id": "feed-cl9xyz123", "deletedAt": "2026-05-04T10:35:00.000Z" } } +``` + +Paginated lists return an array under `data` and cursor metadata under `meta`: + +```json +{ + "data": [ /* items */ ], + "meta": { + "current": { "limit": 20, "count": 5 }, + "next": { "affix": "append", "sentAt": 1730301000, "id": "feed-cl9xyz123" } + } +} +``` + +Pagination is cursor-based. Pass `affix`, `sentAt` (or the configured sort field), and `id` from `meta.next` back into the next request to page forward. + +#### Error envelope (canonical) + +Every non-2xx response from the Campaigns service uses exactly this shape, wrapped under `error`: + +```json +{ + "error": { + "code": "ERR_FEED_ITEM_NOT_FOUND", + "message": "Feed item not found", + "devMessage": "Feed item not found: feed-cl9xyz123", + "details": { "feedItemId": "feed-cl9xyz123" }, + "source": "campaigns-service" + } +} +``` + +| Field | Convention | +| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `code` | Always `ERR_`-prefixed, SCREAMING_SNAKE. Domain codes (`ERR_TEMPLATE_NOT_FOUND`) and framework codes (`ERR_VALIDATION`, `ERR_UNAUTHORIZED`) share the prefix. | +| `message` | User-facing, identifier-free, safe to render in a UI. Never carries IDs or PII. | +| `devMessage` | Developer-facing. Carries identifiers and extra context for debugging. Defaults to `message` when omitted. | +| `details` | Structured drill-down. Object for domain errors (`{ "channelId": "feed-1" }`); array of strings for validation failures (one per failed field). | +| `source` | Always `"campaigns-service"`. Lets gateway-fronted callers attribute failures when multiple services share the gateway. | + +Unhandled internal errors fail closed; no stack trace or internal detail leaks: + +```json +{ + "error": { + "code": "ERR_INTERNAL", + "message": "An unexpected error occurred.", + "devMessage": "An unexpected error occurred.", + "source": "campaigns-service" + } +} +``` + +### Error code reference + +Non-exhaustive. These are the codes a typical caller will encounter. + +| Class | Codes | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Domain | `ERR_TEMPLATE_NOT_FOUND`, `ERR_CHANNEL_NOT_FOUND`, `ERR_FEED_ITEM_NOT_FOUND` | +| Framework | `ERR_VALIDATION` (400), `ERR_UNAUTHORIZED` (401), `ERR_NOT_FOUND` (404), `ERR_CONFLICT` (409), `ERR_TOO_MANY_REQUESTS` (429), `ERR_BAD_REQUEST` (4xx fallback), `ERR_INTERNAL` (500) | + +### Available operations + +| Operation | Method | Path | +| ----------------------------------------------------------------------------------------- | -------- | --------------------------------------------- | +| [List channels](/rest-api/campaigns-apis/channels/list-channels) | `GET` | `/channels` | +| [Check channel availability](/rest-api/campaigns-apis/channels/check-availability) | `GET` | `/channels/availability` | +| [List templates](/rest-api/campaigns-apis/templates/list-templates) | `GET` | `/templates` | +| [Get template](/rest-api/campaigns-apis/templates/get-template) | `GET` | `/templates/{templateId}` | +| [Delete feed item](/rest-api/campaigns-apis/notification-feed/delete-feed-item) | `DELETE` | `/notification-feed/{feedItemId}` | + +### Variable resolution + +Within template content, variables are referenced with `{{variableKey}}` syntax (for example `Hi {{user_name}}, your order #{{order_id}} shipped`) and resolved at send-time. + +For templated sends and campaign deliveries, three sources contribute to the final value substituted into channel content. Higher rows win: + +| Priority | Source | Where it lives | +| ----------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| 1 (highest) | Per-user variables | The `variables` map on a transactional send request, or per-row from a campaign's CSV import | +| 2 | Campaign-level defaults | The campaign's `config.variables` | +| 3 | Template variable-schema defaults | The template's `variableSchema[].default` field | + +If none of the three has a value, the token is left unresolved (`{{key}}`) in the rendered output. diff --git a/rest-api/campaigns-apis/sequences/sequence-metrics.mdx b/rest-api/campaigns-apis/sequences/sequence-metrics.mdx new file mode 100644 index 000000000..85ef6be9f --- /dev/null +++ b/rest-api/campaigns-apis/sequences/sequence-metrics.mdx @@ -0,0 +1,20 @@ +--- +title: "Sequence Metrics" +description: "Per-step delivery breakdown for sequenced campaigns." +--- + +Returns a per-step delivery breakdown for campaigns that use channel sequencing. Each step in the sequence reports its own delivery and engagement counts. + +### Endpoint + +``` +GET /campaigns/{campaignId}/sequence-metrics +``` + +### Path parameters + +| Parameter | Type | Required | Description | +| ------------ | ------ | -------- | -------------------- | +| `campaignId` | string | Yes | The campaign's CUID. | + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/setup-and-authentication.mdx b/rest-api/campaigns-apis/setup-and-authentication.mdx new file mode 100644 index 000000000..fdac1a4a3 --- /dev/null +++ b/rest-api/campaigns-apis/setup-and-authentication.mdx @@ -0,0 +1,58 @@ +--- +title: "Setup & Authentication" +description: "Credentials, headers, and base URL for the Campaigns REST API." +--- + +The Campaigns REST API is a **server-side API**. It runs behind the CometChat Gateway, which authenticates and authorizes every request before forwarding it to the service. You authenticate by attaching a REST API key on every request. The service itself never validates tokens. + +### Server-only + +These endpoints are intended for your back-end, the CometChat Dashboard, and your ops tools. Do not call them from a browser or a mobile client. Shipping a REST API key to an end-user client would expose full admin access to your app. + +End-user clients (mobile apps, in-app bell widgets) should integrate with the Campaigns SDK, which is the client-safe consumer surface for feed reads, unread counts, and engagement tracking. + +### Base URL + +``` +https://{appId}.api-{region}.cometchat.io/v3/campaigns +``` + +Regions: `us`, `eu`, `in`. Replace `{appId}` with your application ID. + +### Credentials + +Generate a **REST API key** with `fullAccess` scope from your CometChat Dashboard. Send it on every request as the `apikey` header. + +```http +apikey: +``` + +App scoping is encoded in the base URL (`https://{appId}.api-{region}.cometchat.io/v3/campaigns`), so you do not need a separate `appid` header. + +### Sample headers + +```http +apikey: +content-type: application/json +``` + +### Common auth errors + +```jsonc +// 401. apikey missing or invalid +{ + "error": { + "code": "ERR_UNAUTHORIZED", + "message": "apikey header is required", + "source": "campaigns-service" + } +} +``` + +### Multi-tenancy + +Every entity is scoped to the `{appId}` in the base URL. Two apps cannot read each other's feed items, templates, or channels. Don't reuse an API key across apps; generate one per app from the Dashboard. + +For the full surface and the canonical response and error envelopes, see [Overview](/rest-api/campaigns-apis/overview). + +For the shared CometChat authentication model (API key scopes, auth tokens, security best practices), see [Authentication](/rest-api/authentication). diff --git a/rest-api/campaigns-apis/templates/get-template.mdx b/rest-api/campaigns-apis/templates/get-template.mdx new file mode 100644 index 000000000..9cc40f82f --- /dev/null +++ b/rest-api/campaigns-apis/templates/get-template.mdx @@ -0,0 +1,8 @@ +--- +openapi: get /templates/{templateId} +description: "Fetch a single template by CUID or slug." +--- + +Returns all versions and the current version's channels and variable schema. Versions are immutable, so saving an edit auto-bumps the `currentVersion`. + +For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/templates/list-templates.mdx b/rest-api/campaigns-apis/templates/list-templates.mdx new file mode 100644 index 000000000..33dd10f15 --- /dev/null +++ b/rest-api/campaigns-apis/templates/list-templates.mdx @@ -0,0 +1,8 @@ +--- +openapi: get /templates +description: "List templates for this app." +--- + +Paginated. Combine `category`, `status`, `search`, and `tags` to narrow the result set. + +For the complete error reference, see [Error Guide](/articles/error-guide). From b4d4ac47448418c27c2ec38d064f3fb368d4776a Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Tue, 26 May 2026 16:01:30 +0530 Subject: [PATCH 02/45] docs(campaigns): Restructure campaigns documentation with detailed API guide - Reorganize campaigns.mdx as overview with key capabilities and setup flow - Add new campaigns subdirectory with dedicated docs for channels, categories, templates, users, webhooks, and notification-settings - Expand API documentation with detailed endpoint reference, request/response examples, and field descriptions - Add prerequisites, limits, and supported channels reference tables - Include step-by-step setup instructions and common use case descriptions - Update docs.json to reflect new documentation structure and navigation hierarchy - Improve content organization for better discoverability and developer onboarding --- campaigns.mdx | 150 +++++++-- campaigns/campaigns.mdx | 102 ++++++ campaigns/categories.mdx | 52 ++++ campaigns/channels.mdx | 52 ++++ campaigns/notification-settings.mdx | 52 ++++ campaigns/templates.mdx | 90 ++++++ campaigns/users.mdx | 42 +++ campaigns/webhooks.mdx | 466 ++++++++++++++++++++++++++++ docs.json | 15 +- 9 files changed, 999 insertions(+), 22 deletions(-) create mode 100644 campaigns/campaigns.mdx create mode 100644 campaigns/categories.mdx create mode 100644 campaigns/channels.mdx create mode 100644 campaigns/notification-settings.mdx create mode 100644 campaigns/templates.mdx create mode 100644 campaigns/users.mdx create mode 100644 campaigns/webhooks.mdx diff --git a/campaigns.mdx b/campaigns.mdx index e3b28da6a..d8703e922 100644 --- a/campaigns.mdx +++ b/campaigns.mdx @@ -1,39 +1,147 @@ --- -title: "Campaigns" +title: "Overview" +sidebarTitle: "Overview" description: "Send transactional notifications, run scheduled campaigns, and manage the per-user in-app feed across in-app and push channels." --- -The CometChat **Campaigns** product is a managed notification surface for sending the right message on the right channel at the right time. It pairs a content layer (templates, variables, categories, tags) with a delivery layer (real-time sends, scheduled campaigns, CSV-driven batches) and a per-user in-app notification feed that your application reads with the SDK. +CometChat Campaigns is a notification management system that lets you design, target, and deliver in-app and push notifications to your users. It provides a visual dashboard for creating notification templates, managing delivery channels, and sending targeted campaigns — either immediately or on a schedule. Your backend can also send notifications programmatically via API. -### What you can do +## Key Capabilities -- **Send transactional notifications.** Deliver a templated message to one or many users in real time, with variable resolution applied per recipient. -- **Organise content with templates.** Author reusable templates with typed variables, categories, and tags. Every notification flows through one. -- **Run scheduled and batch campaigns.** Send to a handful of users in real time or to tens of thousands through inline batch and CSV-driven flows. -- **Track delivery and engagement.** Capture delivered, read, viewed, clicked, and interacted signals so your analytics and unread counts stay accurate. -- **Administer the in-app feed.** Soft-delete feed items, audit per-user delivery, and manage retention. +- **Send transactional notifications** — Deliver a templated message to one or many users in real time, with per-recipient variable resolution. +- **Organise content with templates** — Author reusable templates with typed variables, categories, and tags. Every notification flows through a template. +- **Run scheduled and batch campaigns** — Send to a handful of users immediately or to tens of thousands through batch and CSV-driven flows. +- **Track delivery and engagement** — Capture delivered, read, clicked, and interacted signals for analytics and unread counts. +- **Administer the in-app feed** — Soft-delete feed items, audit per-user delivery, and manage retention. -### Common use cases +## Supported Channels -#### Transactional alerts +| Channel | Description | +|---------|-------------| +| `in_app` | In-app notification feed rendered client-side via SDK | +| `push` | Push notifications delivered via FCM/APNs through CometChat's push infrastructure | -Order shipped, payment receipt, password reset, security alert. One templated message per event, dispatched in real time, with per-recipient variable substitution (`{{order_id}}`, `{{user_name}}`). +## Setup Flow -#### Marketing campaigns +The typical setup to send your first notification: -Product launches, promotional offers, re-engagement nudges. Schedule the send, upload a recipient list, and let the campaign worker fan out across in-app and push. + + + Go to Channels → Create Channel → Select type (`in_app` or `push`) → Name it → Save. + + + Go to Categories → Create → Name it (e.g., "Marketing", "Alerts"). + + + Go to Templates → Create Template → Enter name → Select channel(s) → Design content in the visual builder → Set status to "Approved". + + + Either via Dashboard (Create a Campaign → Select template → Add recipients → Send Now) or via API (`POST /notifications/messages`). + + + +## Prerequisites + +- A CometChat app with `appId` and `apiKey` +- At least one enabled channel created +- At least one approved template +- Users registered in CometChat (targeted by their UID) + +## Limits + +| Resource | Limit | +|----------|-------| +| Recipients per notification (realtime) | 10 | +| Recipients per notification (batch) | 10,000 | +| Pagination page size | Max 100 | +| Max campaigns | No hard limit | +| Max templates | No hard limit | + +## Send Notifications via API + +You can send notifications directly from your backend without creating a campaign in the dashboard. This is ideal for transactional or event-driven notifications (e.g., "Your order shipped", "New message received"). + +**Endpoint:** `POST /notifications/messages` + +**Headers:** +- `appid` — your CometChat app ID +- `apikey` — your CometChat API key + +**Request Body:** -#### Operational messages +```json +{ + "templateId": "cc-template-order-shipped", + "receivers": ["user-123", "user-456"], + "variables": { + "user-123": { "orderNumber": "ORD-789", "name": "John" }, + "user-456": { "orderNumber": "ORD-790", "name": "Jane" } + } +} +``` -Maintenance windows, policy updates, account changes. Same template authoring flow, typically dispatched to a broad audience and routed through the in-app feed so users can revisit them. +**Fields:** -### Who this is for +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `templateId` | string | Yes | The template slug (e.g., `cc-template-welcome`) | +| `receivers` | string[] | Yes | Array of user UIDs (1 to 10,000) | +| `variables` | object | No | Per-user variable values keyed by UID | +| `tag` | string | No | Optional analytics tag | -This section documents the **server-side REST API**. It's the surface your back-end, the CometChat Dashboard, and your ops tools call to manage templates, channels, and sends. It is not meant for direct use from a browser or mobile client. +**Delivery Modes:** +- ≤ 10 receivers → **Realtime** (synchronous, immediate delivery) +- \> 10 receivers → **Batch** (queued, processed asynchronously) + +**Requirements:** +- Template must be in `approved` status +- Called from your server (admin-only — `onbehalfof` header is rejected) + +**Response (realtime):** + +```json +{ + "notificationId": "abc123", + "channels": ["in_app"], + "mode": "realtime" +} +``` + +**Response (batch):** + +```json +{ + "batchId": "xyz789", + "total": 500, + "channels": ["in_app", "push"], + "mode": "batch" +} +``` + +## Common Use Cases + +### Transactional alerts +Order shipped, payment receipt, password reset, security alert. One templated message per event, dispatched in real time, with per-recipient variable substitution (`{{order_id}}`, `{{user_name}}`). + +### Marketing campaigns +Product launches, promotional offers, re-engagement nudges. Schedule the send, upload a recipient list, and let the campaign worker fan out across in-app and push. -End-user clients (a mobile app reading its own feed, an in-app bell widget) should integrate with the **Campaigns SDK** instead, which exposes the feed read, unread count, mark-delivered/read, and engagement-tracking surfaces in a way that's safe for client-side use. +### Operational messages +Maintenance windows, policy updates, account changes. Dispatched to a broad audience and routed through the in-app feed so users can revisit them. -### Start here +## Quick Navigation -- [REST API Overview](/rest-api/campaigns-apis/overview). Endpoint reference, response envelope, error codes. -- [Setup & Authentication](/rest-api/campaigns-apis/setup-and-authentication). Credentials, headers, base URL. + + + Configure delivery channels for in-app and push notifications + + + Organize notifications with categories for filtering + + + Create reusable notification templates with variables and versioning + + + Create and manage targeted notification campaigns + + diff --git a/campaigns/campaigns.mdx b/campaigns/campaigns.mdx new file mode 100644 index 000000000..db8e0b77a --- /dev/null +++ b/campaigns/campaigns.mdx @@ -0,0 +1,102 @@ +--- +title: "Campaigns" +sidebarTitle: "Campaigns" +description: "Create, schedule, and manage targeted notification campaigns for batch user delivery." +--- + +A campaign is a batch notification send to a targeted list of users. Campaigns provide scheduling, delivery tracking, and lifecycle management on top of the core notification send. + +## Campaign vs Direct Notification + +| | Campaign | Direct Notification | +|-|----------|-------------------| +| Entry point | Dashboard wizard | API: `POST /notifications/messages` | +| Recipients | Manual list, CSV upload | `receivers` array in request body | +| Scheduling | Immediate or scheduled | Always immediate | +| Tracking | Full lifecycle (status, sent/failed counts) | Notification row created, no campaign-level tracking | +| Sequences | Supported (multi-step) | Not supported | + +## Creating a Campaign + +The dashboard wizard walks you through four steps: + + + + Choose your input mode: + - **Manual** — Enter user IDs directly + - **CSV** — Upload a file with a `user_id` column and optional variable columns + - **User picker** — Select from the Users list + + + Pick an approved template. The template determines the content and delivery channels. + + + Enter campaign name, set per-user variables, and add an optional analytics tag. + + + Review the summary. Choose **Send Now** or **Schedule** for a future date/time. + + + +## Scheduling + +| Option | Description | +|--------|-------------| +| Send Now | Immediate dispatch | +| Schedule | Set a future date/time (Unix timestamp) | +| Recurring | Not supported | + + +You can click "Send Now" on a scheduled campaign to override the schedule and send immediately. + + +## Campaign Status Flow + +```mermaid +flowchart LR + A[draft] --> B[sending] + A --> C[scheduled] + C --> B + C --> D[cancelled] + B --> E[completed] + B --> F[failed] + B --> G[partially_failed] +``` + +| Status | Description | +|--------|-------------| +| `draft` | Campaign created but not yet sent or scheduled | +| `scheduled` | Queued for future delivery | +| `sending` | Currently dispatching to recipients | +| `completed` | All recipients processed successfully | +| `failed` | All recipients failed | +| `partially_failed` | Some recipients succeeded, some failed | +| `cancelled` | Cancelled before send time | + +## Cancel and Delete + +- **Cancel** — Cancels a scheduled or draft campaign. Cannot cancel once `sending` has started. +- **Pause** — Not supported. Once sending starts, it runs to completion. +- **Delete** — Only draft campaigns can be deleted. Cancelled campaigns cannot be deleted. + +## Campaign Properties + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | Campaign name | +| `templateId` | string | Template CUID | +| `templateVersion` | number | Pinned template version | +| `status` | enum | `draft` \| `scheduled` \| `sending` \| `completed` \| `failed` \| `partially_failed` \| `cancelled` | +| `scheduledAt` | number | Unix timestamp (null for immediate) | +| `totalTargets` | number | Total recipients | +| `sentCount` | number | Successfully delivered | +| `failedCount` | number | Failed deliveries | +| `tag` | string | Optional analytics tag | + +## Limits + +| Limit | Value | +|-------|-------| +| Max recipients per campaign | 10,000 | +| Max concurrent campaigns | No hard limit | +| Max campaigns per app | No hard limit | diff --git a/campaigns/categories.mdx b/campaigns/categories.mdx new file mode 100644 index 000000000..067475f1d --- /dev/null +++ b/campaigns/categories.mdx @@ -0,0 +1,52 @@ +--- +title: "Categories" +sidebarTitle: "Categories" +description: "Organize notifications with categories for filtering and user-facing feed segmentation." +--- + +Categories are labels used to organize templates and filter notification feed items. When a template has a category assigned, all notifications sent with that template carry the category — allowing end-users to filter their feed by category. + +## How Categories Work + +```mermaid +flowchart LR + A[Category] --> B[Template] + B --> C[Feed Item] + C --> D[User Feed filtered by category] +``` + +- A template has a `templateCategory` field (category name) +- Feed items inherit `templateCategory` and `categoryId` from the template +- The feed API supports filtering: `GET /notification-feed?category=Marketing` +- The `categoryFilterEnabled` flag on the template channel controls whether filtering is active + +## Managing Categories + +1. Go to **Categories** in the sidebar +2. **Create** — Click "Create Category" → Enter name and optional description → Save +3. **Edit** — Click a category → Update name or description → Save +4. **Delete** — Click delete → Confirm removal + +## Category Properties + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | Category name (unique per app) | +| `description` | string | Optional description | + +## Feed Filtering + +End-users can filter their notification feed by category using the SDK or feed API: + +``` +GET /notification-feed?category=Marketing +``` + +This only works when `categoryFilterEnabled` is set to `true` on the template's channel configuration. + +## Limits + +| Limit | Value | +|-------|-------| +| Max categories per app | No hard limit | +| Duplicate names | Not allowed (returns 409 error) | diff --git a/campaigns/channels.mdx b/campaigns/channels.mdx new file mode 100644 index 000000000..6fbcb2c14 --- /dev/null +++ b/campaigns/channels.mdx @@ -0,0 +1,52 @@ +--- +title: "Channels" +sidebarTitle: "Channels" +description: "Configure delivery channels for your campaign notifications — in-app feed and push notifications." +--- + +Channels define where notifications are delivered. Each template references one or more channel instances, and each channel instance is tied to a specific delivery type. + +## Supported Channel Types + +| Type | Description | +|------|-------------| +| `in_app` | Feed notifications rendered by SDK | +| `push` | Lock-screen notifications via FCM/APNs | + +## Creating a Channel + +1. Go to **Channels** → Click **Create Channel** +2. Select the channel type (`in_app` or `push`) +3. Configure the channel: + - **Name** — Display name (e.g., "Default Feed", "Promotions Feed") + - **Channel ID** — Auto-generated slug: `cc-notification-channel-` (immutable after creation) + - **Enabled** — Toggle on/off +4. Click **Save** + +## Channel Properties + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | Display name | +| `channelId` | string | Unique slug (immutable after creation) | +| `type` | enum | `in_app` \| `push` | +| `enabled` | boolean | Whether channel is available for template assignment | + + +Push notification configuration (FCM/APNs keys) is managed at the CometChat app level, not per-channel in Campaigns. + + +## Multiple Channels + +You can create multiple channels of the same type. For example: +- "Promotions Feed" (`in_app`) — for marketing notifications +- "Alerts Feed" (`in_app`) — for transactional alerts + +Templates reference a specific channel instance by its `channelId`. + +## Limits + +| Limit | Value | +|-------|-------| +| `in_app` channels per app | No hard limit | +| `push` channels per app | No hard limit | diff --git a/campaigns/notification-settings.mdx b/campaigns/notification-settings.mdx new file mode 100644 index 000000000..e270d8781 --- /dev/null +++ b/campaigns/notification-settings.mdx @@ -0,0 +1,52 @@ +--- +title: "Notification Settings" +sidebarTitle: "Notification Settings" +description: "App-level configuration for campaign notification behavior including retention, delivery mechanisms, and defaults." +--- + +Notification Settings is an app-level configuration that controls global behavior for the campaigns service. These settings apply to the entire app — they are not per-user preferences. + +## Available Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `retentionDays` | number | 90 | Days before feed items are purged from the database | +| `defaultChannels` | string[] | `[]` | Default channel types pre-selected for new templates | +| `realtimeFanout` | string[] | `[]` | Realtime delivery mechanisms. Values: `["websocket"]` | +| `config` | object | `{}` | Arbitrary key-value configuration | + +## Realtime Fanout + +Setting `realtimeFanout: ["websocket"]` enables WebSocket delivery for in-app notifications. Without this, feed items are only available via polling the feed API. + +## Default Channels + +The `defaultChannels` setting pre-selects channel types when creating new templates in the dashboard. This is a convenience setting — it does not restrict which channels can be used. + +## Retention + +The `retentionDays` setting controls how long feed items are stored in the database before the daily purge job hard-deletes them. + + +This is separate from `messageRetentionHours` on individual templates, which controls when a feed item disappears from the user's feed (soft expiry). The global `retentionDays` controls the database-level cleanup. + + +## What Is Not Configurable + +| Feature | Status | +|---------|--------| +| Per-user preferences (opt-in/opt-out) | Not supported | +| Retry policy | Not configurable | +| Delivery priority | Not configurable | +| Quiet hours | Not supported | +| Rate limiting | Not configurable | + +## Configuration + +This setting is configured via an internal API and is not exposed in the dashboard UI: + +```bash +PUT /settings?appId= +Header: x-internal-api-key: +Body: { "realtimeFanout": ["websocket"], "retentionDays": 30 } +``` diff --git a/campaigns/templates.mdx b/campaigns/templates.mdx new file mode 100644 index 000000000..e843de76b --- /dev/null +++ b/campaigns/templates.mdx @@ -0,0 +1,90 @@ +--- +title: "Templates" +sidebarTitle: "Templates" +description: "Create reusable notification templates with variables, versioning, and multi-channel support." +--- + +A template is a reusable notification design that defines the content, delivery channels, and personalization variables for a notification. Every notification — whether sent via campaign or API — flows through a template. + +## Template Properties + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Display name | +| `templateId` | string | Auto-generated | Slug: `cc-template-` (immutable) | +| `templateCategory` | string | No | Category name (e.g., "Marketing") | +| `label` | string | No | Display label shown on notification (e.g., "Promo") | +| `alternativeText` | string | No | Plain-text fallback when rich content can't render | +| `tags` | string[] | No | Tags for filtering and segmentation | +| `status` | enum | Yes | `draft` \| `approved` \| `archived` | +| `channels` | array | Yes | Channel configurations (at least one) | +| `variableSchema` | array | No | Variable definitions | + +## Content Types + +| dataType | Description | +|----------|-------------| +| `ui_template` | Visual template designed in the Bubble Builder (drag-and-drop editor) | +| `data_template` | Raw JSON payload for custom SDK rendering | + +## Variables + +Variables allow per-recipient personalization in notification content. + +- **Syntax**: `{{variable_name}}` in template content +- **Naming**: Letters, numbers, dots, and underscores only (`^[a-zA-Z_][a-zA-Z0-9_.]*`) +- **Schema**: Each variable is defined with `key`, `type`, `name` (label), `required`, and `defaultValue` +- **Resolution**: Per-user values are passed at send time in the `variables` field + +## Template Versioning + +Templates are versioned to maintain a history of changes: + +- Templates start at version 1 +- Editing an **approved** template auto-bumps the version (creates a new snapshot) +- You can manually create a version via the "New Version" button +- Campaigns pin to a specific `templateVersion` at send time +- Old versions are immutable — safe for historical reference + +## Channel Configuration + +Each template has one or more channel configurations: + +| Field | Type | Description | +|-------|------|-------------| +| `channelType` | string | `in_app` \| `push` | +| `channelId` | string | Links to a specific Channel entity | +| `content` | object | Notification content (Bubble Builder JSON or custom) | +| `dataType` | string | `ui_template` \| `data_template` | +| `messageRetentionHours` | number | Hours before feed item expires (0 = never) | +| `categoryFilterEnabled` | boolean | Enable category-based filtering in feed | +| `templateLabelEnabled` | boolean | Show label badge on notification | + +For sequence campaigns, additional fields are available: + +| Field | Type | Description | +|-------|------|-------------| +| `stopCondition` | string | `delivered` \| `read` \| `engaged` (in_app) or `delivered` \| `clicked` (push) | +| `waitMinutes` | number | Wait time between steps: 5, 10, 30, 60, 240, or 1440 | +| `sequenceOrder` | number | Position in sequence (1, 2, 3...) | + +## Template Statuses + +| Status | Description | +|--------|-------------| +| `draft` | Work in progress — cannot be used to send | +| `approved` | Ready to use — can be selected for campaigns and notifications | +| `archived` | Soft-deleted — hidden from lists, not usable | + + +Only templates with `approved` status can be used to send notifications. + + +## Limits + +| Limit | Value | +|-------|-------| +| Max templates per app | No hard limit | +| Max channels per template | No hard limit (practical: 2–3) | +| Max variables per template | No hard limit | +| Max versions per template | No hard limit | diff --git a/campaigns/users.mdx b/campaigns/users.mdx new file mode 100644 index 000000000..343d518da --- /dev/null +++ b/campaigns/users.mdx @@ -0,0 +1,42 @@ +--- +title: "Users" +sidebarTitle: "Users" +description: "Browse and target CometChat users for campaign notifications." +--- + +A user in Campaigns is the same as a CometChat user — identified by their `uid`. The Users page in the dashboard lets you browse users registered in your CometChat app. No separate user creation is needed for Campaigns. + +## Targeting Users + +When creating a campaign, you can target users through the following methods: + +| Method | Description | +|--------|-------------| +| Manual UID entry | Type user IDs directly when creating a campaign | +| CSV upload | Upload a CSV file with a `user_id` column and optional per-user variables | +| User picker | Select from the Users list in the dashboard | + + +There is no segment-based targeting (e.g., "all users who signed up last week"). Targeting is explicit — you provide the list of UIDs. + + +## Filtering Users + +The Users page supports filtering by: + +| Filter | Description | +|--------|-------------| +| Search | Filter by UID or name | +| Role | Filter by CometChat user role | +| Status | Filter by online / offline status | + +## User Preferences + +User-level notification preferences (opt-in/opt-out) are not currently supported. All users in the recipient list receive the notification. + +## Limits + +| Limit | Value | +|-------|-------| +| Max recipients per campaign | 10,000 | +| Users list page size | Configurable (default 20, max 100) | diff --git a/campaigns/webhooks.mdx b/campaigns/webhooks.mdx new file mode 100644 index 000000000..773fe78be --- /dev/null +++ b/campaigns/webhooks.mdx @@ -0,0 +1,466 @@ +--- +title: "Webhooks" +sidebarTitle: "Webhooks" +description: "Track campaign delivery and engagement in real-time with webhook events for campaigns, notifications, feed items, and push notifications." +--- + +Campaign webhook events are triggered during the lifecycle of campaigns, notifications, feed items, and push notifications. These events allow you to track delivery and engagement in real-time. + + +Campaign webhooks use the same webhook infrastructure as other CometChat webhooks. Configure your webhook endpoint in the [Webhooks settings](/rest-api/management-apis/webhooks/overview). + + +## Event Types + +| Trigger | Description | +|---------|-------------| +| [`after_campaign_completed`](#after_campaign_completed) | Campaign finishes sending to all targets | +| [`after_campaign_failed`](#after_campaign_failed) | Campaign fails to deliver to its targets | +| [`after_notification_created`](#after_notification_created) | Notification is created and begins dispatching | +| [`after_feed_item_sent`](#after_feed_item_sent) | In-app feed item is sent to a user | +| [`after_feed_item_delivered`](#after_feed_item_delivered) | Feed item is delivered to the user's device | +| [`after_feed_item_read`](#after_feed_item_read) | User reads a feed item | +| [`after_feed_item_interacted`](#after_feed_item_interacted) | User interacts with a feed item | +| [`after_push_notification_sent`](#after_push_notification_sent) | Push notification is sent | +| [`after_push_notification_delivered`](#after_push_notification_delivered) | Push notification is delivered to the user's device | +| [`after_push_notification_clicked`](#after_push_notification_clicked) | User clicks on a push notification | + +## Status Flow + +**Feed Items:** `sent` → `delivered` → `read` → `interacted` + +**Push Notifications:** `sent` → `delivered` → `clicked` + +**Campaigns:** `dispatching` → `completed` / `failed` + +--- + +## Campaign Events + +### after\_campaign\_completed + +Triggered when a campaign finishes sending to all targets. + + + +```json after_campaign_completed +{ + "trigger": "after_campaign_completed", + "data": { + "campaignId": "", + "appId": "", + "name": "Welcome Campaign", + "status": "completed", + "templateId": "", + "templateVersion": 1, + "totalTargets": 100, + "sentCount": 100, + "failedCount": 0, + "tag": "onboarding", + "scheduledAt": 1696930000, + "sentAt": 1696932000, + "completedAt": 1696932060, + "createdAt": 1696929000, + "updatedAt": 1696932060 + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +### after\_campaign\_failed + +Triggered when a campaign fails to deliver to its targets. + + + +```json after_campaign_failed +{ + "trigger": "after_campaign_failed", + "data": { + "campaignId": "", + "appId": "", + "name": "Promo Campaign", + "status": "failed", + "templateId": "", + "templateVersion": 1, + "totalTargets": 50, + "sentCount": 0, + "failedCount": 50, + "tag": "promo", + "scheduledAt": 1696930000, + "sentAt": 1696932000, + "completedAt": 1696932070, + "createdAt": 1696929000, + "updatedAt": 1696932070 + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +--- + +## Notification Events + +### after\_notification\_created + +Triggered when a notification is created and begins dispatching. + + + +```json after_notification_created +{ + "trigger": "after_notification_created", + "data": { + "notificationId": "", + "appId": "", + "templateId": "", + "templateVersion": 1, + "category": "updates", + "label": "weekly-digest", + "tags": ["digest", "weekly"], + "sendMode": "realtime", + "campaignId": "", + "priority": "normal", + "channels": ["in_app"], + "dataType": "ui_template", + "status": "dispatching", + "totalTargets": 3, + "sentCount": 0, + "failedCount": 0, + "createdAt": 1696932000 + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +--- + +## Feed Item Events + +### after\_feed\_item\_sent + +Triggered when an in-app feed item is sent to a user. + + + +```json after_feed_item_sent +{ + "trigger": "after_feed_item_sent", + "data": { + "id": "", + "appId": "", + "notificationId": "", + "campaignId": "", + "receiver": "cometchat-uid-1", + "templateId": "", + "templateVersion": 1, + "channelType": "in_app", + "channelId": "", + "templateCategory": "promotions", + "categoryId": "", + "label": "promo", + "tags": ["offer", "summer"], + "alternativeText": "You have a new offer!", + "dataType": "ui_template", + "status": "sent", + "sentAt": 1696932000, + "realtimeFanout": ["websocket"], + "content": { + "version": "1.0", + "body": [ + {"id": "el_1", "type": "text", "content": "Hello!", "variant": "heading2"}, + {"id": "el_2", "type": "divider"}, + {"id": "el_3", "type": "text", "content": "Your notification message here.", "variant": "body"} + ], + "style": { + "background": {"light": "#E8E8E8", "dark": "#E8E8E8"}, + "borderRadius": 16, + "borderColor": {"light": "#DFE6E9", "dark": "#DFE6E9"}, + "padding": 12 + } + } + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +### after\_feed\_item\_delivered + +Triggered when a feed item is delivered to the user's device. + + + +```json after_feed_item_delivered +{ + "trigger": "after_feed_item_delivered", + "data": { + "id": "", + "appId": "", + "notificationId": "", + "campaignId": "", + "receiver": "cometchat-uid-1", + "templateId": "", + "templateVersion": 1, + "channelType": "in_app", + "channelId": "", + "templateCategory": "promotions", + "categoryId": "", + "label": "promo", + "tags": ["offer", "summer"], + "alternativeText": "You have a new offer!", + "dataType": "ui_template", + "status": "delivered", + "sentAt": 1696932000, + "deliveredAt": 1696932060, + "engagedAt": 1696932055, + "realtimeFanout": ["websocket"] + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +### after\_feed\_item\_read + +Triggered when a user reads a feed item. + + + +```json after_feed_item_read +{ + "trigger": "after_feed_item_read", + "data": { + "id": "", + "appId": "", + "notificationId": "", + "campaignId": "", + "receiver": "cometchat-uid-1", + "templateId": "", + "templateVersion": 1, + "channelType": "in_app", + "channelId": "", + "templateCategory": "promotions", + "categoryId": "", + "label": "promo", + "tags": ["offer", "summer"], + "alternativeText": "You have a new offer!", + "dataType": "ui_template", + "variables": {}, + "status": "read", + "sentAt": 1696932000, + "deliveredAt": 1696932060, + "readAt": 1696932120, + "metadata": {}, + "realtimeFanout": ["websocket"] + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +### after\_feed\_item\_interacted + +Triggered when a user interacts with a feed item (e.g., clicks a button or link). + + + +```json after_feed_item_interacted +{ + "trigger": "after_feed_item_interacted", + "data": { + "id": "", + "appId": "", + "notificationId": "", + "campaignId": "", + "receiver": "cometchat-uid-1", + "templateId": "", + "templateVersion": 1, + "channelType": "in_app", + "channelId": "", + "templateCategory": "promotions", + "categoryId": "", + "label": "promo", + "tags": ["offer", "summer"], + "alternativeText": "You have a new offer!", + "dataType": "ui_template", + "variables": {}, + "status": "read", + "sentAt": 1696932000, + "deliveredAt": 1696932060, + "readAt": 1696932120, + "interactedAt": 1696932180, + "metadata": {}, + "realtimeFanout": ["websocket"] + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +--- + +## Push Notification Events + +### after\_push\_notification\_sent + +Triggered when a push notification is sent. + + + +```json after_push_notification_sent +{ + "trigger": "after_push_notification_sent", + "data": { + "pushNotificationId": "", + "appId": "", + "notificationId": "", + "campaignId": "", + "receiver": "cometchat-uid-1", + "templateId": "", + "templateVersion": 1, + "channelType": "push", + "channelId": "", + "category": "promotions", + "label": "summer-sale", + "tags": ["offer", "summer"], + "alternativeText": "Check out our summer sale!", + "dataType": "ui_template", + "variables": {}, + "status": "sent", + "sentAt": 1696932000, + "tag": "promo", + "metadata": {} + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +### after\_push\_notification\_delivered + +Triggered when a push notification is delivered to the user's device. + + + +```json after_push_notification_delivered +{ + "trigger": "after_push_notification_delivered", + "data": { + "pushNotificationId": "", + "appId": "", + "notificationId": "", + "campaignId": "", + "receiver": "cometchat-uid-1", + "templateId": "", + "templateVersion": 1, + "channelType": "push", + "channelId": "", + "category": "promotions", + "label": "summer-sale", + "tags": ["offer", "summer"], + "alternativeText": "Check out our summer sale!", + "dataType": "ui_template", + "variables": {}, + "status": "delivered", + "sentAt": 1696932000, + "deliveredAt": 1696932005, + "tag": "promo" + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +### after\_push\_notification\_clicked + +Triggered when a user clicks/taps on a push notification. + + + +```json after_push_notification_clicked +{ + "trigger": "after_push_notification_clicked", + "data": { + "pushNotificationId": "", + "appId": "", + "notificationId": "", + "campaignId": "", + "receiver": "cometchat-uid-1", + "templateId": "", + "templateVersion": 1, + "channelType": "push", + "channelId": "", + "category": "promotions", + "label": "summer-sale", + "tags": ["offer", "summer"], + "alternativeText": "Check out our summer sale!", + "dataType": "ui_template", + "variables": {}, + "status": "clicked", + "sentAt": 1696932000, + "deliveredAt": 1696932005, + "clickedAt": 1696932060, + "tag": "promo", + "metadata": {} + }, + "appId": "", + "region": "", + "webhook": "" +} +``` + + + +--- + +## Common Fields + +| Field | Description | +|-------|-------------| +| `trigger` | The event name | +| `appId` | Your CometChat app ID | +| `region` | The region of the app | +| `webhook` | The webhook ID that received this event | +| `data.campaignId` | The campaign this event belongs to (null if sent directly via API) | +| `data.notificationId` | Unique identifier for the notification batch | +| `data.templateId` | The template used for this notification | +| `data.templateVersion` | Version of the template | +| `data.receiver` | The UID of the target user | +| `data.channelType` | Delivery channel: `in_app` or `push` | +| `data.channelId` | The channel configuration ID | +| `data.status` | Current status of the item | +| `data.tags` | Custom tags associated with the notification | +| `data.label` | Custom label for categorization | +| `data.variables` | Template variables resolved for the receiver | +| `data.metadata` | Custom metadata attached to the notification | diff --git a/docs.json b/docs.json index 5feb4a252..96a4d055c 100644 --- a/docs.json +++ b/docs.json @@ -6478,12 +6478,25 @@ "tab": "Campaigns", "pages": [ "campaigns", + { + "group": "Guides", + "icon": "grid-2", + "pages": [ + "campaigns/users", + "campaigns/channels", + "campaigns/templates", + "campaigns/campaigns", + "campaigns/categories", + "campaigns/notification-settings" + ] + }, { "group": "Features", "icon": "puzzle-piece", "pages": [ "campaigns/sequences", - "campaigns/analytics" + "campaigns/analytics", + "campaigns/webhooks" ] } ] From ee3911dcd53e8bb7dff571885ae1440310374352 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Tue, 26 May 2026 22:31:32 +0530 Subject: [PATCH 03/45] docs(campaigns): Expand campaigns API documentation with comprehensive endpoint guides - Add 50+ new endpoint documentation files for campaigns, categories, channels, notifications, and templates - Expand analytics documentation with detailed metric endpoints for campaigns, channels, templates, and users - Add notification feed API documentation with filtering, pagination, and engagement tracking - Add push notifications API documentation with delivery and engagement reporting - Update campaigns-apis.json OpenAPI specification with complete endpoint definitions and parameters - Update campaigns.mdx and related guide files with restructured content - Update docs.json to reflect new documentation structure and organization --- campaigns-apis.json | 2763 +++++++++++++++-- campaigns.mdx | 63 +- campaigns/campaigns.mdx | 3 +- campaigns/channels.mdx | 6 +- campaigns/notification-settings.mdx | 84 +- campaigns/users.mdx | 42 +- docs.json | 69 +- .../analytics/campaign-metrics.mdx | 20 +- .../analytics/channel-metrics.mdx | 14 +- .../analytics/overview-metrics.mdx | 22 +- .../analytics/template-metrics.mdx | 20 +- .../campaigns-apis/analytics/user-metrics.mdx | 43 +- .../campaigns/add-recipients.mdx | 4 + .../campaigns/cancel-campaign.mdx | 4 + .../campaigns/create-campaign.mdx | 4 + .../campaigns/delete-campaign.mdx | 4 + .../campaigns-apis/campaigns/get-campaign.mdx | 4 + .../campaigns-apis/campaigns/import-csv.mdx | 4 + .../campaigns/import-status.mdx | 4 + .../campaigns/list-campaigns.mdx | 4 + .../campaigns/list-recipients.mdx | 4 + .../campaigns/recipient-summary.mdx | 4 + .../campaigns/schedule-campaign.mdx | 4 + .../campaigns/send-campaign.mdx | 4 + .../campaigns/update-campaign.mdx | 4 + .../campaigns-apis/campaigns/upload-url.mdx | 4 + .../categories/create-category.mdx | 4 + .../categories/delete-category.mdx | 4 + .../categories/get-category.mdx | 4 + .../categories/list-categories.mdx | 4 + .../categories/update-category.mdx | 4 + .../channels/create-channel.mdx | 4 + .../campaigns-apis/channels/get-channel.mdx | 4 + .../channels/update-channel.mdx | 4 + .../notification-feed/get-feed-item.mdx | 4 + .../notification-feed/get-unread-count.mdx | 4 + .../notification-feed/list-feed.mdx | 4 + .../notification-feed/mark-as-delivered.mdx | 4 + .../notification-feed/mark-as-read.mdx | 4 + .../notification-feed/report-engagement.mdx | 4 + .../notifications/send-notification.mdx | 4 + .../get-push-notification.mdx | 4 + .../list-push-notifications.mdx | 4 + .../push-notifications/mark-clicked.mdx | 4 + .../push-notifications/mark-delivered.mdx | 4 + .../push-notifications/report-engagement.mdx | 4 + .../sequences/sequence-metrics.mdx | 20 +- .../templates/archive-template.mdx | 4 + .../templates/create-template.mdx | 4 + .../templates/create-version.mdx | 4 + .../templates/update-channel-content.mdx | 4 + .../templates/update-template.mdx | 4 + temp.md | 1 + 53 files changed, 2816 insertions(+), 510 deletions(-) create mode 100644 rest-api/campaigns-apis/campaigns/add-recipients.mdx create mode 100644 rest-api/campaigns-apis/campaigns/cancel-campaign.mdx create mode 100644 rest-api/campaigns-apis/campaigns/create-campaign.mdx create mode 100644 rest-api/campaigns-apis/campaigns/delete-campaign.mdx create mode 100644 rest-api/campaigns-apis/campaigns/get-campaign.mdx create mode 100644 rest-api/campaigns-apis/campaigns/import-csv.mdx create mode 100644 rest-api/campaigns-apis/campaigns/import-status.mdx create mode 100644 rest-api/campaigns-apis/campaigns/list-campaigns.mdx create mode 100644 rest-api/campaigns-apis/campaigns/list-recipients.mdx create mode 100644 rest-api/campaigns-apis/campaigns/recipient-summary.mdx create mode 100644 rest-api/campaigns-apis/campaigns/schedule-campaign.mdx create mode 100644 rest-api/campaigns-apis/campaigns/send-campaign.mdx create mode 100644 rest-api/campaigns-apis/campaigns/update-campaign.mdx create mode 100644 rest-api/campaigns-apis/campaigns/upload-url.mdx create mode 100644 rest-api/campaigns-apis/categories/create-category.mdx create mode 100644 rest-api/campaigns-apis/categories/delete-category.mdx create mode 100644 rest-api/campaigns-apis/categories/get-category.mdx create mode 100644 rest-api/campaigns-apis/categories/list-categories.mdx create mode 100644 rest-api/campaigns-apis/categories/update-category.mdx create mode 100644 rest-api/campaigns-apis/channels/create-channel.mdx create mode 100644 rest-api/campaigns-apis/channels/get-channel.mdx create mode 100644 rest-api/campaigns-apis/channels/update-channel.mdx create mode 100644 rest-api/campaigns-apis/notification-feed/get-feed-item.mdx create mode 100644 rest-api/campaigns-apis/notification-feed/get-unread-count.mdx create mode 100644 rest-api/campaigns-apis/notification-feed/list-feed.mdx create mode 100644 rest-api/campaigns-apis/notification-feed/mark-as-delivered.mdx create mode 100644 rest-api/campaigns-apis/notification-feed/mark-as-read.mdx create mode 100644 rest-api/campaigns-apis/notification-feed/report-engagement.mdx create mode 100644 rest-api/campaigns-apis/notifications/send-notification.mdx create mode 100644 rest-api/campaigns-apis/push-notifications/get-push-notification.mdx create mode 100644 rest-api/campaigns-apis/push-notifications/list-push-notifications.mdx create mode 100644 rest-api/campaigns-apis/push-notifications/mark-clicked.mdx create mode 100644 rest-api/campaigns-apis/push-notifications/mark-delivered.mdx create mode 100644 rest-api/campaigns-apis/push-notifications/report-engagement.mdx create mode 100644 rest-api/campaigns-apis/templates/archive-template.mdx create mode 100644 rest-api/campaigns-apis/templates/create-template.mdx create mode 100644 rest-api/campaigns-apis/templates/create-version.mdx create mode 100644 rest-api/campaigns-apis/templates/update-channel-content.mdx create mode 100644 rest-api/campaigns-apis/templates/update-template.mdx create mode 100644 temp.md diff --git a/campaigns-apis.json b/campaigns-apis.json index 6e2a24429..7fd1df173 100644 --- a/campaigns-apis.json +++ b/campaigns-apis.json @@ -1,362 +1,2569 @@ { "openapi": "3.0.0", - "info": { - "title": "Campaigns API", - "description": "Server-side REST API for CometChat Campaigns. Send transactional in-app and push notifications, manage templates and channels, and administer the notification feed. For backend integrators and the CometChat Dashboard. SDK-facing endpoints for end-user clients are documented separately.", - "version": "3.0" - }, - "servers": [ - { - "url": "https://{appId}.api-{region}.cometchat.io/v3/campaigns", - "variables": { - "appId": { - "default": "appId", - "description": "(Required) App ID" - }, - "region": { - "enum": ["us", "eu", "in"], - "default": "us", - "description": "Select Region" - } + "paths": { + "/notification-feed/unread-count": { + "get": { + "operationId": "NotificationFeedController_getUnreadCount", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of user making client request", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "templateCategory", + "required": true, + "in": "query", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "" } }, + "summary": "Get unread count for the requesting user", + "tags": ["Notification Feed"] + } + }, + "/notification-feed": { + "get": { + "operationId": "NotificationFeedController_findFeed", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of user making client request", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "readState", + "required": false, + "in": "query", + "description": "Filter by read state", + "schema": { "type": "string", "enum": ["read", "unread", "all"] } + }, + { + "name": "dateFrom", + "required": false, + "in": "query", + "description": "Start date filter (unix timestamp in seconds)", + "schema": { "type": "number" } + }, + { + "name": "dateTo", + "required": false, + "in": "query", + "description": "End date filter (unix timestamp in seconds)", + "schema": { "type": "number" } + }, + { + "name": "tags", + "required": false, + "in": "query", + "description": "Comma-separated tags to filter by", + "schema": { "type": "string" } + }, + { + "name": "tagMatch", + "required": false, + "in": "query", + "description": "Tag matching strategy: 'any' (OR) or 'all' (AND)", + "schema": { "type": "string", "enum": ["any", "all"] } + }, + { + "name": "templateCategory", + "required": false, + "in": "query", + "description": "Filter by templateCategory (per-app TemplateCategory.name)", + "schema": { "type": "string" } + }, + { + "name": "channelId", + "required": false, + "in": "query", + "description": "Filter by in-app channel instance ID", + "schema": { "type": "string" } + }, + { + "name": "includeDeleted", + "required": false, + "in": "query", + "description": "Include soft-deleted feed items", + "schema": { "default": false, "type": "boolean" } + }, + { + "name": "includeExpired", + "required": false, + "in": "query", + "description": "Include expired feed items", + "schema": { "default": false, "type": "boolean" } + }, + { + "name": "sentAt", + "required": false, + "in": "query", + "description": "Cursor: sentAt unix timestamp of last item from previous page", + "schema": { "type": "number" } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Cursor: id of last item from previous page", + "schema": { "type": "string" } + }, + { + "name": "affix", + "required": false, + "in": "query", + "description": "Cursor direction", + "schema": { "type": "string", "enum": ["append", "prepend"] } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Number of items per page", + "schema": { "minimum": 1, "maximum": 100, "type": "number" } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "Field to sort by", + "schema": { "type": "string", "enum": ["sentAt", "createdAt"] } + }, + { + "name": "order", + "required": false, + "in": "query", + "description": "Sort direction", + "schema": { "type": "string", "enum": ["asc", "desc"] } + }, + { + "name": "receiver", + "required": false, + "in": "query", + "description": "Admin-only: scope to a specific user. Ignored when onbehalfof is present.", + "schema": { "type": "string" } + }, + { + "name": "templateOnly", + "required": false, + "in": "query", + "description": "Pass \"true\" to opt out of server-side rendering. Default: false (server renders and returns content per item).", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "" } }, + "summary": "Query feed with filters and cursor pagination", + "tags": ["Notification Feed"] + } + }, + "/notification-feed/{id}": { + "get": { + "operationId": "NotificationFeedController_findById", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of user making client request", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + }, + { + "name": "templateOnly", + "required": false, + "in": "query", + "description": "Pass \"true\" to opt out of server-side rendering. Default: false (server renders and returns content).", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "" } }, + "summary": "Get a feed item by ID", + "tags": ["Notification Feed"] + }, + "delete": { + "operationId": "NotificationFeedController_delete", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of user making client request", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "204": { "description": "" } }, + "summary": "Soft-delete a feed item (admin only)", + "tags": ["Notification Feed"] + } + }, + "/notification-feed/{id}/read": { + "post": { + "operationId": "NotificationFeedController_markAsRead", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of user making client request", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "" } }, + "summary": "Mark a feed item as read (idempotent)", + "tags": ["Notification Feed"] + } + }, + "/notification-feed/{id}/delivered": { + "post": { + "operationId": "NotificationFeedController_markAsDelivered", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of user making client request", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "" } }, + "summary": "Mark a feed item as delivered (idempotent)", + "tags": ["Notification Feed"] + } + }, + "/notification-feed/{id}/engagement": { + "post": { + "operationId": "NotificationFeedController_reportEngagement", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of user making client request", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/EngagementEventDto" } + } + } + }, + "responses": { "200": { "description": "" } }, + "summary": "Report an interacted engagement event with optional topic discriminator", + "tags": ["Notification Feed"] + } + }, + "/settings": { + "get": { + "operationId": "SettingsController_get", + "parameters": [ + { + "name": "x-internal-api-key", + "in": "header", + "description": "Internal API key for platform-level settings access", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "appId", + "required": true, + "in": "query", + "description": "Tenant application ID", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Settings retrieved" }, + "404": { "description": "Not found or unauthorized" } + }, + "summary": "Get tenant settings (internal only)", + "tags": ["Settings"] + }, + "put": { + "operationId": "SettingsController_update", + "parameters": [ + { + "name": "x-internal-api-key", + "in": "header", + "description": "Internal API key for platform-level settings access", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "appId", + "required": true, + "in": "query", + "description": "Tenant application ID", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateSettingsDto" } + } + } + }, + "responses": { + "200": { "description": "Settings updated" }, + "400": { "description": "Invalid input" } + }, + "summary": "Update tenant settings (internal only)", + "tags": ["Settings"] + } + }, + "/templates/categories": { + "post": { + "operationId": "CategoriesController_create", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateCategoryDto" } + } + } + }, + "responses": { + "201": { "description": "Category created" }, + "400": { "description": "Invalid input" }, + "409": { "description": "Duplicate category name" } + }, + "summary": "Create a new template category (admin only)", + "tags": ["Template Categories"] + }, + "get": { + "operationId": "CategoriesController_findAll", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "updatedAt", + "required": false, + "in": "query", + "description": "Cursor: updatedAt unix timestamp of last item", + "schema": { "type": "number" } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Cursor: id of last item", + "schema": { "type": "string" } + }, + { + "name": "affix", + "required": false, + "in": "query", + "description": "Cursor direction", + "schema": { "type": "string", "enum": ["append", "prepend"] } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Items per page", + "schema": { "minimum": 1, "maximum": 100, "type": "number" } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "Sort field", + "schema": { + "type": "string", + "enum": ["name", "createdAt", "updatedAt"] + } + }, + { + "name": "order", + "required": false, + "in": "query", + "description": "Sort direction", + "schema": { "type": "string", "enum": ["asc", "desc"] } + } + ], + "responses": { "200": { "description": "Categories list" } }, + "summary": "List all template categories", + "tags": ["Template Categories"] + } + }, + "/templates/categories/{id}": { + "get": { + "operationId": "CategoriesController_findById", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Category found" }, + "404": { "description": "Category not found" } + }, + "summary": "Get a template category by ID", + "tags": ["Template Categories"] + }, + "put": { + "operationId": "CategoriesController_update", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateCategoryDto" } + } + } + }, + "responses": { + "200": { "description": "Category updated" }, + "404": { "description": "Category not found" }, + "409": { "description": "Duplicate category name" } + }, + "summary": "Update a template category (admin only)", + "tags": ["Template Categories"] + }, + "delete": { + "operationId": "CategoriesController_delete", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Category deleted" }, + "404": { "description": "Category not found" } + }, + "summary": "Delete a template category (admin only)", + "tags": ["Template Categories"] + } + }, + "/templates": { + "post": { + "operationId": "TemplatesController_create", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateTemplateDto" } + } + } + }, + "responses": { + "201": { "description": "Template created" }, + "400": { "description": "Invalid input or variable schema" }, + "409": { "description": "Duplicate channel type" } + }, + "summary": "Create a new template (admin only)", + "tags": ["Templates"] + }, + "get": { + "operationId": "TemplatesController_findAll", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "updatedAt", + "required": false, + "in": "query", + "description": "Cursor: updatedAt unix timestamp of last item", + "schema": { "type": "number" } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Cursor: id of last item", + "schema": { "type": "string" } + }, + { + "name": "affix", + "required": false, + "in": "query", + "description": "Cursor direction", + "schema": { "type": "string", "enum": ["append", "prepend"] } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Items per page", + "schema": { "minimum": 1, "maximum": 100, "type": "number" } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "Sort field", + "schema": { + "type": "string", + "enum": ["updatedAt", "createdAt", "name"] + } + }, + { + "name": "order", + "required": false, + "in": "query", + "description": "Sort direction", + "schema": { "type": "string", "enum": ["asc", "desc"] } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "Filter by template status", + "schema": { "type": "string" } + }, + { + "name": "search", + "required": false, + "in": "query", + "description": "Search by name or templateId", + "schema": { "type": "string" } + }, + { + "name": "tags", + "required": false, + "in": "query", + "description": "Comma-separated tags to filter by", + "schema": { "type": "string" } + }, + { + "name": "tagMatch", + "required": false, + "in": "query", + "description": "Tag matching strategy: 'any' (OR) or 'all' (AND)", + "schema": { "type": "string", "enum": ["any", "all"] } + }, + { + "name": "templateCategory", + "required": false, + "in": "query", + "description": "Filter by templateCategory name", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "Templates list" } }, + "summary": "List all templates", + "tags": ["Templates"] + } + }, + "/templates/{id}": { + "get": { + "operationId": "TemplatesController_findById", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Template found" }, + "404": { "description": "Template not found" } + }, + "summary": "Get a template by ID", + "tags": ["Templates"] + }, + "put": { + "operationId": "TemplatesController_update", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateTemplateDto" } + } + } + }, + "responses": { + "200": { "description": "Template updated" }, + "400": { "description": "Invalid variable schema" }, + "404": { "description": "Template not found" } + }, + "summary": "Update template metadata (admin only)", + "tags": ["Templates"] + }, + "delete": { + "operationId": "TemplatesController_archive", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Template archived" }, + "404": { "description": "Template not found" } + }, + "summary": "Archive a template (admin only)", + "tags": ["Templates"] + } + }, + "/templates/{id}/channels/{channelType}": { + "put": { + "operationId": "TemplatesController_updateChannelContent", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + }, + { + "name": "channelType", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateChannelContentDto" + } + } + } + }, + "responses": { + "200": { "description": "Channel content updated" }, + "404": { "description": "Template not found" } + }, + "summary": "Update channel content for a template (admin only)", + "tags": ["Templates"] + } + }, + "/templates/{id}/versions": { + "post": { + "operationId": "TemplatesController_createVersion", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "201": { "description": "Version created" }, + "404": { "description": "Template not found" } + }, + "summary": "Create a new template version (admin only)", + "tags": ["Templates"] + } + }, + "/channels": { + "post": { + "operationId": "ChannelsController_create", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateChannelDto" } + } + } + }, + "responses": { + "201": { "description": "Channel created" }, + "400": { "description": "Called with onbehalfof" }, + "403": { "description": "Channel type restricted" }, + "409": { "description": "Duplicate channelId or limit reached" } + }, + "summary": "Create a new channel (admin only)", + "tags": ["Channels"] + }, + "get": { + "operationId": "ChannelsController_list", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "updatedAt", + "required": false, + "in": "query", + "description": "Cursor: updatedAt unix timestamp of last item", + "schema": { "type": "number" } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Cursor: id of last item", + "schema": { "type": "string" } + }, + { + "name": "affix", + "required": false, + "in": "query", + "description": "Cursor direction", + "schema": { "type": "string", "enum": ["append", "prepend"] } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Items per page", + "schema": { "minimum": 1, "maximum": 100, "type": "number" } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "Sort field", + "schema": { + "type": "string", + "enum": ["channelType", "createdAt", "updatedAt"] + } + }, + { + "name": "order", + "required": false, + "in": "query", + "description": "Sort direction", + "schema": { "type": "string", "enum": ["asc", "desc"] } + }, + { + "name": "search", + "required": false, + "in": "query", + "description": "Search by name or channelId", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "Paginated channel list" } }, + "summary": "List channels for the app", + "tags": ["Channels"] + } + }, + "/channels/availability": { + "get": { + "operationId": "ChannelsController_getAvailability", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Channel type availability list" }, + "400": { "description": "Called with onbehalfof" } + }, + "summary": "Get channel type availability (admin only)", + "tags": ["Channels"] + } + }, + "/channels/{id}": { + "get": { + "operationId": "ChannelsController_getById", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Channel found" }, + "404": { "description": "Channel not found" } + }, + "summary": "Get a channel by ID", + "tags": ["Channels"] + }, + "put": { + "operationId": "ChannelsController_update", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateChannelDto" } + } + } + }, + "responses": { + "200": { "description": "Channel updated" }, + "400": { "description": "Called with onbehalfof" }, + "404": { "description": "Channel not found" } + }, + "summary": "Update a channel (admin only)", + "tags": ["Channels"] + } + }, + "/push-notifications": { + "get": { + "operationId": "PushNotificationsController_findByReceiver", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of the user", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "limit", + "required": true, + "in": "query", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "Push notifications list" } }, + "summary": "List push notifications for a user", + "tags": ["Push Notifications"] + } + }, + "/push-notifications/{id}": { + "get": { + "operationId": "PushNotificationsController_findById", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of the user", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "Push notification details" } }, + "summary": "Get a push notification by ID", + "tags": ["Push Notifications"] + } + }, + "/push-notifications/{id}/delivered": { + "put": { + "operationId": "PushNotificationsController_markDelivered", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of the user", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "Marked as delivered" } }, + "summary": "Mark push notification as delivered", + "tags": ["Push Notifications"] + } + }, + "/push-notifications/{id}/clicked": { + "put": { + "operationId": "PushNotificationsController_markClicked", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of the user", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "Marked as clicked" } }, + "summary": "Mark push notification as clicked", + "tags": ["Push Notifications"] + } + }, + "/push-notifications/{id}/engagement": { + "post": { + "operationId": "PushNotificationsController_reportEngagement", + "parameters": [ + { + "name": "onbehalfof", + "in": "header", + "description": "UID of the user", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PushEngagementEventDto" + } + } + } + }, + "responses": { "200": { "description": "Engagement recorded" } }, + "summary": "Report push notification engagement event (interacted with optional topic)", + "tags": ["Push Notifications"] + } + }, + "/campaigns": { + "post": { + "operationId": "CampaignsController_create", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateCampaignDto" } + } + } + }, + "responses": { "201": { "description": "Campaign created" } }, + "summary": "Create a new campaign", + "tags": ["Campaigns"] + }, + "get": { + "operationId": "CampaignsController_findAll", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "updatedAt", + "required": false, + "in": "query", + "description": "Cursor: updatedAt unix timestamp of last item", + "schema": { "type": "number" } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Cursor: id of last item", + "schema": { "type": "string" } + }, + { + "name": "affix", + "required": false, + "in": "query", + "description": "Cursor direction", + "schema": { "type": "string", "enum": ["append", "prepend"] } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Items per page", + "schema": { "minimum": 1, "maximum": 100, "type": "number" } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "Sort field", + "schema": { + "type": "string", + "enum": ["updatedAt", "createdAt", "name"] + } + }, + { + "name": "order", + "required": false, + "in": "query", + "description": "Sort direction", + "schema": { "type": "string", "enum": ["asc", "desc"] } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "Filter by campaign status", + "schema": { "type": "string" } + }, + { + "name": "search", + "required": false, + "in": "query", + "description": "Search by name", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "Campaign list" } }, + "summary": "List campaigns (enriched with template + recipient summary)", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}": { + "get": { + "operationId": "CampaignsController_findById", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Campaign found" }, + "404": { "description": "Campaign not found" } + }, + "summary": "Get a campaign by ID (enriched)", + "tags": ["Campaigns"] + }, + "put": { + "operationId": "CampaignsController_update", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateCampaignDto" } + } + } + }, + "responses": { + "200": { "description": "Campaign updated" }, + "400": { "description": "Campaign not in draft status" }, + "404": { "description": "Campaign not found" } + }, + "summary": "Update a campaign (draft only)", + "tags": ["Campaigns"] + }, + "delete": { + "operationId": "CampaignsController_delete", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Campaign deleted" }, + "400": { "description": "Campaign not in draft status" }, + "404": { "description": "Campaign not found" } + }, + "summary": "Delete a campaign (draft only)", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}/recipients": { + "post": { + "operationId": "CampaignsController_addRecipients", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AddRecipientsDto" } + } + } + }, + "responses": { + "201": { "description": "Recipients added" }, + "400": { "description": "Campaign not in draft status" } + }, + "summary": "Add recipients manually (user IDs)", + "tags": ["Campaigns"] + }, + "get": { + "operationId": "CampaignsController_getRecipients", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + }, + { + "name": "updatedAt", + "required": false, + "in": "query", + "description": "Cursor: updatedAt unix timestamp of last item", + "schema": { "type": "number" } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Cursor: id of last item", + "schema": { "type": "string" } + }, + { + "name": "affix", + "required": false, + "in": "query", + "description": "Cursor direction", + "schema": { "type": "string", "enum": ["append", "prepend"] } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Items per page", + "schema": { "minimum": 1, "maximum": 100, "type": "number" } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "Sort field", + "schema": { "type": "string", "enum": ["updatedAt", "createdAt"] } + }, + { + "name": "order", + "required": false, + "in": "query", + "description": "Sort direction", + "schema": { "type": "string", "enum": ["asc", "desc"] } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "Filter by status", + "schema": { + "type": "string", + "enum": ["pending", "processing", "completed", "failed"] + } + } + ], + "responses": { "200": { "description": "Recipient list" } }, + "summary": "List campaign recipients", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}/recipients/summary": { + "get": { + "operationId": "CampaignsController_getRecipientSummary", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "Recipient summary" } }, + "summary": "Get recipient status summary", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}/recipients/upload-url": { + "post": { + "operationId": "CampaignsController_getUploadUrl", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "201": { "description": "Presigned URL generated" }, + "400": { "description": "Campaign not in draft status" } + }, + "summary": "Get S3 presigned URL for CSV upload", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}/import-csv": { + "post": { + "operationId": "CampaignsController_importCsv", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CsvUploadDto" } + } + } + }, + "responses": { + "202": { "description": "Import job created" }, + "400": { "description": "Campaign not in draft status" }, + "409": { "description": "Import already in progress" } + }, + "summary": "Start async CSV import for campaign recipients", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}/import-status": { + "get": { + "operationId": "CampaignsController_getImportStatus", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Import status returned" }, + "404": { "description": "No active import found" } + }, + "summary": "Get current import job status for a campaign", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}/send": { + "post": { + "operationId": "CampaignsController_send", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Campaign send enqueued" }, + "400": { + "description": "Campaign not in sendable status or no recipients" + } + }, + "summary": "Trigger campaign send", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}/schedule": { + "post": { + "operationId": "CampaignsController_schedule", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ScheduleCampaignDto" } + } + } + }, + "responses": { + "200": { "description": "Campaign scheduled" }, + "400": { + "description": "Campaign not in draft status or invalid time" + } + }, + "summary": "Schedule campaign send", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}/sequence-metrics": { + "get": { + "operationId": "CampaignsController_getSequenceMetrics", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { "200": { "description": "Sequence metrics returned" } }, + "summary": "Get per-step sequence delivery metrics for a campaign", + "tags": ["Campaigns"] + } + }, + "/campaigns/{id}/cancel": { + "post": { + "operationId": "CampaignsController_cancel", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "Campaign cancelled" }, + "400": { "description": "Campaign not in cancellable status" } + }, + "summary": "Cancel a campaign", + "tags": ["Campaigns"] + } + }, + "/notifications/messages": { + "post": { + "description": "Sendbird-equivalent POST /notifications/messages. 1–10 receivers = realtime (synchronous, returns notificationId). 11–10,000 receivers = inline batch (asynchronous, returns batchId). Larger sends should use the Campaigns CSV flow.", + "operationId": "NotificationSendController_send", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SendNotificationDto" } + } + } + }, + "responses": { + "200": { + "description": "Realtime: { notificationId, channels[], mode: \"realtime\" }. Batch: { batchId, total, channels[], mode: \"batch\" }." + }, + "400": { + "description": "Template not found, template not approved, or recipient count exceeds limits" + }, + "403": { + "description": "Admin-only — onbehalfof header must not be present" + } + }, + "summary": "Send a notification using a template", + "tags": ["Notifications"] + } + }, + "/analytics/events": { + "post": { + "operationId": "AnalyticsController_recordEvent", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RecordEventDto" } + } + } + }, + "responses": { + "201": { "description": "Event recorded" }, + "400": { "description": "Invalid input" } + }, + "summary": "Record an analytics event", + "tags": ["Analytics"] + } + }, + "/analytics/overview": { + "get": { + "operationId": "AnalyticsController_getOverview", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "dimension", + "required": false, + "in": "query", + "description": "Rollup dimension to query", + "schema": { + "type": "string", + "enum": ["campaign", "template", "channel"] + } + }, + { + "name": "dimensionId", + "required": false, + "in": "query", + "description": "Dimension ID (campaign ID, template ID, or channel type)", + "schema": { "type": "string" } + }, + { + "name": "period", + "required": false, + "in": "query", + "description": "Rollup period", + "schema": { "type": "string", "enum": ["hourly", "daily"] } + }, + { + "name": "startDate", + "required": false, + "in": "query", + "description": "Start date filter (ISO 8601)", + "schema": { "type": "string" } + }, + { + "name": "endDate", + "required": false, + "in": "query", + "description": "End date filter (ISO 8601)", + "schema": { "type": "string" } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Maximum number of results", + "schema": { "default": 50, "type": "number" } + } + ], + "responses": { "200": { "description": "Analytics overview" } }, + "summary": "Get aggregated analytics overview", + "tags": ["Analytics"] } - } - ], - "security": [ - { - "apiKey": [] - } - ], - "tags": [ - { - "name": "Notification Feed", - "description": "Admin operations on feed items produced by sends and campaigns." }, - { - "name": "Channels", - "description": "Read channel instances and per-type availability." - }, - { - "name": "Templates", - "description": "Read templates and their pinned versions. Templates are the atomic content primitive, and every notification flows through one." - } - ], - "paths": { - "/notification-feed/{feedItemId}": { - "parameters": [ - { "$ref": "#/components/parameters/feedItemId" } - ], - "delete": { - "tags": ["Notification Feed"], - "summary": "Delete feed item", - "description": "Soft-delete a feed item. The row remains until retention purge hard-deletes it.", - "operationId": "delete-feed-item", - "responses": { - "200": { - "description": "Deleted.", - "content": { - "application/json": { - "example": { "data": { "id": "feed-cl9xyz123", "deletedAt": "2026-05-04T10:35:00.000Z" } } - } + "/analytics/campaigns/{campaignId}": { + "get": { + "operationId": "AnalyticsController_getCampaignAnalytics", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "campaignId", + "required": true, + "in": "path", + "schema": { "type": "string" } + }, + { + "name": "dimension", + "required": false, + "in": "query", + "description": "Rollup dimension to query", + "schema": { + "type": "string", + "enum": ["campaign", "template", "channel"] } }, - "401": { "$ref": "#/components/responses/Unauthorized" }, - "404": { "$ref": "#/components/responses/NotFound" } - } + { + "name": "dimensionId", + "required": false, + "in": "query", + "description": "Dimension ID (campaign ID, template ID, or channel type)", + "schema": { "type": "string" } + }, + { + "name": "period", + "required": false, + "in": "query", + "description": "Rollup period", + "schema": { "type": "string", "enum": ["hourly", "daily"] } + }, + { + "name": "startDate", + "required": false, + "in": "query", + "description": "Start date filter (ISO 8601)", + "schema": { "type": "string" } + }, + { + "name": "endDate", + "required": false, + "in": "query", + "description": "End date filter (ISO 8601)", + "schema": { "type": "string" } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Maximum number of results", + "schema": { "default": 50, "type": "number" } + } + ], + "responses": { "200": { "description": "Campaign analytics" } }, + "summary": "Get campaign analytics drill-down", + "tags": ["Analytics"] } }, - "/channels": { + "/analytics/templates/{templateId}": { "get": { - "tags": ["Channels"], - "summary": "List channels", - "description": "List channel instances configured for this app. Paginated.", - "operationId": "list-channels", + "operationId": "AnalyticsController_getTemplateAnalytics", "parameters": [ { - "name": "type", + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "templateId", + "required": true, + "in": "path", + "schema": { "type": "string" } + }, + { + "name": "dimension", + "required": false, + "in": "query", + "description": "Rollup dimension to query", + "schema": { + "type": "string", + "enum": ["campaign", "template", "channel"] + } + }, + { + "name": "dimensionId", + "required": false, + "in": "query", + "description": "Dimension ID (campaign ID, template ID, or channel type)", + "schema": { "type": "string" } + }, + { + "name": "period", + "required": false, + "in": "query", + "description": "Rollup period", + "schema": { "type": "string", "enum": ["hourly", "daily"] } + }, + { + "name": "startDate", + "required": false, "in": "query", - "description": "Filter by channel type.", - "schema": { "type": "string", "enum": ["in_app", "push"] } + "description": "Start date filter (ISO 8601)", + "schema": { "type": "string" } }, { - "name": "enabled", + "name": "endDate", + "required": false, "in": "query", - "description": "Filter by enabled state.", - "schema": { "type": "boolean" } + "description": "End date filter (ISO 8601)", + "schema": { "type": "string" } }, { "name": "limit", + "required": false, "in": "query", - "schema": { "type": "integer", "default": 20 } + "description": "Maximum number of results", + "schema": { "default": 50, "type": "number" } } ], - "responses": { - "200": { - "description": "Paginated channel list.", - "content": { - "application/json": { - "example": { - "data": [ - { - "id": "ch-cl9abc111", - "appId": "app_123", - "name": "Default Feed", - "channelId": "default-feed", - "type": "in_app", - "enabled": true, - "metadata": {}, - "templateCount": 4, - "createdAt": "2026-04-01T00:00:00.000Z", - "updatedAt": "2026-04-01T00:00:00.000Z" - } - ], - "meta": { "current": { "limit": 20, "count": 1 } } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + "responses": { "200": { "description": "Template analytics" } }, + "summary": "Get template analytics drill-down", + "tags": ["Analytics"] } }, - "/channels/availability": { + "/analytics/channels": { "get": { - "tags": ["Channels"], - "summary": "Check availability", - "description": "Return per-type counts and configured limits. Used by the Dashboard to gate channel creation.", - "operationId": "check-channel-availability", - "responses": { - "200": { - "description": "Availability by channel type.", - "content": { - "application/json": { - "example": { - "data": { - "in_app": { "count": 2, "limit": 5, "available": 3 }, - "push": { "count": 1, "limit": 5, "available": 4 } - } - } - } + "operationId": "AnalyticsController_getChannelBreakdown", + "parameters": [ + { + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "dimension", + "required": false, + "in": "query", + "description": "Rollup dimension to query", + "schema": { + "type": "string", + "enum": ["campaign", "template", "channel"] } }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + { + "name": "dimensionId", + "required": false, + "in": "query", + "description": "Dimension ID (campaign ID, template ID, or channel type)", + "schema": { "type": "string" } + }, + { + "name": "period", + "required": false, + "in": "query", + "description": "Rollup period", + "schema": { "type": "string", "enum": ["hourly", "daily"] } + }, + { + "name": "startDate", + "required": false, + "in": "query", + "description": "Start date filter (ISO 8601)", + "schema": { "type": "string" } + }, + { + "name": "endDate", + "required": false, + "in": "query", + "description": "End date filter (ISO 8601)", + "schema": { "type": "string" } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Maximum number of results", + "schema": { "default": 50, "type": "number" } + } + ], + "responses": { "200": { "description": "Channel breakdown" } }, + "summary": "Get channel breakdown analytics", + "tags": ["Analytics"] } }, - "/templates": { + "/analytics/users/{userId}": { "get": { - "tags": ["Templates"], - "summary": "List templates", - "description": "List templates for this app. Paginated.", - "operationId": "list-templates", + "operationId": "AnalyticsController_getUserInsights", "parameters": [ { - "name": "category", - "in": "query", - "description": "Filter by category name.", + "name": "appid", + "in": "header", + "description": "Tenant application ID", + "required": true, "schema": { "type": "string" } }, { - "name": "status", + "name": "userId", + "required": true, + "in": "path", + "schema": { "type": "string" } + }, + { + "name": "dimension", + "required": false, "in": "query", - "schema": { "type": "string", "enum": ["draft", "approved", "archived"] } + "description": "Rollup dimension to query", + "schema": { + "type": "string", + "enum": ["campaign", "template", "channel"] + } }, { - "name": "search", + "name": "dimensionId", + "required": false, "in": "query", - "description": "Substring match on name or templateId.", + "description": "Dimension ID (campaign ID, template ID, or channel type)", "schema": { "type": "string" } }, { - "name": "tags", + "name": "period", + "required": false, + "in": "query", + "description": "Rollup period", + "schema": { "type": "string", "enum": ["hourly", "daily"] } + }, + { + "name": "startDate", + "required": false, "in": "query", - "description": "Comma-separated tag list. Pair with `tagMatch`.", + "description": "Start date filter (ISO 8601)", "schema": { "type": "string" } }, { - "name": "tagMatch", + "name": "endDate", + "required": false, "in": "query", - "schema": { "type": "string", "enum": ["any", "all"], "default": "any" } + "description": "End date filter (ISO 8601)", + "schema": { "type": "string" } }, { "name": "limit", + "required": false, "in": "query", - "schema": { "type": "integer", "default": 20 } + "description": "Maximum number of results", + "schema": { "default": 50, "type": "number" } } ], "responses": { "200": { - "description": "Paginated template list.", - "content": { - "application/json": { - "example": { - "data": [ - { - "id": "tmpl-cl9def789", - "templateId": "order_update", - "appId": "app_123", - "name": "Order Update", - "category": "Updates", - "label": "Orders", - "tags": ["transactional", "orders"], - "status": "approved", - "currentVersion": 1, - "variableSchema": [ - { "key": "user_name", "name": "User Name", "type": "string", "required": true }, - { "key": "order_id", "name": "Order ID", "type": "string", "required": true } - ], - "config": { "sequenceEnabled": false }, - "createdAt": "2026-04-15T12:00:00.000Z", - "updatedAt": "2026-04-15T12:00:00.000Z" - } - ], - "meta": { "current": { "limit": 20, "count": 1 } } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + "description": "Aggregated counts: viewed, clicked, interacted, lastEngagement (unix seconds or null)" + } + }, + "summary": "Get user-level engagement insights", + "tags": ["Analytics"] } }, - "/templates/{templateId}": { - "get": { - "tags": ["Templates"], - "summary": "Get template", - "description": "Fetch a single template by CUID or `templateId` slug. Includes all versions and the current version's channels and variable schema.", - "operationId": "get-template", + "/analytics/rollup": { + "post": { + "operationId": "AnalyticsController_triggerRollup", "parameters": [ { - "name": "templateId", - "in": "path", + "name": "appid", + "in": "header", + "description": "Tenant application ID", "required": true, - "description": "Template CUID or `templateId` slug.", "schema": { "type": "string" } } ], + "responses": { "200": { "description": "Rollup triggered" } }, + "summary": "Trigger rollup computation", + "tags": ["Analytics"] + } + }, + "/health": { + "get": { + "operationId": "HealthController_check", + "parameters": [], "responses": { - "200": { - "description": "Template.", - "content": { - "application/json": { - "example": { - "data": { - "id": "tmpl-cl9def789", - "templateId": "order_update", - "appId": "app_123", - "name": "Order Update", - "category": "Updates", - "label": "Orders", - "tags": ["transactional", "orders"], - "status": "approved", - "currentVersion": 1, - "versions": [ - { - "version": 1, - "channels": [ - { - "channelType": "in_app", - "channelId": "default-feed", - "dataType": "ui_template", - "categoryFilterEnabled": true, - "templateLabelEnabled": true, - "messageRetentionHours": 720, - "content": { - "title": "Order #{{order_id}} update", - "body": "Hi {{user_name}}, your order is on the way." - } - } - ], - "variableSchema": [ - { "key": "user_name", "name": "User Name", "type": "string", "required": true }, - { "key": "order_id", "name": "Order ID", "type": "string", "required": true } - ], - "createdAt": "2026-04-15T12:00:00.000Z" - } - ], - "variableSchema": [ - { "key": "user_name", "name": "User Name", "type": "string", "required": true }, - { "key": "order_id", "name": "Order ID", "type": "string", "required": true } - ], - "config": { "sequenceEnabled": false }, - "createdAt": "2026-04-15T12:00:00.000Z", - "updatedAt": "2026-04-15T12:00:00.000Z" - } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" }, - "404": { "$ref": "#/components/responses/NotFound" } - } + "200": { "description": "All dependencies healthy." }, + "503": { "description": "At least one dependency is down." } + }, + "tags": ["Health"] } } }, + "info": { + "title": "Campaigns Service API", + "description": "Campaigns Service REST API", + "version": "1.0", + "contact": {} + }, + "tags": [ + { "name": "Notification Feed", "description": "Operations on the per-user in-app notification feed." }, + { "name": "Channels", "description": "Manage channel instances and per-type availability." }, + { "name": "Templates", "description": "Manage templates and their versions." }, + { "name": "Template Categories", "description": "Manage template categories for feed filtering." }, + { "name": "Campaigns", "description": "Create, schedule, and manage notification campaigns." }, + { "name": "Notifications", "description": "Send notifications directly via API." }, + { "name": "Push Notifications", "description": "Manage push notification delivery and engagement." }, + { "name": "Analytics", "description": "Delivery and engagement analytics." }, + { "name": "Settings", "description": "App-level campaign settings (internal)." }, + { "name": "Health", "description": "Service health check." } + ], + "servers": [ + { + "url": "https://{appId}.api-{region}.cometchat.io/v3/campaigns", + "variables": { + "appId": { "default": "appId", "description": "(Required) App ID" }, + "region": { "enum": ["us", "eu", "in"], "default": "us", "description": "Select Region" } + } + } + ], "components": { - "parameters": { - "feedItemId": { - "name": "feedItemId", - "in": "path", - "required": true, - "description": "(Required) Feed item ID.", - "schema": { "type": "string" } + "securitySchemes": { + "appid": { "type": "apiKey", "in": "header", "name": "appid" }, + "basic-auth": { + "type": "http", + "scheme": "basic", + "description": "Service-to-service basic auth" } }, - "securitySchemes": { - "apiKey": { - "type": "apiKey", - "description": "API Key with fullAccess scope (REST API Key from the Dashboard).", - "name": "apikey", - "in": "header" - } - }, - "responses": { - "BadRequest": { - "description": "400 Bad Request. Validation or contract failure. See the error envelope on the Campaigns API overview.", - "content": { - "application/json": { - "example": { - "error": { - "code": "ERR_VALIDATION", - "message": "receivers must contain at least one userId", - "devMessage": "receivers must contain at least one userId", - "details": ["receivers must contain at least one userId"], - "source": "campaigns-service" - } - } + "schemas": { + "EngagementEventDto": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "description": "Freeform topic discriminator for the interacted event. Well-known value: \"clicked\". Custom values: any alphanumeric + underscore/hyphen string ≤64 chars.", + "example": "clicked", + "maxLength": 64 } } }, - "Unauthorized": { - "description": "401 Unauthorized. Missing or invalid app credentials.", - "content": { - "application/json": { + "UpdateSettingsDto": { + "type": "object", + "properties": { + "retentionDays": { + "type": "number", + "description": "Data retention period in days", + "example": 90, + "minimum": 1 + }, + "defaultChannels": { + "description": "Default channel types for new campaigns", + "example": ["push", "in_app"], + "type": "array", + "items": { "type": "string" } + }, + "realtimeFanout": { + "type": "array", + "description": "Realtime fanout policy for in_app FeedItem deliveries. Empty array = feed_only (default). Allowed values: 'websocket', 'push'. Consumers of after_feed_item_sent (WebSocket fanout svc, push wake-up svc) self-filter on this array.", + "example": ["websocket"], + "items": { "type": "string", "enum": ["websocket", "push"] } + }, + "config": { + "type": "object", + "description": "Additional service-level configuration. Supports: deliveryMechanisms: { websocketEnabled: boolean, pushEnabled: boolean } — controls default delivery mechanisms for announcements. Defaults: { websocketEnabled: true, pushEnabled: false }. Feed is always enabled.", "example": { - "error": { - "code": "ERR_UNAUTHORIZED", - "message": "apikey header is required", - "devMessage": "apikey header is required", - "source": "campaigns-service" + "timezone": "UTC", + "deliveryMechanisms": { + "websocketEnabled": true, + "pushEnabled": false } } } } }, - "NotFound": { - "description": "404 Not Found. Resource does not exist.", - "content": { - "application/json": { + "CreateCategoryDto": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Category name" }, + "description": { + "type": "string", + "description": "Category description" + } + }, + "required": ["name"] + }, + "UpdateCategoryDto": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Category name" }, + "description": { + "type": "string", + "description": "Category description" + } + } + }, + "ChannelContentDto": { + "type": "object", + "properties": { + "channelId": { + "type": "string", + "description": "Channel instance ID (links to Channel entity)" + }, + "channelType": { + "type": "string", + "description": "Channel type identifier" + }, + "content": { + "type": "object", + "description": "Channel content (defaults applied if omitted)" + }, + "dataType": { + "type": "string", + "description": "Data type", + "enum": ["ui_template", "data_template"] + }, + "categoryFilterEnabled": { + "type": "boolean", + "description": "Enable category filter (in-app only)" + }, + "templateLabelEnabled": { + "type": "boolean", + "description": "Enable template label (in-app only)" + }, + "messageRetentionHours": { + "type": "number", + "description": "Message retention in hours" + }, + "sequenceOrder": { + "type": "number", + "description": "Position in sequence (1, 2, 3...)" + }, + "stopCondition": { + "type": "string", + "description": "Stop condition for sequence step. Allowed values depend on channelType: feed channels (in_app) accept `delivered` | `read` | `engaged`; push channels accept `delivered` | `clicked`. Validated per-channel at template save (ENG-35239)." + }, + "waitMinutes": { + "type": "number", + "description": "Wait time in minutes before advancing to next step", + "enum": [5, 10, 30, 60, 240, 1440] + } + }, + "required": ["channelType"] + }, + "CreateTemplateDto": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Template name" }, + "templateId": { + "type": "string", + "description": "Human-readable slug (auto-generated from name if omitted)" + }, + "templateCategory": { + "type": "string", + "description": "Template category", + "enum": [ + "Onboarding", + "Transactional", + "Marketing", + "Product_Showcase", + "Alerts", + "Polls", + "Custom" + ] + }, + "label": { + "type": "string", + "description": "Display label shown on notification (e.g. Promo, Alert)" + }, + "alternativeText": { + "type": "string", + "description": "Plain-text fallback when push is suppressed or rich content cannot render. Sendbird-parity field." + }, + "tags": { + "description": "First-class tags for filtering/segmentation", + "type": "array", + "items": { "type": "string" } + }, + "status": { + "type": "string", + "description": "Template status", + "enum": ["draft", "approved", "archived"] + }, + "channels": { + "description": "Channel configurations", + "type": "array", + "items": { "$ref": "#/components/schemas/ChannelContentDto" } + }, + "variableSchema": { + "description": "Variable schema definitions", + "type": "array", + "items": { "type": "string" } + }, + "config": { + "type": "object", + "description": "Additional configuration" + } + }, + "required": ["name", "channels"] + }, + "UpdateTemplateDto": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "templateCategory": { "type": "string" }, + "label": { + "type": "string", + "description": "Display label shown on notification" + }, + "alternativeText": { + "type": "string", + "description": "Plain-text fallback when push is suppressed or rich content cannot render. Sendbird-parity field." + }, + "tags": { + "description": "First-class tags for filtering/segmentation", + "type": "array", + "items": { "type": "string" } + }, + "status": { "type": "string" }, + "variableSchema": { "type": "array", "items": { "type": "string" } }, + "config": { "type": "object" } + } + }, + "UpdateChannelContentDto": { + "type": "object", + "properties": { + "content": { + "type": "object", + "description": "Channel content payload" + } + }, + "required": ["content"] + }, + "CreateChannelDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Channel display name", + "example": "My Push Channel" + }, + "type": { + "type": "string", + "description": "Channel type", + "enum": ["in_app", "push", "sms", "email", "whatsapp", "custom"], + "example": "push" + }, + "channelId": { + "type": "string", + "description": "Channel slug (auto-generated from name if omitted)", + "example": "cc-notification-channel-my-push" + }, + "enabled": { + "type": "boolean", + "description": "Whether the channel is enabled", + "default": false + }, + "metadata": { + "type": "object", + "description": "Channel-specific metadata", + "example": { "apiKey": "xxx", "senderId": "yyy" } + } + }, + "required": ["name", "type"] + }, + "UpdateChannelDto": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Channel display name" }, + "enabled": { + "type": "boolean", + "description": "Whether the channel is enabled" + }, + "metadata": { + "type": "object", + "description": "Channel-specific metadata", + "example": { "apiKey": "xxx", "senderId": "yyy" } + } + } + }, + "PushEngagementEventDto": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "description": "Freeform topic discriminator for the interacted event. Custom values: any alphanumeric + underscore/hyphen string ≤64 chars.", + "example": "dismissed", + "maxLength": 64 + } + } + }, + "CreateCampaignDto": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Campaign name" }, + "templateId": { + "type": "string", + "description": "Template ID (CUID or templateId slug)" + }, + "templateVersion": { + "type": "number", + "description": "Template version number to pin", + "minimum": 1 + }, + "variables": { + "type": "object", + "description": "Campaign-level default variables — applied to every recipient as a fallback layer below per-user CSV values and above template variableSchema defaults. Example: `{ \"promoCode\": \"SUMMER25\", \"supportEmail\": \"help@acme.io\" }`.", + "example": { "promoCode": "SUMMER25" } + }, + "config": { + "type": "object", + "description": "Additional campaign configuration (free-form)" + } + }, + "required": ["name", "templateId", "templateVersion"] + }, + "UpdateCampaignDto": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Campaign name" }, + "config": { + "type": "object", + "description": "Additional campaign configuration" + } + } + }, + "AddRecipientsDto": { + "type": "object", + "properties": { + "userIds": { + "description": "Array of user IDs to add as recipients", + "minItems": 1, + "maxItems": 10000, + "type": "array", + "items": { "type": "string" } + }, + "userVariables": { + "type": "object", + "description": "Per-user variables, keyed by userId. Persisted on each `CampaignRecipient.variables` row at insert time. Renderer substitutes these into template content per recipient. Example: `{ \"user_42\": { \"name\": \"Ajay\" }, \"user_43\": { \"name\": \"Sam\" } }`.", + "example": { "user_42": { "name": "Ajay" } } + } + }, + "required": ["userIds"] + }, + "CsvUploadDto": { + "type": "object", + "properties": { + "s3Key": { + "type": "string", + "description": "S3 object key of the uploaded CSV file" + } + }, + "required": ["s3Key"] + }, + "ScheduleCampaignDto": { + "type": "object", + "properties": { + "scheduledAt": { + "type": "number", + "description": "Scheduled send time as Unix timestamp (seconds)", + "example": 1714000000 + } + }, + "required": ["scheduledAt"] + }, + "SendNotificationDto": { + "type": "object", + "properties": { + "templateId": { + "type": "string", + "description": "Template CUID or templateId slug. Template must be in approved status.", + "example": "order_update" + }, + "receivers": { + "description": "Array of target user IDs. 1–10 = realtime (synchronous, returns notificationId immediately). 11–10,000 = batch (asynchronous, returns batchId; processing happens via queue).", + "minItems": 1, + "maxItems": 10000, + "type": "array", + "items": { "type": "string" } + }, + "variables": { + "type": "object", + "description": "Per-user variables. Keyed by userId; values are { variableName: value } objects. Variables are applied to the template content at delivery time.", "example": { - "error": { - "code": "ERR_FEED_ITEM_NOT_FOUND", - "message": "Feed item not found", - "devMessage": "Feed item not found: feed-cl9xyz123", - "details": { "feedItemId": "feed-cl9xyz123" }, - "source": "campaigns-service" - } + "user_42": { "user_name": "John", "order_id": "12345" }, + "user_43": { "user_name": "Sarah", "order_id": "12346" } } + }, + "tag": { + "type": "string", + "description": "Optional analytics tag attached to the send (passes through to delivery records)." } - } + }, + "required": ["templateId", "receivers"] + }, + "RecordEventDto": { + "type": "object", + "properties": { + "notificationId": { + "type": "string", + "description": "Notification (batch) ID this engagement relates to" + }, + "feedItemId": { + "type": "string", + "description": "FeedItem ID (for in-app engagement)" + }, + "pushNotificationId": { + "type": "string", + "description": "PushNotification ID (for push engagement)" + }, + "campaignId": { + "type": "string", + "description": "Campaign ID (optional)" + }, + "templateId": { + "type": "string", + "description": "Template ID (optional)" + }, + "userId": { + "type": "string", + "description": "User ID who triggered the event" + }, + "channelType": { + "type": "string", + "description": "Channel type (in_app, push, ...)" + }, + "eventType": { + "type": "string", + "description": "Engagement event type", + "enum": ["sent", "delivered", "clicked", "interacted", "failed"] + }, + "topic": { + "type": "string", + "description": "Topic discriminator for interacted events (≤64 chars)" + } + }, + "required": ["notificationId", "userId", "channelType", "eventType"] } } - } + }, + "security": [{ "basic-auth": [] }] } diff --git a/campaigns.mdx b/campaigns.mdx index d8703e922..92ee28307 100644 --- a/campaigns.mdx +++ b/campaigns.mdx @@ -57,74 +57,13 @@ The typical setup to send your first notification: | Max campaigns | No hard limit | | Max templates | No hard limit | -## Send Notifications via API - -You can send notifications directly from your backend without creating a campaign in the dashboard. This is ideal for transactional or event-driven notifications (e.g., "Your order shipped", "New message received"). - -**Endpoint:** `POST /notifications/messages` - -**Headers:** -- `appid` — your CometChat app ID -- `apikey` — your CometChat API key - -**Request Body:** - -```json -{ - "templateId": "cc-template-order-shipped", - "receivers": ["user-123", "user-456"], - "variables": { - "user-123": { "orderNumber": "ORD-789", "name": "John" }, - "user-456": { "orderNumber": "ORD-790", "name": "Jane" } - } -} -``` - -**Fields:** - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `templateId` | string | Yes | The template slug (e.g., `cc-template-welcome`) | -| `receivers` | string[] | Yes | Array of user UIDs (1 to 10,000) | -| `variables` | object | No | Per-user variable values keyed by UID | -| `tag` | string | No | Optional analytics tag | - -**Delivery Modes:** -- ≤ 10 receivers → **Realtime** (synchronous, immediate delivery) -- \> 10 receivers → **Batch** (queued, processed asynchronously) - -**Requirements:** -- Template must be in `approved` status -- Called from your server (admin-only — `onbehalfof` header is rejected) - -**Response (realtime):** - -```json -{ - "notificationId": "abc123", - "channels": ["in_app"], - "mode": "realtime" -} -``` - -**Response (batch):** - -```json -{ - "batchId": "xyz789", - "total": 500, - "channels": ["in_app", "push"], - "mode": "batch" -} -``` - ## Common Use Cases ### Transactional alerts Order shipped, payment receipt, password reset, security alert. One templated message per event, dispatched in real time, with per-recipient variable substitution (`{{order_id}}`, `{{user_name}}`). ### Marketing campaigns -Product launches, promotional offers, re-engagement nudges. Schedule the send, upload a recipient list, and let the campaign worker fan out across in-app and push. +Product launches, promotional offers, re-engagement nudges. Schedule the send, upload a recipient list, and deliver across in-app and push channels. ### Operational messages Maintenance windows, policy updates, account changes. Dispatched to a broad audience and routed through the in-app feed so users can revisit them. diff --git a/campaigns/campaigns.mdx b/campaigns/campaigns.mdx index db8e0b77a..659b8e0d2 100644 --- a/campaigns/campaigns.mdx +++ b/campaigns/campaigns.mdx @@ -11,7 +11,7 @@ A campaign is a batch notification send to a targeted list of users. Campaigns p | | Campaign | Direct Notification | |-|----------|-------------------| | Entry point | Dashboard wizard | API: `POST /notifications/messages` | -| Recipients | Manual list, CSV upload | `receivers` array in request body | +| Recipients | CSV upload, User picker | `receivers` array in request body | | Scheduling | Immediate or scheduled | Always immediate | | Tracking | Full lifecycle (status, sent/failed counts) | Notification row created, no campaign-level tracking | | Sequences | Supported (multi-step) | Not supported | @@ -23,7 +23,6 @@ The dashboard wizard walks you through four steps: Choose your input mode: - - **Manual** — Enter user IDs directly - **CSV** — Upload a file with a `user_id` column and optional variable columns - **User picker** — Select from the Users list diff --git a/campaigns/channels.mdx b/campaigns/channels.mdx index 6fbcb2c14..5ff695c64 100644 --- a/campaigns/channels.mdx +++ b/campaigns/channels.mdx @@ -38,15 +38,15 @@ Push notification configuration (FCM/APNs keys) is managed at the CometChat app ## Multiple Channels -You can create multiple channels of the same type. For example: +You can create multiple `in_app` channels. For example: - "Promotions Feed" (`in_app`) — for marketing notifications - "Alerts Feed" (`in_app`) — for transactional alerts -Templates reference a specific channel instance by its `channelId`. +Push channels are limited to one per app. Templates reference a specific channel instance by its `channelId`. ## Limits | Limit | Value | |-------|-------| | `in_app` channels per app | No hard limit | -| `push` channels per app | No hard limit | +| `push` channels per app | 1 | diff --git a/campaigns/notification-settings.mdx b/campaigns/notification-settings.mdx index e270d8781..673aa0231 100644 --- a/campaigns/notification-settings.mdx +++ b/campaigns/notification-settings.mdx @@ -1,52 +1,70 @@ --- title: "Notification Settings" sidebarTitle: "Notification Settings" -description: "App-level configuration for campaign notification behavior including retention, delivery mechanisms, and defaults." +description: "Configure push notification providers (FCM, APNs, Custom) for delivering campaign notifications." --- -Notification Settings is an app-level configuration that controls global behavior for the campaigns service. These settings apply to the entire app — they are not per-user preferences. +The Notification Settings page lets you manage push notification provider credentials for your app. You must configure at least one provider to deliver push notifications through campaigns. -## Available Settings +## Push Notifications -| Setting | Type | Default | Description | -|---------|------|---------|-------------| -| `retentionDays` | number | 90 | Days before feed items are purged from the database | -| `defaultChannels` | string[] | `[]` | Default channel types pre-selected for new templates | -| `realtimeFanout` | string[] | `[]` | Realtime delivery mechanisms. Values: `["websocket"]` | -| `config` | object | `{}` | Arbitrary key-value configuration | +A global toggle at the top of the page enables or disables push notifications for the entire app. -## Realtime Fanout +## Providers -Setting `realtimeFanout: ["websocket"]` enables WebSocket delivery for in-app notifications. Without this, feed items are only available via polling the feed API. +### Firebase Cloud Messaging (FCM) -## Default Channels +| Field | Type | Description | +|-------|------|-------------| +| `providerId` | string | Unique name/identifier for this credential | +| `serviceAccountFilename` | string | Name of the uploaded JSON file | +| `serviceAccountCreds` | object | Service account JSON contents (`project_id`, `client_email`, `private_key`, `private_key_id`) | +| `notificationInPayload` | object | Per-platform toggle for chat/call notifications | -The `defaultChannels` setting pre-selects channel types when creating new templates in the dashboard. This is a convenience setting — it does not restrict which channels can be used. +The `notificationInPayload` field controls whether notifications are included in the payload per platform: -## Retention +```json +{ + "ios": { "chat": true, "call": true }, + "android": { "chat": true, "call": true }, + "web": { "chat": true, "call": false } +} +``` -The `retentionDays` setting controls how long feed items are stored in the database before the daily purge job hard-deletes them. +### Apple Push Notification service (APNs) - -This is separate from `messageRetentionHours` on individual templates, which controls when a feed item disappears from the user's feed (soft expiry). The global `retentionDays` controls the database-level cleanup. - +| Field | Type | Description | +|-------|------|-------------| +| `providerId` | string | Unique name/identifier for this credential | +| `productionMode` | boolean | `true` for production, `false` for development/sandbox | +| `teamId` | string | Apple Team ID | +| `bundleId` | string | App Bundle ID | +| `keyId` | string | APNs Key ID | +| `p8KeyFilename` | string | Uploaded `.p8` key filename | +| `p8Key` | string | The `.p8` key content | +| `includeContentAvailable` | boolean | Include `content-available` flag in push payload | +| `includeMutableContent` | boolean | Include `mutable-content` flag in push payload | -## What Is Not Configurable +### Custom Push Notification Provider -| Feature | Status | -|---------|--------| -| Per-user preferences (opt-in/opt-out) | Not supported | -| Retry policy | Not configurable | -| Delivery priority | Not configurable | -| Quiet hours | Not supported | -| Rate limiting | Not configurable | +| Field | Type | Description | +|-------|------|-------------| +| `webhookURL` | string | Your webhook endpoint URL | +| `isEnabled` | boolean | Enable/disable the custom provider | +| `useBasicAuth` | boolean | Enable Basic Authentication | +| `basicAuthUsername` | string | Username (required if `useBasicAuth` is true) | +| `basicAuthPassword` | string | Password (required if `useBasicAuth` is true) | -## Configuration +## Multiple Credentials -This setting is configured via an internal API and is not exposed in the dashboard UI: +- **FCM** — Multiple credentials supported. Set one as default (star icon). +- **APNs** — Multiple credentials supported. Set one as default (star icon). +- **Custom** — Limited to a single instance. -```bash -PUT /settings?appId= -Header: x-internal-api-key: -Body: { "realtimeFanout": ["websocket"], "retentionDays": 30 } -``` +## Validation + +Credentials are saved without a test connection. Validation happens at delivery time — if credentials are invalid, push notifications will fail with an error in the campaign delivery report. + + +There is no auto-verify or test connection feature. Ensure your credentials are correct before sending campaigns. + diff --git a/campaigns/users.mdx b/campaigns/users.mdx index 343d518da..43764829d 100644 --- a/campaigns/users.mdx +++ b/campaigns/users.mdx @@ -12,13 +12,9 @@ When creating a campaign, you can target users through the following methods: | Method | Description | |--------|-------------| -| Manual UID entry | Type user IDs directly when creating a campaign | | CSV upload | Upload a CSV file with a `user_id` column and optional per-user variables | | User picker | Select from the Users list in the dashboard | - -There is no segment-based targeting (e.g., "all users who signed up last week"). Targeting is explicit — you provide the list of UIDs. - ## Filtering Users @@ -29,11 +25,49 @@ The Users page supports filtering by: | Search | Filter by UID or name | | Role | Filter by CometChat user role | | Status | Filter by online / offline status | +| Created At | Filter by user creation date | ## User Preferences User-level notification preferences (opt-in/opt-out) are not currently supported. All users in the recipient list receive the notification. +## Sending Notifications to Users + +You don't always need to create a campaign to reach your users. You can send notifications directly — either from the dashboard for a single user, or via API for multiple users. + +### From the Dashboard + +On the User Detail page, there's a **Send Notification** button that lets you send a notification to that specific user. Pick a template, fill in variables, and send. + +### From Your Backend (API) + +For sending to multiple users without a campaign, use the API. The flow is: + +1. Have at least one channel and one approved template ready. +2. Call `POST /notifications/messages` from your server with the template and list of user UIDs. +3. Optionally include per-user variables for personalization (like name, order number, etc.). +4. Get a response confirming the send. + +### Delivery Modes + +| | ≤ 10 recipients | > 10 recipients | +|-|-----------------|-----------------| +| Mode | Realtime | Batch | +| Delivery | Immediate — notifications are created before the response returns | Queued — processed in the background | +| Response | `notificationId` + channels | `batchId` + total count | +| Confirmation | Response = delivery done | Response = job queued, track via webhooks | + +### Tracking Delivery + +- **Realtime (≤ 10):** A successful response means all notifications were delivered. +- **Batch (> 10):** The response confirms the job is queued. + + +There is no dashboard view for direct notification status. Only campaigns have a detail page with delivery tracking. + + +For the API reference, see [Send Notification](/rest-api/campaigns-apis/notifications/send-notification). + ## Limits | Limit | Value | diff --git a/docs.json b/docs.json index 96a4d055c..25a30d3d1 100644 --- a/docs.json +++ b/docs.json @@ -6509,13 +6509,59 @@ "pages": [ "rest-api/campaigns-apis/overview", "rest-api/campaigns-apis/setup-and-authentication", + { + "group": "Notifications", + "expanded": false, + "icon": "paper-plane", + "pages": [ + "rest-api/campaigns-apis/notifications/send-notification" + ] + }, + { + "group": "Campaigns", + "expanded": false, + "icon": "bullhorn", + "pages": [ + "rest-api/campaigns-apis/campaigns/create-campaign", + "rest-api/campaigns-apis/campaigns/list-campaigns", + "rest-api/campaigns-apis/campaigns/get-campaign", + "rest-api/campaigns-apis/campaigns/update-campaign", + "rest-api/campaigns-apis/campaigns/delete-campaign", + "rest-api/campaigns-apis/campaigns/add-recipients", + "rest-api/campaigns-apis/campaigns/list-recipients", + "rest-api/campaigns-apis/campaigns/recipient-summary", + "rest-api/campaigns-apis/campaigns/upload-url", + "rest-api/campaigns-apis/campaigns/import-csv", + "rest-api/campaigns-apis/campaigns/import-status", + "rest-api/campaigns-apis/campaigns/send-campaign", + "rest-api/campaigns-apis/campaigns/schedule-campaign", + "rest-api/campaigns-apis/campaigns/cancel-campaign" + ] + }, { "group": "Templates", "expanded": false, "icon": "file-lines", "pages": [ + "rest-api/campaigns-apis/templates/create-template", "rest-api/campaigns-apis/templates/list-templates", - "rest-api/campaigns-apis/templates/get-template" + "rest-api/campaigns-apis/templates/get-template", + "rest-api/campaigns-apis/templates/update-template", + "rest-api/campaigns-apis/templates/archive-template", + "rest-api/campaigns-apis/templates/update-channel-content", + "rest-api/campaigns-apis/templates/create-version" + ] + }, + { + "group": "Template Categories", + "expanded": false, + "icon": "folder", + "pages": [ + "rest-api/campaigns-apis/categories/create-category", + "rest-api/campaigns-apis/categories/list-categories", + "rest-api/campaigns-apis/categories/get-category", + "rest-api/campaigns-apis/categories/update-category", + "rest-api/campaigns-apis/categories/delete-category" ] }, { @@ -6523,7 +6569,10 @@ "expanded": false, "icon": "tower-broadcast", "pages": [ + "rest-api/campaigns-apis/channels/create-channel", "rest-api/campaigns-apis/channels/list-channels", + "rest-api/campaigns-apis/channels/get-channel", + "rest-api/campaigns-apis/channels/update-channel", "rest-api/campaigns-apis/channels/check-availability" ] }, @@ -6532,9 +6581,27 @@ "expanded": false, "icon": "bell", "pages": [ + "rest-api/campaigns-apis/notification-feed/list-feed", + "rest-api/campaigns-apis/notification-feed/get-feed-item", + "rest-api/campaigns-apis/notification-feed/get-unread-count", + "rest-api/campaigns-apis/notification-feed/mark-as-delivered", + "rest-api/campaigns-apis/notification-feed/mark-as-read", + "rest-api/campaigns-apis/notification-feed/report-engagement", "rest-api/campaigns-apis/notification-feed/delete-feed-item" ] }, + { + "group": "Push Notifications", + "expanded": false, + "icon": "mobile", + "pages": [ + "rest-api/campaigns-apis/push-notifications/list-push-notifications", + "rest-api/campaigns-apis/push-notifications/get-push-notification", + "rest-api/campaigns-apis/push-notifications/mark-delivered", + "rest-api/campaigns-apis/push-notifications/mark-clicked", + "rest-api/campaigns-apis/push-notifications/report-engagement" + ] + }, { "group": "Sequences", "expanded": false, diff --git a/rest-api/campaigns-apis/analytics/campaign-metrics.mdx b/rest-api/campaigns-apis/analytics/campaign-metrics.mdx index 5bd0d9df4..bc27d5982 100644 --- a/rest-api/campaigns-apis/analytics/campaign-metrics.mdx +++ b/rest-api/campaigns-apis/analytics/campaign-metrics.mdx @@ -1,20 +1,4 @@ --- -title: "Campaign Metrics" -description: "Delivery and engagement drilldown for a specific campaign." +openapi: get /analytics/campaigns/{campaignId} +description: "Get campaign analytics drill-down." --- - -Returns delivery and engagement metrics scoped to a single campaign. - -### Endpoint - -``` -GET /analytics/campaigns/{campaignId} -``` - -### Path parameters - -| Parameter | Type | Required | Description | -| ------------ | ------ | -------- | -------------------- | -| `campaignId` | string | Yes | The campaign's CUID. | - -For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/analytics/channel-metrics.mdx b/rest-api/campaigns-apis/analytics/channel-metrics.mdx index 175a17cbd..e958fa6d2 100644 --- a/rest-api/campaigns-apis/analytics/channel-metrics.mdx +++ b/rest-api/campaigns-apis/analytics/channel-metrics.mdx @@ -1,14 +1,4 @@ --- -title: "Channel Metrics" -description: "Delivery and engagement breakdown by channel type." +openapi: get /analytics/channels +description: "Get channel breakdown analytics." --- - -Returns delivery and engagement metrics broken down by channel. - -### Endpoint - -``` -GET /analytics/channels -``` - -For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/analytics/overview-metrics.mdx b/rest-api/campaigns-apis/analytics/overview-metrics.mdx index 269c2f8a4..11c336895 100644 --- a/rest-api/campaigns-apis/analytics/overview-metrics.mdx +++ b/rest-api/campaigns-apis/analytics/overview-metrics.mdx @@ -1,22 +1,4 @@ --- -title: "Overview Metrics" -description: "Aggregate delivery and engagement counts across all campaigns." +openapi: get /analytics/overview +description: "Get aggregated analytics overview." --- - -Returns aggregate notification metrics for the app over a date range. - -### Endpoint - -``` -GET /analytics/overview -``` - -### Query parameters - -| Parameter | Type | Required | Description | -| ----------- | ------ | -------- | -------------------------------------------------- | -| `period` | string | No | Granularity: `hourly` or `daily`. | -| `startDate` | string | No | Start of the window (ISO-8601 date or unix seconds). | -| `endDate` | string | No | End of the window (ISO-8601 date or unix seconds). | - -For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/analytics/template-metrics.mdx b/rest-api/campaigns-apis/analytics/template-metrics.mdx index 9af1f21e4..0bd86b05f 100644 --- a/rest-api/campaigns-apis/analytics/template-metrics.mdx +++ b/rest-api/campaigns-apis/analytics/template-metrics.mdx @@ -1,20 +1,4 @@ --- -title: "Template Metrics" -description: "Delivery and engagement drilldown for a specific template." +openapi: get /analytics/templates/{templateId} +description: "Get template analytics drill-down." --- - -Returns delivery and engagement metrics across all sends that used a specific template. - -### Endpoint - -``` -GET /analytics/templates/{templateId} -``` - -### Path parameters - -| Parameter | Type | Required | Description | -| ------------ | ------ | -------- | ----------------------------------- | -| `templateId` | string | Yes | Template CUID or `templateId` slug. | - -For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/analytics/user-metrics.mdx b/rest-api/campaigns-apis/analytics/user-metrics.mdx index 841957f0a..8041d8a93 100644 --- a/rest-api/campaigns-apis/analytics/user-metrics.mdx +++ b/rest-api/campaigns-apis/analytics/user-metrics.mdx @@ -1,43 +1,4 @@ --- -title: "User Metrics" -description: "Per-user engagement insights." +openapi: get /analytics/users/{userId} +description: "Get user-level engagement insights." --- - -Returns aggregate engagement counts and last engagement timestamp for a specific user. - -### Endpoint - -``` -GET /analytics/users/{userId} -``` - -### Path parameters - -| Parameter | Type | Required | Description | -| --------- | ------ | -------- | --------------- | -| `userId` | string | Yes | The user's UID. | - -### Query parameters - -| Parameter | Type | Required | Description | -| ----------- | ------ | -------- | ----------------------------------- | -| `startDate` | string | No | Start of the window. | -| `endDate` | string | No | End of the window. | - -### Response - -```json -{ - "data": { - "userId": "user_42", - "viewed": 47, - "clicked": 12, - "interacted": 3, - "lastEngagement": 1730301234 - } -} -``` - -This response shape is from the source-of-truth handoff. `lastEngagement` is unix seconds, or null if the user has never engaged. - -For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/campaigns/add-recipients.mdx b/rest-api/campaigns-apis/campaigns/add-recipients.mdx new file mode 100644 index 000000000..1081eb56a --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/add-recipients.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /campaigns/{id}/recipients +description: "Add recipients manually." +--- diff --git a/rest-api/campaigns-apis/campaigns/cancel-campaign.mdx b/rest-api/campaigns-apis/campaigns/cancel-campaign.mdx new file mode 100644 index 000000000..19cf72e94 --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/cancel-campaign.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /campaigns/{id}/cancel +description: "Cancel a campaign." +--- diff --git a/rest-api/campaigns-apis/campaigns/create-campaign.mdx b/rest-api/campaigns-apis/campaigns/create-campaign.mdx new file mode 100644 index 000000000..2a1c92ea5 --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/create-campaign.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /campaigns +description: "Create a new campaign." +--- diff --git a/rest-api/campaigns-apis/campaigns/delete-campaign.mdx b/rest-api/campaigns-apis/campaigns/delete-campaign.mdx new file mode 100644 index 000000000..bf1a66313 --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/delete-campaign.mdx @@ -0,0 +1,4 @@ +--- +openapi: delete /campaigns/{id} +description: "Delete a campaign (draft only)." +--- diff --git a/rest-api/campaigns-apis/campaigns/get-campaign.mdx b/rest-api/campaigns-apis/campaigns/get-campaign.mdx new file mode 100644 index 000000000..38c3631c3 --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/get-campaign.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /campaigns/{id} +description: "Get a campaign by ID." +--- diff --git a/rest-api/campaigns-apis/campaigns/import-csv.mdx b/rest-api/campaigns-apis/campaigns/import-csv.mdx new file mode 100644 index 000000000..03756099e --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/import-csv.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /campaigns/{id}/import-csv +description: "Start async CSV import for campaign recipients." +--- diff --git a/rest-api/campaigns-apis/campaigns/import-status.mdx b/rest-api/campaigns-apis/campaigns/import-status.mdx new file mode 100644 index 000000000..b26d2360d --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/import-status.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /campaigns/{id}/import-status +description: "Get current import job status." +--- diff --git a/rest-api/campaigns-apis/campaigns/list-campaigns.mdx b/rest-api/campaigns-apis/campaigns/list-campaigns.mdx new file mode 100644 index 000000000..d15792d82 --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/list-campaigns.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /campaigns +description: "List campaigns." +--- diff --git a/rest-api/campaigns-apis/campaigns/list-recipients.mdx b/rest-api/campaigns-apis/campaigns/list-recipients.mdx new file mode 100644 index 000000000..649072fb3 --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/list-recipients.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /campaigns/{id}/recipients +description: "List campaign recipients." +--- diff --git a/rest-api/campaigns-apis/campaigns/recipient-summary.mdx b/rest-api/campaigns-apis/campaigns/recipient-summary.mdx new file mode 100644 index 000000000..1672640cd --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/recipient-summary.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /campaigns/{id}/recipients/summary +description: "Get recipient status summary." +--- diff --git a/rest-api/campaigns-apis/campaigns/schedule-campaign.mdx b/rest-api/campaigns-apis/campaigns/schedule-campaign.mdx new file mode 100644 index 000000000..25dcc0c04 --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/schedule-campaign.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /campaigns/{id}/schedule +description: "Schedule campaign send." +--- diff --git a/rest-api/campaigns-apis/campaigns/send-campaign.mdx b/rest-api/campaigns-apis/campaigns/send-campaign.mdx new file mode 100644 index 000000000..4e23e915a --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/send-campaign.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /campaigns/{id}/send +description: "Trigger campaign send." +--- diff --git a/rest-api/campaigns-apis/campaigns/update-campaign.mdx b/rest-api/campaigns-apis/campaigns/update-campaign.mdx new file mode 100644 index 000000000..45b0f14d7 --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/update-campaign.mdx @@ -0,0 +1,4 @@ +--- +openapi: put /campaigns/{id} +description: "Update a campaign (draft only)." +--- diff --git a/rest-api/campaigns-apis/campaigns/upload-url.mdx b/rest-api/campaigns-apis/campaigns/upload-url.mdx new file mode 100644 index 000000000..23490a75f --- /dev/null +++ b/rest-api/campaigns-apis/campaigns/upload-url.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /campaigns/{id}/recipients/upload-url +description: "Get presigned URL for CSV upload." +--- diff --git a/rest-api/campaigns-apis/categories/create-category.mdx b/rest-api/campaigns-apis/categories/create-category.mdx new file mode 100644 index 000000000..69ac7e006 --- /dev/null +++ b/rest-api/campaigns-apis/categories/create-category.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /templates/categories +description: "Create a new template category." +--- diff --git a/rest-api/campaigns-apis/categories/delete-category.mdx b/rest-api/campaigns-apis/categories/delete-category.mdx new file mode 100644 index 000000000..96fc14014 --- /dev/null +++ b/rest-api/campaigns-apis/categories/delete-category.mdx @@ -0,0 +1,4 @@ +--- +openapi: delete /templates/categories/{id} +description: "Delete a template category." +--- diff --git a/rest-api/campaigns-apis/categories/get-category.mdx b/rest-api/campaigns-apis/categories/get-category.mdx new file mode 100644 index 000000000..40dafc476 --- /dev/null +++ b/rest-api/campaigns-apis/categories/get-category.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /templates/categories/{id} +description: "Get a template category by ID." +--- diff --git a/rest-api/campaigns-apis/categories/list-categories.mdx b/rest-api/campaigns-apis/categories/list-categories.mdx new file mode 100644 index 000000000..e19db0bd5 --- /dev/null +++ b/rest-api/campaigns-apis/categories/list-categories.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /templates/categories +description: "List all template categories." +--- diff --git a/rest-api/campaigns-apis/categories/update-category.mdx b/rest-api/campaigns-apis/categories/update-category.mdx new file mode 100644 index 000000000..fae2fc23e --- /dev/null +++ b/rest-api/campaigns-apis/categories/update-category.mdx @@ -0,0 +1,4 @@ +--- +openapi: put /templates/categories/{id} +description: "Update a template category." +--- diff --git a/rest-api/campaigns-apis/channels/create-channel.mdx b/rest-api/campaigns-apis/channels/create-channel.mdx new file mode 100644 index 000000000..62fafe298 --- /dev/null +++ b/rest-api/campaigns-apis/channels/create-channel.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /channels +description: "Create a new channel." +--- diff --git a/rest-api/campaigns-apis/channels/get-channel.mdx b/rest-api/campaigns-apis/channels/get-channel.mdx new file mode 100644 index 000000000..9f8e35585 --- /dev/null +++ b/rest-api/campaigns-apis/channels/get-channel.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /channels/{id} +description: "Get a channel by ID." +--- diff --git a/rest-api/campaigns-apis/channels/update-channel.mdx b/rest-api/campaigns-apis/channels/update-channel.mdx new file mode 100644 index 000000000..93bf8cb58 --- /dev/null +++ b/rest-api/campaigns-apis/channels/update-channel.mdx @@ -0,0 +1,4 @@ +--- +openapi: put /channels/{id} +description: "Update a channel." +--- diff --git a/rest-api/campaigns-apis/notification-feed/get-feed-item.mdx b/rest-api/campaigns-apis/notification-feed/get-feed-item.mdx new file mode 100644 index 000000000..9c57da9ee --- /dev/null +++ b/rest-api/campaigns-apis/notification-feed/get-feed-item.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /notification-feed/{id} +description: "Get a feed item by ID." +--- diff --git a/rest-api/campaigns-apis/notification-feed/get-unread-count.mdx b/rest-api/campaigns-apis/notification-feed/get-unread-count.mdx new file mode 100644 index 000000000..3684db9f0 --- /dev/null +++ b/rest-api/campaigns-apis/notification-feed/get-unread-count.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /notification-feed/unread-count +description: "Get unread count for the requesting user." +--- diff --git a/rest-api/campaigns-apis/notification-feed/list-feed.mdx b/rest-api/campaigns-apis/notification-feed/list-feed.mdx new file mode 100644 index 000000000..436a8e0ff --- /dev/null +++ b/rest-api/campaigns-apis/notification-feed/list-feed.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /notification-feed +description: "Query feed with filters and cursor pagination." +--- diff --git a/rest-api/campaigns-apis/notification-feed/mark-as-delivered.mdx b/rest-api/campaigns-apis/notification-feed/mark-as-delivered.mdx new file mode 100644 index 000000000..a71be5648 --- /dev/null +++ b/rest-api/campaigns-apis/notification-feed/mark-as-delivered.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /notification-feed/{id}/delivered +description: "Mark a feed item as delivered." +--- diff --git a/rest-api/campaigns-apis/notification-feed/mark-as-read.mdx b/rest-api/campaigns-apis/notification-feed/mark-as-read.mdx new file mode 100644 index 000000000..444acd3ee --- /dev/null +++ b/rest-api/campaigns-apis/notification-feed/mark-as-read.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /notification-feed/{id}/read +description: "Mark a feed item as read." +--- diff --git a/rest-api/campaigns-apis/notification-feed/report-engagement.mdx b/rest-api/campaigns-apis/notification-feed/report-engagement.mdx new file mode 100644 index 000000000..232848544 --- /dev/null +++ b/rest-api/campaigns-apis/notification-feed/report-engagement.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /notification-feed/{id}/engagement +description: "Report an interacted engagement event." +--- diff --git a/rest-api/campaigns-apis/notifications/send-notification.mdx b/rest-api/campaigns-apis/notifications/send-notification.mdx new file mode 100644 index 000000000..53c1d806f --- /dev/null +++ b/rest-api/campaigns-apis/notifications/send-notification.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /notifications/messages +description: "Send a notification using a template." +--- diff --git a/rest-api/campaigns-apis/push-notifications/get-push-notification.mdx b/rest-api/campaigns-apis/push-notifications/get-push-notification.mdx new file mode 100644 index 000000000..26602d559 --- /dev/null +++ b/rest-api/campaigns-apis/push-notifications/get-push-notification.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /push-notifications/{id} +description: "Get a push notification by ID." +--- diff --git a/rest-api/campaigns-apis/push-notifications/list-push-notifications.mdx b/rest-api/campaigns-apis/push-notifications/list-push-notifications.mdx new file mode 100644 index 000000000..6b0bced57 --- /dev/null +++ b/rest-api/campaigns-apis/push-notifications/list-push-notifications.mdx @@ -0,0 +1,4 @@ +--- +openapi: get /push-notifications +description: "List push notifications for a user." +--- diff --git a/rest-api/campaigns-apis/push-notifications/mark-clicked.mdx b/rest-api/campaigns-apis/push-notifications/mark-clicked.mdx new file mode 100644 index 000000000..88bb87a68 --- /dev/null +++ b/rest-api/campaigns-apis/push-notifications/mark-clicked.mdx @@ -0,0 +1,4 @@ +--- +openapi: put /push-notifications/{id}/clicked +description: "Mark push notification as clicked." +--- diff --git a/rest-api/campaigns-apis/push-notifications/mark-delivered.mdx b/rest-api/campaigns-apis/push-notifications/mark-delivered.mdx new file mode 100644 index 000000000..08ef35773 --- /dev/null +++ b/rest-api/campaigns-apis/push-notifications/mark-delivered.mdx @@ -0,0 +1,4 @@ +--- +openapi: put /push-notifications/{id}/delivered +description: "Mark push notification as delivered." +--- diff --git a/rest-api/campaigns-apis/push-notifications/report-engagement.mdx b/rest-api/campaigns-apis/push-notifications/report-engagement.mdx new file mode 100644 index 000000000..5b3d9cd06 --- /dev/null +++ b/rest-api/campaigns-apis/push-notifications/report-engagement.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /push-notifications/{id}/engagement +description: "Report push notification engagement event." +--- diff --git a/rest-api/campaigns-apis/sequences/sequence-metrics.mdx b/rest-api/campaigns-apis/sequences/sequence-metrics.mdx index 85ef6be9f..c24c8029f 100644 --- a/rest-api/campaigns-apis/sequences/sequence-metrics.mdx +++ b/rest-api/campaigns-apis/sequences/sequence-metrics.mdx @@ -1,20 +1,4 @@ --- -title: "Sequence Metrics" -description: "Per-step delivery breakdown for sequenced campaigns." +openapi: get /campaigns/{id}/sequence-metrics +description: "Get per-step sequence delivery metrics for a campaign." --- - -Returns a per-step delivery breakdown for campaigns that use channel sequencing. Each step in the sequence reports its own delivery and engagement counts. - -### Endpoint - -``` -GET /campaigns/{campaignId}/sequence-metrics -``` - -### Path parameters - -| Parameter | Type | Required | Description | -| ------------ | ------ | -------- | -------------------- | -| `campaignId` | string | Yes | The campaign's CUID. | - -For the complete error reference, see [Error Guide](/articles/error-guide). diff --git a/rest-api/campaigns-apis/templates/archive-template.mdx b/rest-api/campaigns-apis/templates/archive-template.mdx new file mode 100644 index 000000000..23367dfdd --- /dev/null +++ b/rest-api/campaigns-apis/templates/archive-template.mdx @@ -0,0 +1,4 @@ +--- +openapi: delete /templates/{id} +description: "Archive a template." +--- diff --git a/rest-api/campaigns-apis/templates/create-template.mdx b/rest-api/campaigns-apis/templates/create-template.mdx new file mode 100644 index 000000000..14d53bd77 --- /dev/null +++ b/rest-api/campaigns-apis/templates/create-template.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /templates +description: "Create a new template." +--- diff --git a/rest-api/campaigns-apis/templates/create-version.mdx b/rest-api/campaigns-apis/templates/create-version.mdx new file mode 100644 index 000000000..c4fd1b235 --- /dev/null +++ b/rest-api/campaigns-apis/templates/create-version.mdx @@ -0,0 +1,4 @@ +--- +openapi: post /templates/{id}/versions +description: "Create a new template version." +--- diff --git a/rest-api/campaigns-apis/templates/update-channel-content.mdx b/rest-api/campaigns-apis/templates/update-channel-content.mdx new file mode 100644 index 000000000..dfcc62129 --- /dev/null +++ b/rest-api/campaigns-apis/templates/update-channel-content.mdx @@ -0,0 +1,4 @@ +--- +openapi: put /templates/{id}/channels/{channelType} +description: "Update channel content for a template." +--- diff --git a/rest-api/campaigns-apis/templates/update-template.mdx b/rest-api/campaigns-apis/templates/update-template.mdx new file mode 100644 index 000000000..fc544048e --- /dev/null +++ b/rest-api/campaigns-apis/templates/update-template.mdx @@ -0,0 +1,4 @@ +--- +openapi: put /templates/{id} +description: "Update template metadata." +--- diff --git a/temp.md b/temp.md new file mode 100644 index 000000000..e2abe9f08 --- /dev/null +++ b/temp.md @@ -0,0 +1 @@ +{"openapi":"3.0.0","paths":{"/notification-feed/unread-count":{"get":{"operationId":"NotificationFeedController_getUnreadCount","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"templateCategory","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Get unread count for the requesting user","tags":["Notification Feed"]}},"/notification-feed":{"get":{"operationId":"NotificationFeedController_findFeed","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"readState","required":false,"in":"query","description":"Filter by read state","schema":{"type":"string","enum":["read","unread","all"]}},{"name":"dateFrom","required":false,"in":"query","description":"Start date filter (unix timestamp in seconds)","schema":{"type":"number"}},{"name":"dateTo","required":false,"in":"query","description":"End date filter (unix timestamp in seconds)","schema":{"type":"number"}},{"name":"tags","required":false,"in":"query","description":"Comma-separated tags to filter by","schema":{"type":"string"}},{"name":"tagMatch","required":false,"in":"query","description":"Tag matching strategy: 'any' (OR) or 'all' (AND)","schema":{"type":"string","enum":["any","all"]}},{"name":"templateCategory","required":false,"in":"query","description":"Filter by templateCategory (per-app TemplateCategory.name)","schema":{"type":"string"}},{"name":"channelId","required":false,"in":"query","description":"Filter by in-app channel instance ID","schema":{"type":"string"}},{"name":"includeDeleted","required":false,"in":"query","description":"Include soft-deleted feed items","schema":{"default":false,"type":"boolean"}},{"name":"includeExpired","required":false,"in":"query","description":"Include expired feed items","schema":{"default":false,"type":"boolean"}},{"name":"sentAt","required":false,"in":"query","description":"Cursor: sentAt unix timestamp of last item from previous page","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item from previous page","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Number of items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Field to sort by","schema":{"type":"string","enum":["sentAt","createdAt"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"receiver","required":false,"in":"query","description":"Admin-only: scope to a specific user. Ignored when onbehalfof is present.","schema":{"type":"string"}},{"name":"templateOnly","required":false,"in":"query","description":"Pass \"true\" to opt out of server-side rendering. Default: false (server renders and returns content per item).","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Query feed with filters and cursor pagination","tags":["Notification Feed"]}},"/notification-feed/{id}":{"get":{"operationId":"NotificationFeedController_findById","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"templateOnly","required":false,"in":"query","description":"Pass \"true\" to opt out of server-side rendering. Default: false (server renders and returns content).","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Get a feed item by ID","tags":["Notification Feed"]},"delete":{"operationId":"NotificationFeedController_delete","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"summary":"Soft-delete a feed item (admin only)","tags":["Notification Feed"]}},"/notification-feed/{id}/read":{"post":{"operationId":"NotificationFeedController_markAsRead","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Mark a feed item as read (idempotent)","tags":["Notification Feed"]}},"/notification-feed/{id}/delivered":{"post":{"operationId":"NotificationFeedController_markAsDelivered","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Mark a feed item as delivered (idempotent)","tags":["Notification Feed"]}},"/notification-feed/{id}/engagement":{"post":{"operationId":"NotificationFeedController_reportEngagement","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngagementEventDto"}}}},"responses":{"200":{"description":""}},"summary":"Report an interacted engagement event with optional topic discriminator","tags":["Notification Feed"]}},"/settings":{"get":{"operationId":"SettingsController_get","parameters":[{"name":"x-internal-api-key","in":"header","description":"Internal API key for platform-level settings access","required":true,"schema":{"type":"string"}},{"name":"appId","required":true,"in":"query","description":"Tenant application ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Settings retrieved"},"404":{"description":"Not found or unauthorized"}},"summary":"Get tenant settings (internal only)","tags":["Settings"]},"put":{"operationId":"SettingsController_update","parameters":[{"name":"x-internal-api-key","in":"header","description":"Internal API key for platform-level settings access","required":true,"schema":{"type":"string"}},{"name":"appId","required":true,"in":"query","description":"Tenant application ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSettingsDto"}}}},"responses":{"200":{"description":"Settings updated"},"400":{"description":"Invalid input"}},"summary":"Update tenant settings (internal only)","tags":["Settings"]}},"/templates/categories":{"post":{"operationId":"CategoriesController_create","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCategoryDto"}}}},"responses":{"201":{"description":"Category created"},"400":{"description":"Invalid input"},"409":{"description":"Duplicate category name"}},"summary":"Create a new template category (admin only)","tags":["Template Categories"]},"get":{"operationId":"CategoriesController_findAll","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["name","createdAt","updatedAt"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Categories list"}},"summary":"List all template categories","tags":["Template Categories"]}},"/templates/categories/{id}":{"get":{"operationId":"CategoriesController_findById","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Category found"},"404":{"description":"Category not found"}},"summary":"Get a template category by ID","tags":["Template Categories"]},"put":{"operationId":"CategoriesController_update","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCategoryDto"}}}},"responses":{"200":{"description":"Category updated"},"404":{"description":"Category not found"},"409":{"description":"Duplicate category name"}},"summary":"Update a template category (admin only)","tags":["Template Categories"]},"delete":{"operationId":"CategoriesController_delete","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Category deleted"},"404":{"description":"Category not found"}},"summary":"Delete a template category (admin only)","tags":["Template Categories"]}},"/templates":{"post":{"operationId":"TemplatesController_create","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTemplateDto"}}}},"responses":{"201":{"description":"Template created"},"400":{"description":"Invalid input or variable schema"},"409":{"description":"Duplicate channel type"}},"summary":"Create a new template (admin only)","tags":["Templates"]},"get":{"operationId":"TemplatesController_findAll","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["updatedAt","createdAt","name"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"status","required":false,"in":"query","description":"Filter by template status","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","description":"Search by name or templateId","schema":{"type":"string"}},{"name":"tags","required":false,"in":"query","description":"Comma-separated tags to filter by","schema":{"type":"string"}},{"name":"tagMatch","required":false,"in":"query","description":"Tag matching strategy: 'any' (OR) or 'all' (AND)","schema":{"type":"string","enum":["any","all"]}},{"name":"templateCategory","required":false,"in":"query","description":"Filter by templateCategory name","schema":{"type":"string"}}],"responses":{"200":{"description":"Templates list"}},"summary":"List all templates","tags":["Templates"]}},"/templates/{id}":{"get":{"operationId":"TemplatesController_findById","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Template found"},"404":{"description":"Template not found"}},"summary":"Get a template by ID","tags":["Templates"]},"put":{"operationId":"TemplatesController_update","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTemplateDto"}}}},"responses":{"200":{"description":"Template updated"},"400":{"description":"Invalid variable schema"},"404":{"description":"Template not found"}},"summary":"Update template metadata (admin only)","tags":["Templates"]},"delete":{"operationId":"TemplatesController_archive","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Template archived"},"404":{"description":"Template not found"}},"summary":"Archive a template (admin only)","tags":["Templates"]}},"/templates/{id}/channels/{channelType}":{"put":{"operationId":"TemplatesController_updateChannelContent","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"channelType","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateChannelContentDto"}}}},"responses":{"200":{"description":"Channel content updated"},"404":{"description":"Template not found"}},"summary":"Update channel content for a template (admin only)","tags":["Templates"]}},"/templates/{id}/versions":{"post":{"operationId":"TemplatesController_createVersion","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"Version created"},"404":{"description":"Template not found"}},"summary":"Create a new template version (admin only)","tags":["Templates"]}},"/channels":{"post":{"operationId":"ChannelsController_create","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateChannelDto"}}}},"responses":{"201":{"description":"Channel created"},"400":{"description":"Called with onbehalfof"},"403":{"description":"Channel type restricted"},"409":{"description":"Duplicate channelId or limit reached"}},"summary":"Create a new channel (admin only)","tags":["Channels"]},"get":{"operationId":"ChannelsController_list","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["channelType","createdAt","updatedAt"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"search","required":false,"in":"query","description":"Search by name or channelId","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated channel list"}},"summary":"List channels for the app","tags":["Channels"]}},"/channels/availability":{"get":{"operationId":"ChannelsController_getAvailability","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Channel type availability list"},"400":{"description":"Called with onbehalfof"}},"summary":"Get channel type availability (admin only)","tags":["Channels"]}},"/channels/{id}":{"get":{"operationId":"ChannelsController_getById","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Channel found"},"404":{"description":"Channel not found"}},"summary":"Get a channel by ID","tags":["Channels"]},"put":{"operationId":"ChannelsController_update","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateChannelDto"}}}},"responses":{"200":{"description":"Channel updated"},"400":{"description":"Called with onbehalfof"},"404":{"description":"Channel not found"}},"summary":"Update a channel (admin only)","tags":["Channels"]}},"/push-notifications":{"get":{"operationId":"PushNotificationsController_findByReceiver","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Push notifications list"}},"summary":"List push notifications for a user","tags":["Push Notifications"]}},"/push-notifications/{id}":{"get":{"operationId":"PushNotificationsController_findById","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Push notification details"}},"summary":"Get a push notification by ID","tags":["Push Notifications"]}},"/push-notifications/{id}/delivered":{"put":{"operationId":"PushNotificationsController_markDelivered","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Marked as delivered"}},"summary":"Mark push notification as delivered","tags":["Push Notifications"]}},"/push-notifications/{id}/clicked":{"put":{"operationId":"PushNotificationsController_markClicked","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Marked as clicked"}},"summary":"Mark push notification as clicked","tags":["Push Notifications"]}},"/push-notifications/{id}/engagement":{"post":{"operationId":"PushNotificationsController_reportEngagement","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushEngagementEventDto"}}}},"responses":{"200":{"description":"Engagement recorded"}},"summary":"Report push notification engagement event (interacted with optional topic)","tags":["Push Notifications"]}},"/campaigns":{"post":{"operationId":"CampaignsController_create","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCampaignDto"}}}},"responses":{"201":{"description":"Campaign created"}},"summary":"Create a new campaign","tags":["Campaigns"]},"get":{"operationId":"CampaignsController_findAll","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["updatedAt","createdAt","name"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"status","required":false,"in":"query","description":"Filter by campaign status","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","description":"Search by name","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign list"}},"summary":"List campaigns (enriched with template + recipient summary)","tags":["Campaigns"]}},"/campaigns/{id}":{"get":{"operationId":"CampaignsController_findById","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign found"},"404":{"description":"Campaign not found"}},"summary":"Get a campaign by ID (enriched)","tags":["Campaigns"]},"put":{"operationId":"CampaignsController_update","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCampaignDto"}}}},"responses":{"200":{"description":"Campaign updated"},"400":{"description":"Campaign not in draft status"},"404":{"description":"Campaign not found"}},"summary":"Update a campaign (draft only)","tags":["Campaigns"]},"delete":{"operationId":"CampaignsController_delete","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign deleted"},"400":{"description":"Campaign not in draft status"},"404":{"description":"Campaign not found"}},"summary":"Delete a campaign (draft only)","tags":["Campaigns"]}},"/campaigns/{id}/recipients":{"post":{"operationId":"CampaignsController_addRecipients","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRecipientsDto"}}}},"responses":{"201":{"description":"Recipients added"},"400":{"description":"Campaign not in draft status"}},"summary":"Add recipients manually (user IDs)","tags":["Campaigns"]},"get":{"operationId":"CampaignsController_getRecipients","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["updatedAt","createdAt"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"status","required":false,"in":"query","description":"Filter by status","schema":{"type":"string","enum":["pending","processing","completed","failed"]}}],"responses":{"200":{"description":"Recipient list"}},"summary":"List campaign recipients","tags":["Campaigns"]}},"/campaigns/{id}/recipients/summary":{"get":{"operationId":"CampaignsController_getRecipientSummary","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Recipient summary"}},"summary":"Get recipient status summary","tags":["Campaigns"]}},"/campaigns/{id}/recipients/upload-url":{"post":{"operationId":"CampaignsController_getUploadUrl","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"Presigned URL generated"},"400":{"description":"Campaign not in draft status"}},"summary":"Get S3 presigned URL for CSV upload","tags":["Campaigns"]}},"/campaigns/{id}/import-csv":{"post":{"operationId":"CampaignsController_importCsv","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CsvUploadDto"}}}},"responses":{"202":{"description":"Import job created"},"400":{"description":"Campaign not in draft status"},"409":{"description":"Import already in progress"}},"summary":"Start async CSV import for campaign recipients","tags":["Campaigns"]}},"/campaigns/{id}/import-status":{"get":{"operationId":"CampaignsController_getImportStatus","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Import status returned"},"404":{"description":"No active import found"}},"summary":"Get current import job status for a campaign","tags":["Campaigns"]}},"/campaigns/{id}/send":{"post":{"operationId":"CampaignsController_send","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign send enqueued"},"400":{"description":"Campaign not in sendable status or no recipients"}},"summary":"Trigger campaign send","tags":["Campaigns"]}},"/campaigns/{id}/schedule":{"post":{"operationId":"CampaignsController_schedule","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleCampaignDto"}}}},"responses":{"200":{"description":"Campaign scheduled"},"400":{"description":"Campaign not in draft status or invalid time"}},"summary":"Schedule campaign send","tags":["Campaigns"]}},"/campaigns/{id}/sequence-metrics":{"get":{"operationId":"CampaignsController_getSequenceMetrics","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Sequence metrics returned"}},"summary":"Get per-step sequence delivery metrics for a campaign","tags":["Campaigns"]}},"/campaigns/{id}/cancel":{"post":{"operationId":"CampaignsController_cancel","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign cancelled"},"400":{"description":"Campaign not in cancellable status"}},"summary":"Cancel a campaign","tags":["Campaigns"]}},"/notifications/messages":{"post":{"description":"Sendbird-equivalent POST /notifications/messages. 1–10 receivers = realtime (synchronous, returns notificationId). 11–10,000 receivers = inline batch (asynchronous, returns batchId). Larger sends should use the Campaigns CSV flow.","operationId":"NotificationSendController_send","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendNotificationDto"}}}},"responses":{"200":{"description":"Realtime: { notificationId, channels[], mode: \"realtime\" }. Batch: { batchId, total, channels[], mode: \"batch\" }."},"400":{"description":"Template not found, template not approved, or recipient count exceeds limits"},"403":{"description":"Admin-only — onbehalfof header must not be present"}},"summary":"Send a notification using a template","tags":["Notifications"]}},"/analytics/events":{"post":{"operationId":"AnalyticsController_recordEvent","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordEventDto"}}}},"responses":{"201":{"description":"Event recorded"},"400":{"description":"Invalid input"}},"summary":"Record an analytics event","tags":["Analytics"]}},"/analytics/overview":{"get":{"operationId":"AnalyticsController_getOverview","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Analytics overview"}},"summary":"Get aggregated analytics overview","tags":["Analytics"]}},"/analytics/campaigns/{campaignId}":{"get":{"operationId":"AnalyticsController_getCampaignAnalytics","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"campaignId","required":true,"in":"path","schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Campaign analytics"}},"summary":"Get campaign analytics drill-down","tags":["Analytics"]}},"/analytics/templates/{templateId}":{"get":{"operationId":"AnalyticsController_getTemplateAnalytics","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"templateId","required":true,"in":"path","schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Template analytics"}},"summary":"Get template analytics drill-down","tags":["Analytics"]}},"/analytics/channels":{"get":{"operationId":"AnalyticsController_getChannelBreakdown","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Channel breakdown"}},"summary":"Get channel breakdown analytics","tags":["Analytics"]}},"/analytics/users/{userId}":{"get":{"operationId":"AnalyticsController_getUserInsights","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Aggregated counts: viewed, clicked, interacted, lastEngagement (unix seconds or null)"}},"summary":"Get user-level engagement insights","tags":["Analytics"]}},"/analytics/rollup":{"post":{"operationId":"AnalyticsController_triggerRollup","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Rollup triggered"}},"summary":"Trigger rollup computation","tags":["Analytics"]}},"/health":{"get":{"operationId":"HealthController_check","parameters":[],"responses":{"200":{"description":"All dependencies healthy."},"503":{"description":"At least one dependency is down."}},"tags":["Health"]}}},"info":{"title":"Campaigns Service API","description":"Campaigns Service REST API","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"appid":{"type":"apiKey","in":"header","name":"appid"},"basic-auth":{"type":"http","scheme":"basic","description":"Service-to-service basic auth"}},"schemas":{"EngagementEventDto":{"type":"object","properties":{"topic":{"type":"string","description":"Freeform topic discriminator for the interacted event. Well-known value: \"clicked\". Custom values: any alphanumeric + underscore/hyphen string ≤64 chars.","example":"clicked","maxLength":64}}},"UpdateSettingsDto":{"type":"object","properties":{"retentionDays":{"type":"number","description":"Data retention period in days","example":90,"minimum":1},"defaultChannels":{"description":"Default channel types for new campaigns","example":["push","in_app"],"type":"array","items":{"type":"string"}},"realtimeFanout":{"type":"array","description":"Realtime fanout policy for in_app FeedItem deliveries. Empty array = feed_only (default). Allowed values: 'websocket', 'push'. Consumers of after_feed_item_sent (WebSocket fanout svc, push wake-up svc) self-filter on this array.","example":["websocket"],"items":{"type":"string","enum":["websocket","push"]}},"config":{"type":"object","description":"Additional service-level configuration. Supports: deliveryMechanisms: { websocketEnabled: boolean, pushEnabled: boolean } — controls default delivery mechanisms for announcements. Defaults: { websocketEnabled: true, pushEnabled: false }. Feed is always enabled.","example":{"timezone":"UTC","deliveryMechanisms":{"websocketEnabled":true,"pushEnabled":false}}}}},"CreateCategoryDto":{"type":"object","properties":{"name":{"type":"string","description":"Category name"},"description":{"type":"string","description":"Category description"}},"required":["name"]},"UpdateCategoryDto":{"type":"object","properties":{"name":{"type":"string","description":"Category name"},"description":{"type":"string","description":"Category description"}}},"ChannelContentDto":{"type":"object","properties":{"channelId":{"type":"string","description":"Channel instance ID (links to Channel entity)"},"channelType":{"type":"string","description":"Channel type identifier"},"content":{"type":"object","description":"Channel content (defaults applied if omitted)"},"dataType":{"type":"string","description":"Data type","enum":["ui_template","data_template"]},"categoryFilterEnabled":{"type":"boolean","description":"Enable category filter (in-app only)"},"templateLabelEnabled":{"type":"boolean","description":"Enable template label (in-app only)"},"messageRetentionHours":{"type":"number","description":"Message retention in hours"},"sequenceOrder":{"type":"number","description":"Position in sequence (1, 2, 3...)"},"stopCondition":{"type":"string","description":"Stop condition for sequence step. Allowed values depend on channelType: feed channels (in_app) accept `delivered` | `read` | `engaged`; push channels accept `delivered` | `clicked`. Validated per-channel at template save (ENG-35239)."},"waitMinutes":{"type":"number","description":"Wait time in minutes before advancing to next step","enum":[5,10,30,60,240,1440]}},"required":["channelType"]},"CreateTemplateDto":{"type":"object","properties":{"name":{"type":"string","description":"Template name"},"templateId":{"type":"string","description":"Human-readable slug (auto-generated from name if omitted)"},"templateCategory":{"type":"string","description":"Template category","enum":["Onboarding","Transactional","Marketing","Product_Showcase","Alerts","Polls","Custom"]},"label":{"type":"string","description":"Display label shown on notification (e.g. Promo, Alert)"},"alternativeText":{"type":"string","description":"Plain-text fallback when push is suppressed or rich content cannot render. Sendbird-parity field."},"tags":{"description":"First-class tags for filtering/segmentation","type":"array","items":{"type":"string"}},"status":{"type":"string","description":"Template status","enum":["draft","approved","archived"]},"channels":{"description":"Channel configurations","type":"array","items":{"$ref":"#/components/schemas/ChannelContentDto"}},"variableSchema":{"description":"Variable schema definitions","type":"array","items":{"type":"string"}},"config":{"type":"object","description":"Additional configuration"}},"required":["name","channels"]},"UpdateTemplateDto":{"type":"object","properties":{"name":{"type":"string"},"templateCategory":{"type":"string"},"label":{"type":"string","description":"Display label shown on notification"},"alternativeText":{"type":"string","description":"Plain-text fallback when push is suppressed or rich content cannot render. Sendbird-parity field."},"tags":{"description":"First-class tags for filtering/segmentation","type":"array","items":{"type":"string"}},"status":{"type":"string"},"variableSchema":{"type":"array","items":{"type":"string"}},"config":{"type":"object"}}},"UpdateChannelContentDto":{"type":"object","properties":{"content":{"type":"object","description":"Channel content payload"}},"required":["content"]},"CreateChannelDto":{"type":"object","properties":{"name":{"type":"string","description":"Channel display name","example":"My Push Channel"},"type":{"type":"string","description":"Channel type","enum":["in_app","push","sms","email","whatsapp","custom"],"example":"push"},"channelId":{"type":"string","description":"Channel slug (auto-generated from name if omitted)","example":"cc-notification-channel-my-push"},"enabled":{"type":"boolean","description":"Whether the channel is enabled","default":false},"metadata":{"type":"object","description":"Channel-specific metadata","example":{"apiKey":"xxx","senderId":"yyy"}}},"required":["name","type"]},"UpdateChannelDto":{"type":"object","properties":{"name":{"type":"string","description":"Channel display name"},"enabled":{"type":"boolean","description":"Whether the channel is enabled"},"metadata":{"type":"object","description":"Channel-specific metadata","example":{"apiKey":"xxx","senderId":"yyy"}}}},"PushEngagementEventDto":{"type":"object","properties":{"topic":{"type":"string","description":"Freeform topic discriminator for the interacted event. Custom values: any alphanumeric + underscore/hyphen string ≤64 chars.","example":"dismissed","maxLength":64}}},"CreateCampaignDto":{"type":"object","properties":{"name":{"type":"string","description":"Campaign name"},"templateId":{"type":"string","description":"Template ID (CUID or templateId slug)"},"templateVersion":{"type":"number","description":"Template version number to pin","minimum":1},"variables":{"type":"object","description":"Campaign-level default variables — applied to every recipient as a fallback layer below per-user CSV values and above template variableSchema defaults. Example: `{ \"promoCode\": \"SUMMER25\", \"supportEmail\": \"help@acme.io\" }`.","example":{"promoCode":"SUMMER25"}},"config":{"type":"object","description":"Additional campaign configuration (free-form)"}},"required":["name","templateId","templateVersion"]},"UpdateCampaignDto":{"type":"object","properties":{"name":{"type":"string","description":"Campaign name"},"config":{"type":"object","description":"Additional campaign configuration"}}},"AddRecipientsDto":{"type":"object","properties":{"userIds":{"description":"Array of user IDs to add as recipients","minItems":1,"maxItems":10000,"type":"array","items":{"type":"string"}},"userVariables":{"type":"object","description":"Per-user variables, keyed by userId. Persisted on each `CampaignRecipient.variables` row at insert time. Renderer substitutes these into template content per recipient. Example: `{ \"user_42\": { \"name\": \"Ajay\" }, \"user_43\": { \"name\": \"Sam\" } }`.","example":{"user_42":{"name":"Ajay"}}}},"required":["userIds"]},"CsvUploadDto":{"type":"object","properties":{"s3Key":{"type":"string","description":"S3 object key of the uploaded CSV file"}},"required":["s3Key"]},"ScheduleCampaignDto":{"type":"object","properties":{"scheduledAt":{"type":"number","description":"Scheduled send time as Unix timestamp (seconds)","example":1714000000}},"required":["scheduledAt"]},"SendNotificationDto":{"type":"object","properties":{"templateId":{"type":"string","description":"Template CUID or templateId slug. Template must be in approved status.","example":"order_update"},"receivers":{"description":"Array of target user IDs. 1–10 = realtime (synchronous, returns notificationId immediately). 11–10,000 = batch (asynchronous, returns batchId; processing happens via queue).","minItems":1,"maxItems":10000,"type":"array","items":{"type":"string"}},"variables":{"type":"object","description":"Per-user variables. Keyed by userId; values are { variableName: value } objects. Variables are applied to the template content at delivery time.","example":{"user_42":{"user_name":"John","order_id":"12345"},"user_43":{"user_name":"Sarah","order_id":"12346"}}},"tag":{"type":"string","description":"Optional analytics tag attached to the send (passes through to delivery records)."}},"required":["templateId","receivers"]},"RecordEventDto":{"type":"object","properties":{"notificationId":{"type":"string","description":"Notification (batch) ID this engagement relates to"},"feedItemId":{"type":"string","description":"FeedItem ID (for in-app engagement)"},"pushNotificationId":{"type":"string","description":"PushNotification ID (for push engagement)"},"campaignId":{"type":"string","description":"Campaign ID (optional)"},"templateId":{"type":"string","description":"Template ID (optional)"},"userId":{"type":"string","description":"User ID who triggered the event"},"channelType":{"type":"string","description":"Channel type (in_app, push, ...)"},"eventType":{"type":"string","description":"Engagement event type","enum":["sent","delivered","clicked","interacted","failed"]},"topic":{"type":"string","description":"Topic discriminator for interacted events (≤64 chars)"}},"required":["notificationId","userId","channelType","eventType"]}}},"security":[{"basic-auth":[]}]} \ No newline at end of file From 367f75e711814b43fc992f530a91634367589356 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 12:25:16 +0530 Subject: [PATCH 04/45] docs(campaigns): Remove unsupported user preferences section from users documentation --- campaigns/users.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/campaigns/users.mdx b/campaigns/users.mdx index 43764829d..5a6b80e5f 100644 --- a/campaigns/users.mdx +++ b/campaigns/users.mdx @@ -27,10 +27,6 @@ The Users page supports filtering by: | Status | Filter by online / offline status | | Created At | Filter by user creation date | -## User Preferences - -User-level notification preferences (opt-in/opt-out) are not currently supported. All users in the recipient list receive the notification. - ## Sending Notifications to Users You don't always need to create a campaign to reach your users. You can send notifications directly — either from the dashboard for a single user, or via API for multiple users. From 67e8937078c49c0de9c1e4beff45aae5ff72d900 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 12:56:01 +0530 Subject: [PATCH 05/45] docs(campaigns): Clarify template requirements and notification delivery options --- campaigns/templates.mdx | 2 +- campaigns/users.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/campaigns/templates.mdx b/campaigns/templates.mdx index e843de76b..49a075997 100644 --- a/campaigns/templates.mdx +++ b/campaigns/templates.mdx @@ -11,7 +11,7 @@ A template is a reusable notification design that defines the content, delivery | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Display name | -| `templateId` | string | Auto-generated | Slug: `cc-template-` (immutable) | +| `templateId` | string | Yes | Slug: `cc-template-` (immutable) | | `templateCategory` | string | No | Category name (e.g., "Marketing") | | `label` | string | No | Display label shown on notification (e.g., "Promo") | | `alternativeText` | string | No | Plain-text fallback when rich content can't render | diff --git a/campaigns/users.mdx b/campaigns/users.mdx index 5a6b80e5f..6418af841 100644 --- a/campaigns/users.mdx +++ b/campaigns/users.mdx @@ -29,7 +29,7 @@ The Users page supports filtering by: ## Sending Notifications to Users -You don't always need to create a campaign to reach your users. You can send notifications directly — either from the dashboard for a single user, or via API for multiple users. +You don't always need to create a campaign to reach your users. You can send notifications directly — either from the dashboard or via API for multiple users. ### From the Dashboard From 54b7ff7348af9316788178407192e1848d862d94 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 13:13:41 +0530 Subject: [PATCH 06/45] docs(campaigns): Remove data_template type and direct notification guidance --- campaigns/templates.mdx | 3 +-- campaigns/users.mdx | 32 -------------------------------- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/campaigns/templates.mdx b/campaigns/templates.mdx index 49a075997..88dc28cfc 100644 --- a/campaigns/templates.mdx +++ b/campaigns/templates.mdx @@ -25,7 +25,6 @@ A template is a reusable notification design that defines the content, delivery | dataType | Description | |----------|-------------| | `ui_template` | Visual template designed in the Bubble Builder (drag-and-drop editor) | -| `data_template` | Raw JSON payload for custom SDK rendering | ## Variables @@ -55,7 +54,7 @@ Each template has one or more channel configurations: | `channelType` | string | `in_app` \| `push` | | `channelId` | string | Links to a specific Channel entity | | `content` | object | Notification content (Bubble Builder JSON or custom) | -| `dataType` | string | `ui_template` \| `data_template` | +| `dataType` | string | `ui_template` | | `messageRetentionHours` | number | Hours before feed item expires (0 = never) | | `categoryFilterEnabled` | boolean | Enable category-based filtering in feed | | `templateLabelEnabled` | boolean | Show label badge on notification | diff --git a/campaigns/users.mdx b/campaigns/users.mdx index 6418af841..25e794335 100644 --- a/campaigns/users.mdx +++ b/campaigns/users.mdx @@ -31,37 +31,6 @@ The Users page supports filtering by: You don't always need to create a campaign to reach your users. You can send notifications directly — either from the dashboard or via API for multiple users. -### From the Dashboard - -On the User Detail page, there's a **Send Notification** button that lets you send a notification to that specific user. Pick a template, fill in variables, and send. - -### From Your Backend (API) - -For sending to multiple users without a campaign, use the API. The flow is: - -1. Have at least one channel and one approved template ready. -2. Call `POST /notifications/messages` from your server with the template and list of user UIDs. -3. Optionally include per-user variables for personalization (like name, order number, etc.). -4. Get a response confirming the send. - -### Delivery Modes - -| | ≤ 10 recipients | > 10 recipients | -|-|-----------------|-----------------| -| Mode | Realtime | Batch | -| Delivery | Immediate — notifications are created before the response returns | Queued — processed in the background | -| Response | `notificationId` + channels | `batchId` + total count | -| Confirmation | Response = delivery done | Response = job queued, track via webhooks | - -### Tracking Delivery - -- **Realtime (≤ 10):** A successful response means all notifications were delivered. -- **Batch (> 10):** The response confirms the job is queued. - - -There is no dashboard view for direct notification status. Only campaigns have a detail page with delivery tracking. - - For the API reference, see [Send Notification](/rest-api/campaigns-apis/notifications/send-notification). ## Limits @@ -69,4 +38,3 @@ For the API reference, see [Send Notification](/rest-api/campaigns-apis/notifica | Limit | Value | |-------|-------| | Max recipients per campaign | 10,000 | -| Users list page size | Configurable (default 20, max 100) | From d3dc2239ec39d04feec1a0bbd467e19a48a6047e Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 13:24:18 +0530 Subject: [PATCH 07/45] docs(campaigns): Simplify campaigns documentation and update API references --- campaigns.mdx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/campaigns.mdx b/campaigns.mdx index 92ee28307..6c50acc54 100644 --- a/campaigns.mdx +++ b/campaigns.mdx @@ -10,9 +10,8 @@ CometChat Campaigns is a notification management system that lets you design, ta - **Send transactional notifications** — Deliver a templated message to one or many users in real time, with per-recipient variable resolution. - **Organise content with templates** — Author reusable templates with typed variables, categories, and tags. Every notification flows through a template. -- **Run scheduled and batch campaigns** — Send to a handful of users immediately or to tens of thousands through batch and CSV-driven flows. +- **Immediate and scheduled campaigns** — Send notifications right away or schedule them for a future date and time. - **Track delivery and engagement** — Capture delivered, read, clicked, and interacted signals for analytics and unread counts. -- **Administer the in-app feed** — Soft-delete feed items, audit per-user delivery, and manage retention. ## Supported Channels @@ -33,10 +32,10 @@ The typical setup to send your first notification: Go to Categories → Create → Name it (e.g., "Marketing", "Alerts"). - Go to Templates → Create Template → Enter name → Select channel(s) → Design content in the visual builder → Set status to "Approved". + Go to Templates → Create Template → Enter name → Select channel(s) → Design content in the visual builder. - Either via Dashboard (Create a Campaign → Select template → Add recipients → Send Now) or via API (`POST /notifications/messages`). + Either via Dashboard (Create a Campaign → Select template → Add recipients → Send Now) or via [Send Notification API](/rest-api/campaigns-apis/notifications/send-notification). @@ -51,11 +50,7 @@ The typical setup to send your first notification: | Resource | Limit | |----------|-------| -| Recipients per notification (realtime) | 10 | -| Recipients per notification (batch) | 10,000 | -| Pagination page size | Max 100 | -| Max campaigns | No hard limit | -| Max templates | No hard limit | +| Recipients per notification | 10,000 | ## Common Use Cases From 4924ca3756d3f0c4a6d4e97735360af45e8cec58 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 13:53:32 +0530 Subject: [PATCH 08/45] docs(campaigns): Simplify documentation by removing advanced features and clarifying core concepts --- campaigns/categories.mdx | 20 -------------------- campaigns/channels.mdx | 2 +- campaigns/sequences.mdx | 21 ++++++--------------- campaigns/templates.mdx | 32 ++++++++++++-------------------- 4 files changed, 19 insertions(+), 56 deletions(-) diff --git a/campaigns/categories.mdx b/campaigns/categories.mdx index 067475f1d..9ee89237c 100644 --- a/campaigns/categories.mdx +++ b/campaigns/categories.mdx @@ -6,20 +6,6 @@ description: "Organize notifications with categories for filtering and user-faci Categories are labels used to organize templates and filter notification feed items. When a template has a category assigned, all notifications sent with that template carry the category — allowing end-users to filter their feed by category. -## How Categories Work - -```mermaid -flowchart LR - A[Category] --> B[Template] - B --> C[Feed Item] - C --> D[User Feed filtered by category] -``` - -- A template has a `templateCategory` field (category name) -- Feed items inherit `templateCategory` and `categoryId` from the template -- The feed API supports filtering: `GET /notification-feed?category=Marketing` -- The `categoryFilterEnabled` flag on the template channel controls whether filtering is active - ## Managing Categories 1. Go to **Categories** in the sidebar @@ -44,9 +30,3 @@ GET /notification-feed?category=Marketing This only works when `categoryFilterEnabled` is set to `true` on the template's channel configuration. -## Limits - -| Limit | Value | -|-------|-------| -| Max categories per app | No hard limit | -| Duplicate names | Not allowed (returns 409 error) | diff --git a/campaigns/channels.mdx b/campaigns/channels.mdx index 5ff695c64..b60e2decf 100644 --- a/campaigns/channels.mdx +++ b/campaigns/channels.mdx @@ -21,7 +21,7 @@ Channels define where notifications are delivered. Each template references one - **Name** — Display name (e.g., "Default Feed", "Promotions Feed") - **Channel ID** — Auto-generated slug: `cc-notification-channel-` (immutable after creation) - **Enabled** — Toggle on/off -4. Click **Save** +4. Click **Create Channel** ## Channel Properties diff --git a/campaigns/sequences.mdx b/campaigns/sequences.mdx index 2d03402c0..73bc6ffaa 100644 --- a/campaigns/sequences.mdx +++ b/campaigns/sequences.mdx @@ -7,7 +7,6 @@ Sequences let you define the order in which notification channels fire and the c ### Why use sequences -- **Reduce cost.** Start with low-cost channels (in-app feed) and only escalate to higher-cost channels (push) when the user has not engaged. - **Improve deliverability.** Target the channel most likely to reach the user based on their prior engagement pattern. - **Avoid notification fatigue.** Stop the chain as soon as the user has seen or acted on the message, rather than hitting them on every channel. @@ -16,7 +15,7 @@ Sequences let you define the order in which notification channels fire and the c Sequences are configured at the template level. When you create or edit a template in the Dashboard: 1. Add two or more channels to the template. -2. Enable the **Sequence** toggle in the template settings (`config.sequenceEnabled = true`). +2. Enable the **Sequence** toggle in the template settings. 3. Arrange the channels in the desired delivery order. The first channel fires immediately at send time. 4. For each subsequent step, configure: - **Stop condition.** The engagement signal that halts the sequence: `delivered`, `viewed`, or `clicked`. @@ -31,7 +30,7 @@ A template with two channels configured as a sequence: | Step | Channel | Stop condition | Wait | | ---- | -------- | -------------- | ------ | | 1 | In-app | delivered | 30 min | -| 2 | Push | (terminal) | n/a | +| 2 | Push | n/a | n/a | At send time, the in-app notification is dispatched immediately. If the item is marked delivered within 30 minutes, the sequence stops and push is never sent. If 30 minutes pass without a delivery signal, push fires as a fallback. @@ -39,18 +38,10 @@ At send time, the in-app notification is dispatched immediately. If the item is When a template uses sequencing, per-step delivery metrics are available through the Dashboard and the analytics API. Which metrics are reported depends on the channel: -| Channel | Requested | Delivered | Viewed | Clicked | -| -------- | :-------: | :-------: | :----: | :-----: | -| In-app | ✓ | ✓ | ✓ | ✓ | -| Push | ✓ | ✓ | n/a | ✓ | - -`Viewed` is in-app only. Push notifications do not emit a viewed signal because there is no in-feed render to scroll into view; use `Clicked` to gauge engagement on the push step. +| Channel | Requested | Delivered | Clicked | +| -------- | :-------: | :-------: | :-----: | +| In-app | ✓ | ✓ | ✓ | +| Push | ✓ | ✓ | ✓ | These metrics help you evaluate whether your sequence order and wait windows are tuned correctly, and where users are dropping off. -### Limitations - -- Sequences require at least two channels on the template. -- The stop condition applies per step, not globally. A `clicked` on step 1 stops the chain, but a `delivered` on step 1 only stops if `delivered` is the configured condition for that step. -- Wait windows are fixed increments (5, 10, 30, 60, 240, 1440 minutes). Custom durations are not supported. -- Sequences are not available for CSV-batch campaigns in the current release. They apply to transactional sends and scheduled campaigns. diff --git a/campaigns/templates.mdx b/campaigns/templates.mdx index 88dc28cfc..c03a859f4 100644 --- a/campaigns/templates.mdx +++ b/campaigns/templates.mdx @@ -16,7 +16,7 @@ A template is a reusable notification design that defines the content, delivery | `label` | string | No | Display label shown on notification (e.g., "Promo") | | `alternativeText` | string | No | Plain-text fallback when rich content can't render | | `tags` | string[] | No | Tags for filtering and segmentation | -| `status` | enum | Yes | `draft` \| `approved` \| `archived` | +| `status` | enum | Yes | `approved` \| `archived` | | `channels` | array | Yes | Channel configurations (at least one) | | `variableSchema` | array | No | Variable definitions | @@ -32,7 +32,14 @@ Variables allow per-recipient personalization in notification content. - **Syntax**: `{{variable_name}}` in template content - **Naming**: Letters, numbers, dots, and underscores only (`^[a-zA-Z_][a-zA-Z0-9_.]*`) -- **Schema**: Each variable is defined with `key`, `type`, `name` (label), `required`, and `defaultValue` +- **Schema**: Each variable has a `key`, `name` (label), `required`, and `defaultValue` +- **Type**: Selected from a dropdown with the following options: + +| Type | Label | Hint | +|------|-------|------| +| `string` | String | Text content | +| `image` | Image | Image URL | +| `action` | Action | Action URL | - **Resolution**: Per-user values are passed at send time in the `variables` field ## Template Versioning @@ -45,6 +52,8 @@ Templates are versioned to maintain a history of changes: - Campaigns pin to a specific `templateVersion` at send time - Old versions are immutable — safe for historical reference +To access old versions via API, use [Get Template](/rest-api/campaigns-apis/templates/get-template) — the response includes a `versions` array with all previous versions. + ## Channel Configuration Each template has one or more channel configurations: @@ -59,31 +68,14 @@ Each template has one or more channel configurations: | `categoryFilterEnabled` | boolean | Enable category-based filtering in feed | | `templateLabelEnabled` | boolean | Show label badge on notification | -For sequence campaigns, additional fields are available: - -| Field | Type | Description | -|-------|------|-------------| -| `stopCondition` | string | `delivered` \| `read` \| `engaged` (in_app) or `delivered` \| `clicked` (push) | -| `waitMinutes` | number | Wait time between steps: 5, 10, 30, 60, 240, or 1440 | -| `sequenceOrder` | number | Position in sequence (1, 2, 3...) | - ## Template Statuses | Status | Description | |--------|-------------| -| `draft` | Work in progress — cannot be used to send | | `approved` | Ready to use — can be selected for campaigns and notifications | | `archived` | Soft-deleted — hidden from lists, not usable | -Only templates with `approved` status can be used to send notifications. +Only templates with `approved` status can be used to send notifications. Archived templates are hidden and cannot be used. -## Limits - -| Limit | Value | -|-------|-------| -| Max templates per app | No hard limit | -| Max channels per template | No hard limit (practical: 2–3) | -| Max variables per template | No hard limit | -| Max versions per template | No hard limit | From 678fef47dd8b992694436e4ae995063d9152f7f3 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 14:26:01 +0530 Subject: [PATCH 09/45] docs(campaigns): Simplify documentation by removing advanced features and unsupported options --- campaigns/analytics.mdx | 8 -------- campaigns/campaigns.mdx | 43 ++-------------------------------------- campaigns/categories.mdx | 6 +----- campaigns/sequences.mdx | 2 +- campaigns/templates.mdx | 1 - 5 files changed, 4 insertions(+), 56 deletions(-) diff --git a/campaigns/analytics.mdx b/campaigns/analytics.mdx index 9b36d2567..ed67e94ba 100644 --- a/campaigns/analytics.mdx +++ b/campaigns/analytics.mdx @@ -15,14 +15,6 @@ Track delivery and engagement at the campaign level. Useful for comparing the pe Measure how a specific template performs across all sends that use it. Since every notification flows through a template, this is the most granular content-level view. Compare open rates between an "order shipped" template and a "weekly digest" template to understand which content resonates. -#### Channels - -Evaluate per-channel effectiveness. If your app uses both in-app and push, the channel dimension shows which channel drives more engagement and where delivery failures concentrate. - -#### Tags - -Group notifications by any free-form label set on the template (`transactional`, `launch`, `re-engagement`, team or campaign cohort). Tags cut across templates and channels, so they're the right dimension when you want a marketing-style rollup without forcing one template per cohort. - ### Metrics tracked | Metric | Description | diff --git a/campaigns/campaigns.mdx b/campaigns/campaigns.mdx index 659b8e0d2..43eae3121 100644 --- a/campaigns/campaigns.mdx +++ b/campaigns/campaigns.mdx @@ -4,17 +4,7 @@ sidebarTitle: "Campaigns" description: "Create, schedule, and manage targeted notification campaigns for batch user delivery." --- -A campaign is a batch notification send to a targeted list of users. Campaigns provide scheduling, delivery tracking, and lifecycle management on top of the core notification send. - -## Campaign vs Direct Notification - -| | Campaign | Direct Notification | -|-|----------|-------------------| -| Entry point | Dashboard wizard | API: `POST /notifications/messages` | -| Recipients | CSV upload, User picker | `receivers` array in request body | -| Scheduling | Immediate or scheduled | Always immediate | -| Tracking | Full lifecycle (status, sent/failed counts) | Notification row created, no campaign-level tracking | -| Sequences | Supported (multi-step) | Not supported | +A campaign lets you send notifications to a targeted group of users using a pre-designed template. You select recipients, choose a template, personalize content with variables, and either send immediately or schedule for later. The dashboard tracks delivery progress and shows how many notifications were sent, delivered, and failed. ## Creating a Campaign @@ -43,7 +33,6 @@ The dashboard wizard walks you through four steps: |--------|-------------| | Send Now | Immediate dispatch | | Schedule | Set a future date/time (Unix timestamp) | -| Recurring | Not supported | You can click "Send Now" on a scheduled campaign to override the schedule and send immediately. @@ -51,20 +40,8 @@ You can click "Send Now" on a scheduled campaign to override the schedule and se ## Campaign Status Flow -```mermaid -flowchart LR - A[draft] --> B[sending] - A --> C[scheduled] - C --> B - C --> D[cancelled] - B --> E[completed] - B --> F[failed] - B --> G[partially_failed] -``` - | Status | Description | |--------|-------------| -| `draft` | Campaign created but not yet sent or scheduled | | `scheduled` | Queued for future delivery | | `sending` | Currently dispatching to recipients | | `completed` | All recipients processed successfully | @@ -75,27 +52,11 @@ flowchart LR ## Cancel and Delete - **Cancel** — Cancels a scheduled or draft campaign. Cannot cancel once `sending` has started. -- **Pause** — Not supported. Once sending starts, it runs to completion. - **Delete** — Only draft campaigns can be deleted. Cancelled campaigns cannot be deleted. -## Campaign Properties - -| Field | Type | Description | -|-------|------|-------------| -| `name` | string | Campaign name | -| `templateId` | string | Template CUID | -| `templateVersion` | number | Pinned template version | -| `status` | enum | `draft` \| `scheduled` \| `sending` \| `completed` \| `failed` \| `partially_failed` \| `cancelled` | -| `scheduledAt` | number | Unix timestamp (null for immediate) | -| `totalTargets` | number | Total recipients | -| `sentCount` | number | Successfully delivered | -| `failedCount` | number | Failed deliveries | -| `tag` | string | Optional analytics tag | - ## Limits | Limit | Value | |-------|-------| | Max recipients per campaign | 10,000 | -| Max concurrent campaigns | No hard limit | -| Max campaigns per app | No hard limit | + diff --git a/campaigns/categories.mdx b/campaigns/categories.mdx index 9ee89237c..ae5166b1c 100644 --- a/campaigns/categories.mdx +++ b/campaigns/categories.mdx @@ -22,11 +22,7 @@ Categories are labels used to organize templates and filter notification feed it ## Feed Filtering -End-users can filter their notification feed by category using the SDK or feed API: - -``` -GET /notification-feed?category=Marketing -``` +End-users can filter their notification feed by category using the SDK or feed API. See [List Feed](/rest-api/campaigns-apis/notification-feed/list-feed) for the full API reference. This only works when `categoryFilterEnabled` is set to `true` on the template's channel configuration. diff --git a/campaigns/sequences.mdx b/campaigns/sequences.mdx index 73bc6ffaa..c086648d7 100644 --- a/campaigns/sequences.mdx +++ b/campaigns/sequences.mdx @@ -18,7 +18,7 @@ Sequences are configured at the template level. When you create or edit a templa 2. Enable the **Sequence** toggle in the template settings. 3. Arrange the channels in the desired delivery order. The first channel fires immediately at send time. 4. For each subsequent step, configure: - - **Stop condition.** The engagement signal that halts the sequence: `delivered`, `viewed`, or `clicked`. + - **Stop condition.** The engagement signal that halts the sequence: `delivered` or `clicked`. - **Wait window.** How long to wait for the stop condition before moving to the next step: 5, 10, 30, 60, 240, or 1440 minutes. If the stop condition is met within the wait window, the remaining steps are skipped. If the window expires without the condition being met, the next channel fires. diff --git a/campaigns/templates.mdx b/campaigns/templates.mdx index c03a859f4..4dd38857b 100644 --- a/campaigns/templates.mdx +++ b/campaigns/templates.mdx @@ -32,7 +32,6 @@ Variables allow per-recipient personalization in notification content. - **Syntax**: `{{variable_name}}` in template content - **Naming**: Letters, numbers, dots, and underscores only (`^[a-zA-Z_][a-zA-Z0-9_.]*`) -- **Schema**: Each variable has a `key`, `name` (label), `required`, and `defaultValue` - **Type**: Selected from a dropdown with the following options: | Type | Label | Hint | From 75c194335d0b616efffd63533858d3b9aebccb60 Mon Sep 17 00:00:00 2001 From: Ashfaq Ali Date: Wed, 27 May 2026 14:40:29 +0530 Subject: [PATCH 10/45] add docs for the campaigns --- sdk/android/v5/campaigns.mdx | 602 ++++++++++++++++++++++++ ui-kit/android/v6/campaigns.mdx | 152 ++++++ ui-kit/android/v6/notification-feed.mdx | 478 +++++++++++++++++++ 3 files changed, 1232 insertions(+) create mode 100644 sdk/android/v5/campaigns.mdx create mode 100644 ui-kit/android/v6/campaigns.mdx create mode 100644 ui-kit/android/v6/notification-feed.mdx diff --git a/sdk/android/v5/campaigns.mdx b/sdk/android/v5/campaigns.mdx new file mode 100644 index 000000000..dd48dfb3c --- /dev/null +++ b/sdk/android/v5/campaigns.mdx @@ -0,0 +1,602 @@ +--- +title: "Campaigns" +--- + +CometChat Campaigns lets you deliver targeted, rich notifications to users via an in-app notification feed. Each notification is a **Card Schema JSON** — a structured layout rendered natively by the CometChat Cards library. + +The SDK provides APIs to fetch feed items, listen for real-time delivery, mark items as read/delivered, report engagement, and retrieve unread counts. + +--- + +## Key Concepts + +| Concept | Description | +| --- | --- | +| **NotificationFeedItem** | A single notification in the feed. Contains Card Schema JSON in its `content` field, a `category` for filtering, timestamps, and metadata. | +| **NotificationCategory** | A category label used for filter chips (e.g., "Promotions", "Updates"). | +| **Card Schema JSON** | The fully rendered card layout (images, text, buttons) inside `NotificationFeedItem.getContent()`. Passed directly to the CometChat Cards renderer. | +| **PushNotification** | Represents a campaign push notification payload received via FCM/APNs. | + +--- + +## How Cards Render in the Notification Feed + +Each `NotificationFeedItem` has a `content` field containing a `JSONObject` — this is the **Card Schema JSON**. This JSON follows the [CometChat Card Schema v1.0 specification](https://schema.cometchat.com/card/v1.0) and is passed directly to the **CometChat Cards** renderer library (`com.cometchat:cards-android`). + +The rendering flow: + +1. Fetch feed items via `NotificationFeedRequest` +2. For each item, extract `item.getContent()` — this is the Card Schema JSON +3. Convert to string: `item.getContent().toString()` +4. Pass to the Cards renderer (`CometChatCardView` or `CometChatCardComposable`) +5. The renderer produces a native Android view from the JSON + +### Card Schema JSON Structure + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://...", "height": 200 }, + { "type": "text", "id": "txt_1", "content": "Flash Sale!", "variant": "heading2" }, + { "type": "button", "id": "btn_1", "label": "Shop Now", "action": { "type": "openUrl", "url": "https://..." } } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale! Shop Now: https://..." +} +``` + +The `body` array contains elements (text, image, button, row, column, etc.) rendered top-to-bottom. Interactive elements like buttons emit actions via a callback — the consumer handles navigation, deep links, or API calls. + +--- + +## Retrieve Notification Feed Items + +Use `NotificationFeedRequest` to fetch a paginated list of feed items. Uses cursor-based pagination internally. + +### Build the Request + + + +```java +NotificationFeedRequest request = new NotificationFeedRequest.NotificationFeedRequestBuilder() + .setLimit(20) + .build(); +``` + + +```kotlin +val request = NotificationFeedRequest.NotificationFeedRequestBuilder() + .setLimit(20) + .build() +``` + + + +### Builder Parameters + +| Method | Type | Default | Description | +| --- | --- | --- | --- | +| `setLimit(int)` | int | 20 | Items per page (max 100) | +| `setReadState(FeedReadState)` | enum | `ALL` | Filter by `READ`, `UNREAD`, or `ALL` | +| `setCategory(String)` | String | null | Filter by category ID | +| `setChannelId(String)` | String | null | Filter by channel | +| `setTags(List)` | List | null | Filter by tags | +| `setDateFrom(String)` | String | null | ISO 8601 date — items sent on or after | +| `setDateTo(String)` | String | null | ISO 8601 date — items sent on or before | + +### Fetch Items + + + +```java +request.fetchNext(new CometChat.CallbackListener>() { + @Override + public void onSuccess(List items) { + for (NotificationFeedItem item : items) { + String cardJson = item.getContent().toString(); + // Pass cardJson to CometChatCardView or CometChatCardComposable + } + } + + @Override + public void onError(CometChatException e) { + Log.e("Feed", "Error: " + e.getMessage()); + } +}); +``` + + +```kotlin +request.fetchNext(object : CometChat.CallbackListener>() { + override fun onSuccess(items: List) { + items.forEach { item -> + val cardJson = item.content.toString() + // Pass cardJson to CometChatCardView or CometChatCardComposable + } + } + + override fun onError(e: CometChatException) { + Log.e("Feed", "Error: ${e.message}") + } +}) +``` + + + +Call `fetchNext()` repeatedly for pagination. When the server has no more items, subsequent calls return an empty list. + +### NotificationFeedItem Fields + +| Field | Type | Description | +| --- | --- | --- | +| `id` | String | Unique item identifier | +| `category` | String | Notification category (e.g., "promotions") | +| `content` | JSONObject | Card Schema JSON — pass to CometChat Cards renderer | +| `readAt` | Long? | Unix timestamp when read, or null if unread | +| `deliveredAt` | Long? | Unix timestamp when delivered, or null | +| `sentAt` | long | Unix timestamp when sent | +| `metadata` | HashMap | Custom key-value metadata | +| `tags` | List\ | Tags for filtering | +| `sender` | String | Sender identifier | +| `receiver` | String | Receiver identifier | +| `receiverType` | String | Receiver type | + +--- + +## Retrieve Notification Categories + +Use `NotificationCategoriesRequest` to fetch available categories for filter chips. + + + +```java +NotificationCategoriesRequest categoriesRequest = new NotificationCategoriesRequest + .NotificationCategoriesRequestBuilder() + .setLimit(50) + .build(); + +categoriesRequest.fetchNext(new CometChat.CallbackListener>() { + @Override + public void onSuccess(List categories) { + for (NotificationCategory category : categories) { + Log.d("Feed", "Category: " + category.getName()); + } + } + + @Override + public void onError(CometChatException e) { + Log.e("Feed", "Error: " + e.getMessage()); + } +}); +``` + + +```kotlin +val categoriesRequest = NotificationCategoriesRequest.NotificationCategoriesRequestBuilder() + .setLimit(50) + .build() + +categoriesRequest.fetchNext(object : CometChat.CallbackListener>() { + override fun onSuccess(categories: List) { + categories.forEach { category -> + Log.d("Feed", "Category: ${category.name}") + } + } + + override fun onError(e: CometChatException) { + Log.e("Feed", "Error: ${e.message}") + } +}) +``` + + + +### NotificationCategory Fields + +| Field | Type | Description | +| --- | --- | --- | +| `id` | String | Category identifier | +| `name` | String | Display name for filter UI | +| `description` | String | Category description | +| `appId` | String | Associated app ID | + +--- + +## Real-Time Notification Feed Listener + +Listen for new feed items arriving via WebSocket. This listener is independent from `MessageListener`, `GroupListener`, and `CallListener`. + + + +```java +CometChat.addNotificationFeedListener("feedListener", new NotificationFeedListener() { + @Override + public void onFeedItemReceived(NotificationFeedItem feedItem) { + Log.d("Feed", "New item: " + feedItem.getId()); + String cardJson = feedItem.getContent().toString(); + // Insert at top of feed and render + } +}); +``` + + +```kotlin +CometChat.addNotificationFeedListener("feedListener", object : NotificationFeedListener() { + override fun onFeedItemReceived(feedItem: NotificationFeedItem) { + Log.d("Feed", "New item: ${feedItem.id}") + val cardJson = feedItem.content.toString() + // Insert at top of feed and render + } +}) +``` + + + +Remove the listener when no longer needed: + + + +```java +CometChat.removeNotificationFeedListener("feedListener"); +``` + + +```kotlin +CometChat.removeNotificationFeedListener("feedListener") +``` + + + +--- + +## Mark Feed Item as Read + +Mark a single item as read. Idempotent — safe to call multiple times. + + + +```java +CometChat.markFeedItemAsRead(feedItem, new CometChat.CallbackListener() { + @Override + public void onSuccess(Void unused) { + Log.d("Feed", "Marked as read"); + } + + @Override + public void onError(CometChatException e) { + Log.e("Feed", "Error: " + e.getMessage()); + } +}); +``` + + +```kotlin +CometChat.markFeedItemAsRead(feedItem, object : CometChat.CallbackListener() { + override fun onSuccess(result: Void?) { + Log.d("Feed", "Marked as read") + } + + override fun onError(e: CometChatException) { + Log.e("Feed", "Error: ${e.message}") + } +}) +``` + + + +--- + +## Mark Feed Item as Delivered + +Mark a single item as delivered. Idempotent. + + + +```java +CometChat.markFeedItemAsDelivered(feedItem, new CometChat.CallbackListener() { + @Override + public void onSuccess(Void unused) { + // Success + } + + @Override + public void onError(CometChatException e) { + Log.e("Feed", "Error: " + e.getMessage()); + } +}); +``` + + +```kotlin +CometChat.markFeedItemAsDelivered(feedItem, object : CometChat.CallbackListener() { + override fun onSuccess(result: Void?) { /* Success */ } + override fun onError(e: CometChatException) { + Log.e("Feed", "Error: ${e.message}") + } +}) +``` + + + +### Batch Delivery + +Mark multiple items as delivered at once: + + + +```java +CometChat.markFeedItemsAsDelivered(feedItems, new CometChat.CallbackListener() { + @Override + public void onSuccess(Void unused) { } + + @Override + public void onError(CometChatException e) { } +}); +``` + + +```kotlin +CometChat.markFeedItemsAsDelivered(feedItems, object : CometChat.CallbackListener() { + override fun onSuccess(result: Void?) { } + override fun onError(e: CometChatException) { } +}) +``` + + + +--- + +## Report Engagement + +Report that a user engaged with a feed item (e.g., viewed, clicked, interacted). Idempotent. + + + +```java +CometChat.reportFeedEngagement(feedItem, "clicked", new CometChat.CallbackListener() { + @Override + public void onSuccess(Void unused) { } + + @Override + public void onError(CometChatException e) { } +}); +``` + + +```kotlin +CometChat.reportFeedEngagement(feedItem, "clicked", object : CometChat.CallbackListener() { + override fun onSuccess(result: Void?) { } + override fun onError(e: CometChatException) { } +}) +``` + + + +The `interactionString` parameter is a free-form string describing the engagement (e.g., `"viewed"`, `"clicked"`, `"interacted"`). + +--- + +## Get Unread Count + +Fetch the total number of unread notification feed items. + + + +```java +CometChat.getNotificationFeedUnreadCount(new CometChat.CallbackListener() { + @Override + public void onSuccess(Integer count) { + Log.d("Feed", "Unread: " + count); + } + + @Override + public void onError(CometChatException e) { + Log.e("Feed", "Error: " + e.getMessage()); + } +}); +``` + + +```kotlin +CometChat.getNotificationFeedUnreadCount(object : CometChat.CallbackListener() { + override fun onSuccess(count: Int) { + Log.d("Feed", "Unread: $count") + } + + override fun onError(e: CometChatException) { + Log.e("Feed", "Error: ${e.message}") + } +}) +``` + + + +--- + +## Fetch Single Feed Item + +Fetch a specific item by ID — useful for deep linking from push notifications. + + + +```java +CometChat.getNotificationFeedItem("item-id-123", new CometChat.CallbackListener() { + @Override + public void onSuccess(NotificationFeedItem item) { + String cardJson = item.getContent().toString(); + // Render the card + } + + @Override + public void onError(CometChatException e) { + Log.e("Feed", "Error: " + e.getMessage()); + } +}); +``` + + +```kotlin +CometChat.getNotificationFeedItem("item-id-123", object : CometChat.CallbackListener() { + override fun onSuccess(item: NotificationFeedItem) { + val cardJson = item.content.toString() + // Render the card + } + + override fun onError(e: CometChatException) { + Log.e("Feed", "Error: ${e.message}") + } +}) +``` + + + +--- + +## Push Notification Tracking + +When a campaign push notification arrives via FCM/APNs, use these methods to report delivery and click engagement. + +### Mark Push Notification as Delivered + +Call this in your `FirebaseMessagingService.onMessageReceived()`: + + + +```java +PushNotification pushNotification = PushNotification.fromJson(pushPayloadJson); + +CometChat.markPushNotificationDelivered(pushNotification, new CometChat.CallbackListener() { + @Override + public void onSuccess(Void unused) { } + + @Override + public void onError(CometChatException e) { } +}); +``` + + +```kotlin +val pushNotification = PushNotification.fromJson(pushPayloadJson) + +CometChat.markPushNotificationDelivered(pushNotification, object : CometChat.CallbackListener() { + override fun onSuccess(result: Void?) { } + override fun onError(e: CometChatException) { } +}) +``` + + + +### Mark Push Notification as Clicked + +Call this when the user taps the push notification: + + + +```java +CometChat.markPushNotificationClicked(pushNotification, new CometChat.CallbackListener() { + @Override + public void onSuccess(Void unused) { } + + @Override + public void onError(CometChatException e) { } +}); +``` + + +```kotlin +CometChat.markPushNotificationClicked(pushNotification, object : CometChat.CallbackListener() { + override fun onSuccess(result: Void?) { } + override fun onError(e: CometChatException) { } +}) +``` + + + +### PushNotification Fields + +| Field | Type | Description | +| --- | --- | --- | +| `id` | String | Announcement ID from the push payload | +| `announcementId` | String | Same as id (for clarity) | +| `campaignId` | String? | Campaign ID if from a campaign | +| `source` | String | Always `"campaign"` for notification feed pushes | + +--- + +## FeedReadState Enum + +| Value | Description | +| --- | --- | +| `READ` | Only read items | +| `UNREAD` | Only unread items | +| `ALL` | All items (default) | + +--- + +## Rendering Cards + +The `content` field of each `NotificationFeedItem` is a Card Schema JSON object. To render it natively, use the CometChat Cards library: + + + +```kotlin +import com.cometchat.cards.CometChatCardComposable +import com.cometchat.cards.core.CometChatCardThemeMode + +@Composable +fun NotificationCard(item: NotificationFeedItem) { + CometChatCardComposable( + cardJson = item.content.toString(), + themeMode = CometChatCardThemeMode.AUTO, + onAction = { event -> + when (event.action) { + is CometChatCardOpenUrlAction -> { + // Open URL in browser + } + is CometChatCardChatWithUserAction -> { + // Navigate to chat + } + } + } + ) +} +``` + + +```kotlin +import com.cometchat.cards.CometChatCardView +import com.cometchat.cards.core.CometChatCardThemeMode + +val cardView = CometChatCardView(context) +cardView.setCardSchema(item.content.toString()) +cardView.setThemeMode(CometChatCardThemeMode.AUTO) +cardView.setActionCallback { event -> + // Handle action: event.action, event.elementId +} +parentLayout.addView(cardView) +``` + + + + +The Cards library is a pure renderer — it does not execute actions. Your code must handle action callbacks (opening URLs, navigating to chats, making API calls, etc.). + + +--- + +## Supported Card Actions + +When a user taps a button or link inside a card, the action callback receives one of these action types: + +| Action Type | Parameters | Description | +| --- | --- | --- | +| `openUrl` | url, openIn | Open a URL in browser or webview | +| `chatWithUser` | uid | Navigate to 1:1 chat | +| `chatWithGroup` | guid | Navigate to group chat | +| `sendMessage` | text, receiverUid, receiverGuid | Send a text message | +| `copyToClipboard` | value | Copy text to clipboard | +| `downloadFile` | url, filename | Download a file | +| `initiateCall` | callType (audio/video), uid, guid | Start a call | +| `apiCall` | url, method, headers, body | Make an HTTP request | +| `customCallback` | callbackId, payload | App-specific logic | diff --git a/ui-kit/android/v6/campaigns.mdx b/ui-kit/android/v6/campaigns.mdx new file mode 100644 index 000000000..627de71d2 --- /dev/null +++ b/ui-kit/android/v6/campaigns.mdx @@ -0,0 +1,152 @@ +--- +title: "Campaigns" +description: "Deliver targeted, rich notifications to users via an in-app notification feed powered by the CometChat Cards renderer." +--- + +CometChat Campaigns enables you to send rich, interactive notifications to users through an in-app notification feed. Each notification is rendered as a native card using the **CometChat Cards** library — supporting images, text, buttons, layouts, and interactive actions. + + + + + +--- + +## Overview + +Campaigns delivers notifications as **Card Schema JSON** — a structured format that defines the visual layout of each notification card. The system consists of three layers: + +1. **CometChat Chat SDK** — Fetches feed items, manages read/delivered state, provides real-time listeners, handles push notification tracking +2. **CometChat Cards Library** — Renders Card Schema JSON into native Android views (Jetpack Compose and XML Views) +3. **CometChat UI Kit** — Provides the ready-to-use `CometChatNotificationFeed` component that wires everything together + +### Architecture Flow + +``` +Dashboard / API → Campaign Created → Push + WebSocket Delivery + ↓ + SDK: NotificationFeedRequest.fetchNext() + ↓ + NotificationFeedItem.getContent() → Card Schema JSON + ↓ + Cards Library: CometChatCardView / CometChatCardComposable + ↓ + Native Rendered Card (images, text, buttons, layouts) + ↓ + User taps button → ActionCallback → Your code handles it +``` + +--- + +## How Cards Work + +Each `NotificationFeedItem` from the SDK contains a `content` field — a `JSONObject` holding the Card Schema JSON. This JSON is passed directly to the CometChat Cards renderer which produces a native view. + +The Cards library is a **pure renderer**: +- **Input**: Card Schema JSON string + theme mode + optional action callback +- **Output**: Native Android view hierarchy + +It does not execute actions, manage message state, or call any SDK methods. When users tap interactive elements (buttons, links), the library emits the action to your callback. You decide what happens — open a URL, navigate to a chat, make an API call, etc. + +### Card Schema JSON Example + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://cdn.example.com/sale.jpg", "height": 180, "fit": "cover", "borderRadius": 8 }, + { "type": "text", "id": "txt_1", "content": "🎉 Flash Sale — 40% Off!", "variant": "heading2" }, + { "type": "text", "id": "txt_2", "content": "Limited time offer on all premium plans.", "variant": "body" }, + { "type": "button", "id": "btn_1", "label": "Claim Offer", "action": { "type": "openUrl", "url": "https://example.com/offer" }, "fullWidth": true } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale — 40% Off! Claim your offer: https://example.com/offer" +} +``` + +The schema supports **20 element types** (text, image, icon, avatar, badge, divider, spacer, chip, progressBar, codeBlock, markdown, row, column, grid, accordion, tabs, button, iconButton, link, table) and **9 action types** (openUrl, chatWithUser, chatWithGroup, sendMessage, copyToClipboard, downloadFile, initiateCall, apiCall, customCallback). + +--- + +## Handling Push Notifications for Campaigns + +When a campaign push notification arrives via FCM, you should: + +1. **Report delivery** — Call `CometChat.markPushNotificationDelivered()` in your `FirebaseMessagingService` +2. **Report click** — Call `CometChat.markPushNotificationClicked()` when the user taps the notification +3. **Deep link** — Use the announcement ID from the push payload to fetch the full item via `CometChat.getNotificationFeedItem(id)` and display it + +```kotlin +// In FirebaseMessagingService.onMessageReceived() +val pushNotification = PushNotification.fromJson(data) +CometChat.markPushNotificationDelivered(pushNotification, ...) + +// When user taps the notification +CometChat.markPushNotificationClicked(pushNotification, ...) + +// Navigate to feed or show specific item +CometChat.getNotificationFeedItem(pushNotification.id, ...) +``` + +See the [SDK Campaigns documentation](/sdk/android/v5/campaigns) for the complete push notification tracking API. + +--- + +## Sending Campaigns + +Campaigns are created and managed from the **CometChat Dashboard** or via the **REST API**. The SDK and UI Kit are consumer-side — they display and interact with campaigns, not create them. + +To send campaigns: +- **Dashboard**: Navigate to Campaigns → Create Campaign → Define audience, content (Card Schema), and delivery channel +- **REST API**: Use the Campaigns API to programmatically create and schedule campaigns + +--- + +## Using the UI Kit Component + +The easiest way to add a notification feed to your app is the `CometChatNotificationFeed` component. It handles fetching, rendering, pagination, filtering, real-time updates, and engagement reporting out of the box. + + + +```kotlin +@Composable +fun NotificationsScreen() { + CometChatNotificationFeed( + modifier = Modifier.fillMaxSize(), + onItemClick = { item -> + // Handle item tap + }, + onBackPress = { /* navigate back */ } + ) +} +``` + + +```xml + +``` + +```kotlin +val feed = findViewById(R.id.notificationFeed) +feed.init(this) // Pass ViewModelStoreOwner +feed.onItemClick = { item -> /* handle tap */ } +``` + + + +See the full [CometChatNotificationFeed component documentation](/ui-kit/android/v6/notification-feed) for all configuration options, styling, and customization. + +--- + +## Next Steps + + + + Full API reference for feed items, categories, engagement, and push tracking + + + Ready-to-use component with filtering, real-time updates, and styling + + diff --git a/ui-kit/android/v6/notification-feed.mdx b/ui-kit/android/v6/notification-feed.mdx new file mode 100644 index 000000000..641430bfa --- /dev/null +++ b/ui-kit/android/v6/notification-feed.mdx @@ -0,0 +1,478 @@ +--- +title: "CometChatNotificationFeed" +description: "Full-screen notification feed component with category filtering, card rendering, real-time updates, and engagement reporting." +--- + +`CometChatNotificationFeed` displays a scrollable notification feed where each item is rendered as a native card using the CometChat Cards library. It handles fetching, pagination, category filtering, timestamp grouping, real-time updates, and read/delivered/engagement reporting automatically. + + + + + +--- + +## Where It Fits + +`CometChatNotificationFeed` is a full-screen component. Drop it into an Activity, Fragment, or navigation destination. It manages its own data fetching, state, and real-time listeners — you just handle navigation callbacks. + + + +```kotlin +@Composable +fun NotificationsScreen(onBack: () -> Unit) { + CometChatNotificationFeed( + modifier = Modifier.fillMaxSize(), + showBackButton = true, + onBackPress = onBack, + onItemClick = { item -> + // Handle item tap (e.g., open detail or deep link) + } + ) +} +``` + + +```xml + +``` + +```kotlin +val feed = findViewById(R.id.notificationFeed) +feed.init(this) // ViewModelStoreOwner (Activity or Fragment) +feed.onItemClick = { item -> /* navigate */ } +feed.onBackPress = { finish() } +``` + + + +--- + +## Quick Start + + + +```kotlin +@Composable +fun NotificationsScreen() { + CometChatNotificationFeed( + modifier = Modifier.fillMaxSize() + ) +} +``` + + +```kotlin +override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val feed = CometChatNotificationFeed(this) + setContentView(feed) + feed.init(this) +} +``` + + + +Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()` and a user logged in. + +--- + +## Filtering Feed Items + +Control what loads using custom request builders: + + + +```kotlin +CometChatNotificationFeed( + feedRequestBuilder = NotificationFeedRequest.NotificationFeedRequestBuilder() + .setLimit(30) + .setReadState(FeedReadState.UNREAD) + .setCategory("promotions") +) +``` + + +```kotlin +feed.setFeedRequestBuilder( + NotificationFeedRequest.NotificationFeedRequestBuilder() + .setLimit(30) + .setReadState(FeedReadState.UNREAD) + .setCategory("promotions") +) +feed.init(this) +``` + + + +### Filter Options + +| Builder Method | Description | +| --- | --- | +| `.setLimit(int)` | Items per page (default 20, max 100) | +| `.setReadState(FeedReadState)` | `READ`, `UNREAD`, or `ALL` | +| `.setCategory(String)` | Filter by category ID | +| `.setChannelId(String)` | Filter by channel | +| `.setTags(List)` | Filter by tags | +| `.setDateFrom(String)` | ISO 8601 date lower bound | +| `.setDateTo(String)` | ISO 8601 date upper bound | + + +Pass the builder object, not the result of `.build()`. The component calls `.build()` internally. + + +--- + +## Actions and Events + +### Callback Methods + +#### `onItemClick` + +Fires when a feed item card is tapped. + + + +```kotlin +CometChatNotificationFeed( + onItemClick = { item -> + // item.id, item.content (Card JSON), item.category + } +) +``` + + +```kotlin +feed.onItemClick = { item -> + // Handle item tap +} +``` + + + +#### `onActionClick` + +Fires when an interactive element (button, link) inside a card is tapped. + + + +```kotlin +CometChatNotificationFeed( + onActionClick = { item, actionMap -> + // actionMap contains action type and parameters + val actionType = actionMap["type"] as? String + when (actionType) { + "openUrl" -> openBrowser(actionMap["url"] as String) + "chatWithUser" -> navigateToChat(actionMap["uid"] as String) + } + } +) +``` + + +```kotlin +feed.onActionClick = { item, actionMap -> + val actionType = actionMap["type"] as? String + // Handle action +} +``` + + + +#### `onError` + +Fires when an internal error occurs (network failure, SDK exception). + + + +```kotlin +CometChatNotificationFeed( + onError = { exception -> + Log.e("Feed", "Error: ${exception.message}") + } +) +``` + + +```kotlin +feed.onError = { exception -> + Log.e("Feed", "Error: ${exception.message}") +} +``` + + + +#### `onBackPress` + +Fires when the back button in the header is tapped. + + + +```kotlin +CometChatNotificationFeed( + showBackButton = true, + onBackPress = { /* navigate back */ } +) +``` + + +```kotlin +feed.setShowBackButton(true) +feed.onBackPress = { finish() } +``` + + + +### Automatic Behaviors + +The component handles these automatically — no manual setup needed: + +| Behavior | Description | +| --- | --- | +| Real-time updates | New items appear at the top via WebSocket listener | +| Delivery reporting | Items are reported as delivered when fetched | +| Read reporting | Items are reported as read after 1 second of visibility | +| Unread count polling | Polls unread count every 30 seconds to update badges | +| Infinite scroll | Fetches next page when scrolling near the bottom | +| Pull-to-refresh | Resets and fetches fresh data on pull | +| Timestamp grouping | Groups items as "Today", "Yesterday", day name, or date | +| Category filtering | Filter chips row for category-based filtering | + +--- + +## Functionality + +| Method (XML Views) | Compose Parameter | Description | +| --- | --- | --- | +| `setTitle("Notifications")` | `title = "Notifications"` | Header title text | +| `setShowHeader(true)` | `showHeader = true` | Toggle header visibility | +| `setShowBackButton(false)` | `showBackButton = false` | Toggle back button | +| `setShowFilterChips(true)` | `showFilterChips = true` | Toggle category filter chips | +| `setFeedRequestBuilder(...)` | `feedRequestBuilder = ...` | Custom feed request | +| `setCategoriesRequestBuilder(...)` | `categoriesRequestBuilder = ...` | Custom categories request | + +--- + +## Custom View Slots + +### Header View + +Replace the entire header: + + + +```kotlin +CometChatNotificationFeed( + headerView = { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text("My Notifications", style = CometChatTheme.typography.heading1Bold) + } + } +) +``` + + + +### State Views + + + +```kotlin +CometChatNotificationFeed( + emptyStateView = { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("No notifications yet") + } + }, + errorStateView = { exception, onRetry -> + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text("Something went wrong") + Button(onClick = onRetry) { Text("Retry") } + } + }, + loadingStateView = { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } +) +``` + + + +--- + +## Style + + + +```kotlin +CometChatNotificationFeed( + style = CometChatNotificationFeedStyle( + backgroundColor = Color(0xFFF5F5F5), + headerBackgroundColor = Color.White, + headerTitleColor = Color(0xFF141414), + chipActiveBackgroundColor = Color(0xFF3399FF), + chipActiveTextColor = Color.White, + chipInactiveBackgroundColor = Color.White, + chipInactiveTextColor = Color(0xFF727272), + cardBackgroundColor = Color.White, + cardBorderColor = Color(0xFFE0E0E0), + cardBorderRadius = 12.dp, + unreadIndicatorColor = Color(0xFF3399FF) + ) +) +``` + + +```kotlin +feed.setStyle(CometChatNotificationFeedStyle( + backgroundColor = Color.parseColor("#F5F5F5"), + headerTitleColor = Color.parseColor("#141414"), + chipActiveBackgroundColor = Color.parseColor("#3399FF"), + chipActiveTextColor = Color.WHITE, + chipInactiveBackgroundColor = Color.TRANSPARENT, + chipInactiveTextColor = Color.DKGRAY, + cardBackgroundColor = Color.WHITE, + cardBorderColor = Color.parseColor("#E0E0E0"), + unreadIndicatorColor = Color.parseColor("#3399FF") +)) +``` + + + +### Style Properties + +| Property | Description | +| --- | --- | +| `backgroundColor` | Screen background color | +| `headerBackgroundColor` | Header bar background | +| `headerTitleColor` | Header title text color | +| `headerBorderColor` | Divider below header | +| `chipActiveBackgroundColor` | Selected filter chip background | +| `chipActiveTextColor` | Selected filter chip text | +| `chipInactiveBackgroundColor` | Unselected filter chip background | +| `chipInactiveTextColor` | Unselected filter chip text | +| `chipBorderColor` | Filter chip border | +| `badgeActiveBackgroundColor` | Active chip badge background | +| `badgeActiveTextColor` | Active chip badge text | +| `badgeInactiveBackgroundColor` | Inactive chip badge background | +| `badgeInactiveTextColor` | Inactive chip badge text | +| `timestampTextColor` | Section header timestamp color | +| `cardBackgroundColor` | Card container background | +| `cardBorderColor` | Card container border | +| `cardBorderRadius` | Card corner radius | +| `cardBorderWidth` | Card border width | +| `unreadIndicatorColor` | Unread dot indicator color | +| `separatorColor` | Separator between cards | + +All colors default to `Color.Unspecified` (Compose) or `0` (XML) to inherit from `CometChatTheme`. Override individual values without losing theme support. + +--- + +## ViewModel Access + +The component uses `CometChatNotificationFeedViewModel` from the shared `chatuikit-core` module. You can provide a custom ViewModel for advanced scenarios: + + + +```kotlin +val viewModel: CometChatNotificationFeedViewModel = viewModel( + factory = CometChatNotificationFeedViewModelFactory( + feedRequestBuilder = customBuilder, + pollingIntervalMs = 60_000L // 1 minute polling + ) +) + +CometChatNotificationFeed( + viewModel = viewModel +) +``` + + + +### ViewModel Factory Parameters + +| Parameter | Default | Description | +| --- | --- | --- | +| `feedRequestBuilder` | null | Custom feed request builder | +| `categoriesRequestBuilder` | null | Custom categories request builder | +| `enableListeners` | true | Enable WebSocket listeners (false for testing) | +| `pollingIntervalMs` | 30000 | Unread count polling interval in ms | + +--- + +## Common Patterns + +### Show only unread items + + + +```kotlin +CometChatNotificationFeed( + feedRequestBuilder = NotificationFeedRequest.NotificationFeedRequestBuilder() + .setReadState(FeedReadState.UNREAD) +) +``` + + + +### Hide filter chips and header + + + +```kotlin +CometChatNotificationFeed( + showHeader = false, + showFilterChips = false +) +``` + + +```kotlin +feed.setShowHeader(false) +feed.setShowFilterChips(false) +``` + + + +### Custom categories request + + + +```kotlin +CometChatNotificationFeed( + categoriesRequestBuilder = NotificationCategoriesRequest.NotificationCategoriesRequestBuilder() + .setLimit(10) +) +``` + + + +--- + +## Next Steps + + + + Overview of how campaigns work end-to-end + + + Low-level SDK APIs for feed items, categories, and engagement + + + Full styling reference for all components + + + Custom ViewModels, repositories, and ListOperations + + From 66cf942d7357b51b3d56191363d4c75e0c7dd754 Mon Sep 17 00:00:00 2001 From: Ashfaq Ali Date: Wed, 27 May 2026 14:42:55 +0530 Subject: [PATCH 11/45] add files in the docs.json --- docs.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs.json b/docs.json index bc49984d3..005da0d24 100644 --- a/docs.json +++ b/docs.json @@ -1726,7 +1726,8 @@ "ui-kit/android/v6/ai-features" ] }, - "ui-kit/android/v6/call-features" + "ui-kit/android/v6/call-features", + "ui-kit/android/v6/campaigns" ] }, { @@ -1770,7 +1771,8 @@ "ui-kit/android/v6/incoming-call", "ui-kit/android/v6/outgoing-call", "ui-kit/android/v6/call-buttons", - "ui-kit/android/v6/call-logs" + "ui-kit/android/v6/call-logs", + "ui-kit/android/v6/notification-feed" ] }, { @@ -3944,6 +3946,12 @@ "sdk/android/v5/reactions" ] }, + { + "group": "Campaigns", + "pages": [ + "sdk/android/v5/campaigns" + ] + }, { "group": "Calling", "pages": [ From e6431ed8040d6e0e7ca61e4fe15c6aa6955156e6 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 15:12:13 +0530 Subject: [PATCH 12/45] docs(campaigns): Simplify webhooks documentation and update analytics page --- campaigns.mdx | 3 + campaigns/analytics.mdx | 65 +++--- campaigns/campaigns.mdx | 2 +- campaigns/webhooks.mdx | 463 +--------------------------------------- 4 files changed, 42 insertions(+), 491 deletions(-) diff --git a/campaigns.mdx b/campaigns.mdx index 6c50acc54..0df83bbe3 100644 --- a/campaigns.mdx +++ b/campaigns.mdx @@ -78,4 +78,7 @@ Maintenance windows, policy updates, account changes. Dispatched to a broad audi Create and manage targeted notification campaigns + + Track delivery and engagement with real-time webhook events + diff --git a/campaigns/analytics.mdx b/campaigns/analytics.mdx index ed67e94ba..517b74dd2 100644 --- a/campaigns/analytics.mdx +++ b/campaigns/analytics.mdx @@ -1,52 +1,53 @@ --- title: "Analytics" -description: "Track notification delivery and engagement metrics across campaigns, templates, channels, and users." +description: "Track notification delivery and engagement metrics from the Campaigns dashboard." --- -The Campaigns analytics surface gives you visibility into how notifications perform after they leave the system. Metrics are available in the Dashboard and through the analytics API, broken down by three primary dimensions. +The Analytics page gives you a snapshot of how your notifications are performing — how many were delivered, read, and engaged with. -### Dimensions +## Metrics -#### Campaigns +| Metric | Description | +|--------|-------------| +| Delivered | Total notifications confirmed delivered | +| Read | Total feed items marked as read | +| Engagement | Total interactions (interacted events) | -Track delivery and engagement at the campaign level. Useful for comparing the performance of different sends, A/B testing subject lines, or evaluating whether a scheduled campaign hit its target audience. +## Engagement Funnel -#### Templates +A visual breakdown showing the progression from sent to engaged: -Measure how a specific template performs across all sends that use it. Since every notification flows through a template, this is the most granular content-level view. Compare open rates between an "order shipped" template and a "weekly digest" template to understand which content resonates. +- **Sent** — total notifications dispatched +- **Delivered** — as a percentage of sent +- **Read** — as a percentage of sent +- **Engagement** — as a percentage of sent -### Metrics tracked +## Recent Campaigns -| Metric | Description | -| ------------ | --------------------------------------------------------------------------- | -| Requested | Notifications dispatched to the delivery layer for this channel. | -| Delivered | Confirmed delivered to the user (device acknowledgement or feed write). | -| Viewed | User scrolled the item into view or opened the notification. | -| Clicked | User tapped a CTA or link within the notification. | -| Interacted | User performed a custom interaction (e.g. dismissed, snoozed, replied). | -| Failed | Delivery failed (provider error, invalid token, user not reachable). | +A table showing your most recent campaigns (excluding drafts) with the following columns: -### Per-user insights +| Column | Description | +|--------|-------------| +| Campaign name | Name of the campaign | +| Status | Current campaign status | +| Sent | Number of notifications sent | +| Delivered | Number confirmed delivered | +| Read | Number marked as read | +| Engagement | Number of interactions | +| Last activity | Timestamp of last activity | -You can also query engagement at the individual user level. This returns aggregate counts for a specific user over a date range: +Clicking a campaign row opens the campaign drill-down page with a detailed breakdown of that campaign's metrics. -- Total viewed, clicked, and interacted counts. -- Last engagement timestamp. +## Campaign Details -This is useful for building user-level health scores, identifying disengaged users, or powering re-engagement logic in your backend. +Shows per-campaign metrics: sent, delivered, read, and interacted breakdown for a specific campaign. -### Filtering +## Template Details -All analytics queries support: +Available from the templates list under analytics. Shows how a specific template performs across all sends that use it. -- **Date range.** Specify `startDate` and `endDate` to scope the window. -- **Period.** Choose `hourly` or `daily` granularity for time-series data. +## Per-User Engagement -### Multi-device deduplication +Per-user engagement data is not on this page — it's available on the individual User Detail page. -When a user receives or interacts with a notification on multiple devices, the same event is deduplicated and counted once. For example, if a push notification is delivered to both a phone and a tablet, it counts as a single delivery in the analytics. - -### Where to access - -- **Dashboard.** Navigate to Campaigns > Analytics for visual charts and drill-downs. -- **API.** Programmatic access to the same data via the analytics endpoints is part of the admin API surface (rolling out separately). For now, use the Dashboard for visual reports. +For programmatic access to analytics data, see the [Analytics API](/rest-api/campaigns-apis/analytics/overview-metrics). diff --git a/campaigns/campaigns.mdx b/campaigns/campaigns.mdx index 43eae3121..cc6cfa6f9 100644 --- a/campaigns/campaigns.mdx +++ b/campaigns/campaigns.mdx @@ -32,7 +32,7 @@ The dashboard wizard walks you through four steps: | Option | Description | |--------|-------------| | Send Now | Immediate dispatch | -| Schedule | Set a future date/time (Unix timestamp) | +| Schedule | Set a future date/time | You can click "Send Now" on a scheduled campaign to override the schedule and send immediately. diff --git a/campaigns/webhooks.mdx b/campaigns/webhooks.mdx index 773fe78be..416a12d03 100644 --- a/campaigns/webhooks.mdx +++ b/campaigns/webhooks.mdx @@ -1,466 +1,13 @@ --- title: "Webhooks" sidebarTitle: "Webhooks" -description: "Track campaign delivery and engagement in real-time with webhook events for campaigns, notifications, feed items, and push notifications." +description: "Track campaign delivery and engagement in real-time with webhook events." --- Campaign webhook events are triggered during the lifecycle of campaigns, notifications, feed items, and push notifications. These events allow you to track delivery and engagement in real-time. - -Campaign webhooks use the same webhook infrastructure as other CometChat webhooks. Configure your webhook endpoint in the [Webhooks settings](/rest-api/management-apis/webhooks/overview). - +Webhook configuration and event payloads are documented in the main Webhooks section. -## Event Types - -| Trigger | Description | -|---------|-------------| -| [`after_campaign_completed`](#after_campaign_completed) | Campaign finishes sending to all targets | -| [`after_campaign_failed`](#after_campaign_failed) | Campaign fails to deliver to its targets | -| [`after_notification_created`](#after_notification_created) | Notification is created and begins dispatching | -| [`after_feed_item_sent`](#after_feed_item_sent) | In-app feed item is sent to a user | -| [`after_feed_item_delivered`](#after_feed_item_delivered) | Feed item is delivered to the user's device | -| [`after_feed_item_read`](#after_feed_item_read) | User reads a feed item | -| [`after_feed_item_interacted`](#after_feed_item_interacted) | User interacts with a feed item | -| [`after_push_notification_sent`](#after_push_notification_sent) | Push notification is sent | -| [`after_push_notification_delivered`](#after_push_notification_delivered) | Push notification is delivered to the user's device | -| [`after_push_notification_clicked`](#after_push_notification_clicked) | User clicks on a push notification | - -## Status Flow - -**Feed Items:** `sent` → `delivered` → `read` → `interacted` - -**Push Notifications:** `sent` → `delivered` → `clicked` - -**Campaigns:** `dispatching` → `completed` / `failed` - ---- - -## Campaign Events - -### after\_campaign\_completed - -Triggered when a campaign finishes sending to all targets. - - - -```json after_campaign_completed -{ - "trigger": "after_campaign_completed", - "data": { - "campaignId": "", - "appId": "", - "name": "Welcome Campaign", - "status": "completed", - "templateId": "", - "templateVersion": 1, - "totalTargets": 100, - "sentCount": 100, - "failedCount": 0, - "tag": "onboarding", - "scheduledAt": 1696930000, - "sentAt": 1696932000, - "completedAt": 1696932060, - "createdAt": 1696929000, - "updatedAt": 1696932060 - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - -### after\_campaign\_failed - -Triggered when a campaign fails to deliver to its targets. - - - -```json after_campaign_failed -{ - "trigger": "after_campaign_failed", - "data": { - "campaignId": "", - "appId": "", - "name": "Promo Campaign", - "status": "failed", - "templateId": "", - "templateVersion": 1, - "totalTargets": 50, - "sentCount": 0, - "failedCount": 50, - "tag": "promo", - "scheduledAt": 1696930000, - "sentAt": 1696932000, - "completedAt": 1696932070, - "createdAt": 1696929000, - "updatedAt": 1696932070 - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - ---- - -## Notification Events - -### after\_notification\_created - -Triggered when a notification is created and begins dispatching. - - - -```json after_notification_created -{ - "trigger": "after_notification_created", - "data": { - "notificationId": "", - "appId": "", - "templateId": "", - "templateVersion": 1, - "category": "updates", - "label": "weekly-digest", - "tags": ["digest", "weekly"], - "sendMode": "realtime", - "campaignId": "", - "priority": "normal", - "channels": ["in_app"], - "dataType": "ui_template", - "status": "dispatching", - "totalTargets": 3, - "sentCount": 0, - "failedCount": 0, - "createdAt": 1696932000 - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - ---- - -## Feed Item Events - -### after\_feed\_item\_sent - -Triggered when an in-app feed item is sent to a user. - - - -```json after_feed_item_sent -{ - "trigger": "after_feed_item_sent", - "data": { - "id": "", - "appId": "", - "notificationId": "", - "campaignId": "", - "receiver": "cometchat-uid-1", - "templateId": "", - "templateVersion": 1, - "channelType": "in_app", - "channelId": "", - "templateCategory": "promotions", - "categoryId": "", - "label": "promo", - "tags": ["offer", "summer"], - "alternativeText": "You have a new offer!", - "dataType": "ui_template", - "status": "sent", - "sentAt": 1696932000, - "realtimeFanout": ["websocket"], - "content": { - "version": "1.0", - "body": [ - {"id": "el_1", "type": "text", "content": "Hello!", "variant": "heading2"}, - {"id": "el_2", "type": "divider"}, - {"id": "el_3", "type": "text", "content": "Your notification message here.", "variant": "body"} - ], - "style": { - "background": {"light": "#E8E8E8", "dark": "#E8E8E8"}, - "borderRadius": 16, - "borderColor": {"light": "#DFE6E9", "dark": "#DFE6E9"}, - "padding": 12 - } - } - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - -### after\_feed\_item\_delivered - -Triggered when a feed item is delivered to the user's device. - - - -```json after_feed_item_delivered -{ - "trigger": "after_feed_item_delivered", - "data": { - "id": "", - "appId": "", - "notificationId": "", - "campaignId": "", - "receiver": "cometchat-uid-1", - "templateId": "", - "templateVersion": 1, - "channelType": "in_app", - "channelId": "", - "templateCategory": "promotions", - "categoryId": "", - "label": "promo", - "tags": ["offer", "summer"], - "alternativeText": "You have a new offer!", - "dataType": "ui_template", - "status": "delivered", - "sentAt": 1696932000, - "deliveredAt": 1696932060, - "engagedAt": 1696932055, - "realtimeFanout": ["websocket"] - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - -### after\_feed\_item\_read - -Triggered when a user reads a feed item. - - - -```json after_feed_item_read -{ - "trigger": "after_feed_item_read", - "data": { - "id": "", - "appId": "", - "notificationId": "", - "campaignId": "", - "receiver": "cometchat-uid-1", - "templateId": "", - "templateVersion": 1, - "channelType": "in_app", - "channelId": "", - "templateCategory": "promotions", - "categoryId": "", - "label": "promo", - "tags": ["offer", "summer"], - "alternativeText": "You have a new offer!", - "dataType": "ui_template", - "variables": {}, - "status": "read", - "sentAt": 1696932000, - "deliveredAt": 1696932060, - "readAt": 1696932120, - "metadata": {}, - "realtimeFanout": ["websocket"] - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - -### after\_feed\_item\_interacted - -Triggered when a user interacts with a feed item (e.g., clicks a button or link). - - - -```json after_feed_item_interacted -{ - "trigger": "after_feed_item_interacted", - "data": { - "id": "", - "appId": "", - "notificationId": "", - "campaignId": "", - "receiver": "cometchat-uid-1", - "templateId": "", - "templateVersion": 1, - "channelType": "in_app", - "channelId": "", - "templateCategory": "promotions", - "categoryId": "", - "label": "promo", - "tags": ["offer", "summer"], - "alternativeText": "You have a new offer!", - "dataType": "ui_template", - "variables": {}, - "status": "read", - "sentAt": 1696932000, - "deliveredAt": 1696932060, - "readAt": 1696932120, - "interactedAt": 1696932180, - "metadata": {}, - "realtimeFanout": ["websocket"] - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - ---- - -## Push Notification Events - -### after\_push\_notification\_sent - -Triggered when a push notification is sent. - - - -```json after_push_notification_sent -{ - "trigger": "after_push_notification_sent", - "data": { - "pushNotificationId": "", - "appId": "", - "notificationId": "", - "campaignId": "", - "receiver": "cometchat-uid-1", - "templateId": "", - "templateVersion": 1, - "channelType": "push", - "channelId": "", - "category": "promotions", - "label": "summer-sale", - "tags": ["offer", "summer"], - "alternativeText": "Check out our summer sale!", - "dataType": "ui_template", - "variables": {}, - "status": "sent", - "sentAt": 1696932000, - "tag": "promo", - "metadata": {} - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - -### after\_push\_notification\_delivered - -Triggered when a push notification is delivered to the user's device. - - - -```json after_push_notification_delivered -{ - "trigger": "after_push_notification_delivered", - "data": { - "pushNotificationId": "", - "appId": "", - "notificationId": "", - "campaignId": "", - "receiver": "cometchat-uid-1", - "templateId": "", - "templateVersion": 1, - "channelType": "push", - "channelId": "", - "category": "promotions", - "label": "summer-sale", - "tags": ["offer", "summer"], - "alternativeText": "Check out our summer sale!", - "dataType": "ui_template", - "variables": {}, - "status": "delivered", - "sentAt": 1696932000, - "deliveredAt": 1696932005, - "tag": "promo" - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - -### after\_push\_notification\_clicked - -Triggered when a user clicks/taps on a push notification. - - - -```json after_push_notification_clicked -{ - "trigger": "after_push_notification_clicked", - "data": { - "pushNotificationId": "", - "appId": "", - "notificationId": "", - "campaignId": "", - "receiver": "cometchat-uid-1", - "templateId": "", - "templateVersion": 1, - "channelType": "push", - "channelId": "", - "category": "promotions", - "label": "summer-sale", - "tags": ["offer", "summer"], - "alternativeText": "Check out our summer sale!", - "dataType": "ui_template", - "variables": {}, - "status": "clicked", - "sentAt": 1696932000, - "deliveredAt": 1696932005, - "clickedAt": 1696932060, - "tag": "promo", - "metadata": {} - }, - "appId": "", - "region": "", - "webhook": "" -} -``` - - - ---- - -## Common Fields - -| Field | Description | -|-------|-------------| -| `trigger` | The event name | -| `appId` | Your CometChat app ID | -| `region` | The region of the app | -| `webhook` | The webhook ID that received this event | -| `data.campaignId` | The campaign this event belongs to (null if sent directly via API) | -| `data.notificationId` | Unique identifier for the notification batch | -| `data.templateId` | The template used for this notification | -| `data.templateVersion` | Version of the template | -| `data.receiver` | The UID of the target user | -| `data.channelType` | Delivery channel: `in_app` or `push` | -| `data.channelId` | The channel configuration ID | -| `data.status` | Current status of the item | -| `data.tags` | Custom tags associated with the notification | -| `data.label` | Custom label for categorization | -| `data.variables` | Template variables resolved for the receiver | -| `data.metadata` | Custom metadata attached to the notification | + + View webhook setup, trigger list, and payload examples + From f9d33405acec5e1128944a73f0371239a91074e8 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 15:20:14 +0530 Subject: [PATCH 13/45] docs(campaigns): Remove webhooks documentation and simplify notification settings --- campaigns.mdx | 3 -- campaigns/notification-settings.mdx | 69 +++-------------------------- docs.json | 3 +- 3 files changed, 7 insertions(+), 68 deletions(-) diff --git a/campaigns.mdx b/campaigns.mdx index 0df83bbe3..6c50acc54 100644 --- a/campaigns.mdx +++ b/campaigns.mdx @@ -78,7 +78,4 @@ Maintenance windows, policy updates, account changes. Dispatched to a broad audi Create and manage targeted notification campaigns - - Track delivery and engagement with real-time webhook events - diff --git a/campaigns/notification-settings.mdx b/campaigns/notification-settings.mdx index 673aa0231..153896772 100644 --- a/campaigns/notification-settings.mdx +++ b/campaigns/notification-settings.mdx @@ -1,70 +1,13 @@ --- title: "Notification Settings" sidebarTitle: "Notification Settings" -description: "Configure push notification providers (FCM, APNs, Custom) for delivering campaign notifications." +description: "Configure push notification providers for delivering campaign notifications." --- -The Notification Settings page lets you manage push notification provider credentials for your app. You must configure at least one provider to deliver push notifications through campaigns. +The Notification Settings page lets you configure push notification providers (FCM, APNs, Custom) for your app. These credentials are required to deliver push notifications through campaigns. -## Push Notifications +For detailed setup instructions, provider configuration, and credential management, visit the Push Notifications documentation. -A global toggle at the top of the page enables or disables push notifications for the entire app. - -## Providers - -### Firebase Cloud Messaging (FCM) - -| Field | Type | Description | -|-------|------|-------------| -| `providerId` | string | Unique name/identifier for this credential | -| `serviceAccountFilename` | string | Name of the uploaded JSON file | -| `serviceAccountCreds` | object | Service account JSON contents (`project_id`, `client_email`, `private_key`, `private_key_id`) | -| `notificationInPayload` | object | Per-platform toggle for chat/call notifications | - -The `notificationInPayload` field controls whether notifications are included in the payload per platform: - -```json -{ - "ios": { "chat": true, "call": true }, - "android": { "chat": true, "call": true }, - "web": { "chat": true, "call": false } -} -``` - -### Apple Push Notification service (APNs) - -| Field | Type | Description | -|-------|------|-------------| -| `providerId` | string | Unique name/identifier for this credential | -| `productionMode` | boolean | `true` for production, `false` for development/sandbox | -| `teamId` | string | Apple Team ID | -| `bundleId` | string | App Bundle ID | -| `keyId` | string | APNs Key ID | -| `p8KeyFilename` | string | Uploaded `.p8` key filename | -| `p8Key` | string | The `.p8` key content | -| `includeContentAvailable` | boolean | Include `content-available` flag in push payload | -| `includeMutableContent` | boolean | Include `mutable-content` flag in push payload | - -### Custom Push Notification Provider - -| Field | Type | Description | -|-------|------|-------------| -| `webhookURL` | string | Your webhook endpoint URL | -| `isEnabled` | boolean | Enable/disable the custom provider | -| `useBasicAuth` | boolean | Enable Basic Authentication | -| `basicAuthUsername` | string | Username (required if `useBasicAuth` is true) | -| `basicAuthPassword` | string | Password (required if `useBasicAuth` is true) | - -## Multiple Credentials - -- **FCM** — Multiple credentials supported. Set one as default (star icon). -- **APNs** — Multiple credentials supported. Set one as default (star icon). -- **Custom** — Limited to a single instance. - -## Validation - -Credentials are saved without a test connection. Validation happens at delivery time — if credentials are invalid, push notifications will fail with an error in the campaign delivery report. - - -There is no auto-verify or test connection feature. Ensure your credentials are correct before sending campaigns. - + + Configure FCM, APNs, and custom push providers + diff --git a/docs.json b/docs.json index 25a30d3d1..aa8a9d6ff 100644 --- a/docs.json +++ b/docs.json @@ -6495,8 +6495,7 @@ "icon": "puzzle-piece", "pages": [ "campaigns/sequences", - "campaigns/analytics", - "campaigns/webhooks" + "campaigns/analytics" ] } ] From 7b038ed2d53153d3f0c314b8a67211fc6df1de31 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 15:24:48 +0530 Subject: [PATCH 14/45] docs(campaigns): Update API endpoint parameter names for consistency --- rest-api/campaigns-apis/notification-feed/delete-feed-item.mdx | 2 +- rest-api/campaigns-apis/templates/get-template.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rest-api/campaigns-apis/notification-feed/delete-feed-item.mdx b/rest-api/campaigns-apis/notification-feed/delete-feed-item.mdx index 15dc7048e..963145519 100644 --- a/rest-api/campaigns-apis/notification-feed/delete-feed-item.mdx +++ b/rest-api/campaigns-apis/notification-feed/delete-feed-item.mdx @@ -1,5 +1,5 @@ --- -openapi: delete /notification-feed/{feedItemId} +openapi: delete /notification-feed/{id} description: "Soft-delete a feed item." --- diff --git a/rest-api/campaigns-apis/templates/get-template.mdx b/rest-api/campaigns-apis/templates/get-template.mdx index 9cc40f82f..03861bdeb 100644 --- a/rest-api/campaigns-apis/templates/get-template.mdx +++ b/rest-api/campaigns-apis/templates/get-template.mdx @@ -1,5 +1,5 @@ --- -openapi: get /templates/{templateId} +openapi: get /templates/{id} description: "Fetch a single template by CUID or slug." --- From 51356201c92fc7158b367480cb891b9a88298e51 Mon Sep 17 00:00:00 2001 From: Ashfaq Ali Date: Wed, 27 May 2026 15:33:54 +0530 Subject: [PATCH 15/45] update docs --- docs.json | 138 ++--------------------------------- sdk/android/v5/campaigns.mdx | 32 +------- 2 files changed, 8 insertions(+), 162 deletions(-) diff --git a/docs.json b/docs.json index 457a99152..ac2e4a6b4 100644 --- a/docs.json +++ b/docs.json @@ -1590,7 +1590,8 @@ "ui-kit/android/v6/ai-features" ] }, - "ui-kit/android/v6/call-features" + "ui-kit/android/v6/call-features", + "ui-kit/android/v6/campaigns" ] }, { @@ -1636,7 +1637,8 @@ "ui-kit/android/v6/call-buttons", "ui-kit/android/v6/call-logs", "ui-kit/android/v6/search", - "ui-kit/android/v6/ai-assistant-chat-history" + "ui-kit/android/v6/ai-assistant-chat-history", + "ui-kit/android/v6/notification-feed" ] }, { @@ -1795,121 +1797,6 @@ } ] }, - { - "version": "v6‎‎‎", - "groups": [ - { - "group": " ", - "pages": [ - "ui-kit/android/v6/overview", - { - "group": "Getting Started", - "pages": [ - "ui-kit/android/v6/getting-started", - "ui-kit/android/v6/getting-started-kotlin", - "ui-kit/android/v6/getting-started-jetpack", - "ui-kit/android/v6/one-to-one-chat", - "ui-kit/android/v6/tab-based-chat", - "ui-kit/android/v6/calling-integration" - ] - }, - { - "group": "Features", - "pages": [ - { - "group": "Chat", - "pages": [ - "ui-kit/android/v6/core-features", - "ui-kit/android/v6/extensions", - "ui-kit/android/v6/ai-features" - ] - }, - "ui-kit/android/v6/call-features", - "ui-kit/android/v6/campaigns" - ] - }, - { - "group": "Theming", - "pages": [ - "ui-kit/android/v6/theme-introduction", - "ui-kit/android/v6/color-resources", - "ui-kit/android/v6/component-styling", - "ui-kit/android/v6/message-bubble-styling", - "ui-kit/android/v6/localize", - "ui-kit/android/v6/sound-manager" - ] - }, - { - "group": "Customization", - "pages": [ - "ui-kit/android/v6/customization-overview", - "ui-kit/android/v6/customization-view-slots", - "ui-kit/android/v6/customization-styles", - "ui-kit/android/v6/customization-viewmodel-data", - "ui-kit/android/v6/customization-events", - "ui-kit/android/v6/customization-state-views", - "ui-kit/android/v6/customization-text-formatters", - "ui-kit/android/v6/customization-menu-options" - ] - }, - { - "group": "Components", - "pages": [ - "ui-kit/android/v6/components-overview", - "ui-kit/android/v6/conversations", - "ui-kit/android/v6/conversation-message-view", - "ui-kit/android/v6/users", - "ui-kit/android/v6/groups", - "ui-kit/android/v6/group-members", - "ui-kit/android/v6/message-header", - "ui-kit/android/v6/message-list", - "ui-kit/android/v6/message-composer", - "ui-kit/android/v6/message-template", - "ui-kit/android/v6/threaded-messages-header", - "ui-kit/android/v6/incoming-call", - "ui-kit/android/v6/outgoing-call", - "ui-kit/android/v6/call-buttons", - "ui-kit/android/v6/call-logs", - "ui-kit/android/v6/notification-feed" - ] - }, - { - "group": "Reference", - "pages": [ - "ui-kit/android/v6/methods", - "ui-kit/android/v6/events" - ] - }, - { - "group": "Guides", - "pages": [ - "ui-kit/android/v6/guide-overview", - "ui-kit/android/v6/guide-threaded-messages", - "ui-kit/android/v6/guide-block-unblock-user", - "ui-kit/android/v6/guide-new-chat", - "ui-kit/android/v6/guide-message-privately", - "ui-kit/android/v6/guide-call-log-details", - "ui-kit/android/v6/guide-group-chat", - "ui-kit/android/v6/custom-text-formatter-guide", - "ui-kit/android/v6/mentions-formatter-guide", - "ui-kit/android/v6/shortcut-formatter-guide" - ] - }, - "ui-kit/android/v6/architecture-data-flow", - { - "group": "Migration Guide", - "pages": [ - "ui-kit/android/v6/upgrading-from-v5" - ] - }, - "ui-kit/android/v6/troubleshooting", - "ui-kit/android/v6/link/sample", - "ui-kit/android/v6/link/figma", - "ui-kit/android/v6/link/changelog" - ] - } - ] - }, { "version": "v4‎‎‎", "groups": [ @@ -4100,21 +3987,6 @@ "sdk/android/v5/campaigns" ] }, - { - "group": "Calling", - "pages": [ - "sdk/android/v5/calling-overview", - "sdk/android/v5/calling-setup", - "sdk/android/v5/default-calling", - "sdk/android/v5/direct-calling", - "sdk/android/v5/standalone-calling", - "sdk/android/v5/recording", - "sdk/android/v5/video-view-customisation", - "sdk/android/v5/presenter-mode", - "sdk/android/v5/call-logs", - "sdk/android/v5/session-timeout" - ] - }, "sdk/android/v5/calling-overview", { "group": "Users", @@ -9819,4 +9691,4 @@ "redirect": true } } -} +} \ No newline at end of file diff --git a/sdk/android/v5/campaigns.mdx b/sdk/android/v5/campaigns.mdx index dd48dfb3c..ef021183f 100644 --- a/sdk/android/v5/campaigns.mdx +++ b/sdk/android/v5/campaigns.mdx @@ -15,13 +15,13 @@ The SDK provides APIs to fetch feed items, listen for real-time delivery, mark i | **NotificationFeedItem** | A single notification in the feed. Contains Card Schema JSON in its `content` field, a `category` for filtering, timestamps, and metadata. | | **NotificationCategory** | A category label used for filter chips (e.g., "Promotions", "Updates"). | | **Card Schema JSON** | The fully rendered card layout (images, text, buttons) inside `NotificationFeedItem.getContent()`. Passed directly to the CometChat Cards renderer. | -| **PushNotification** | Represents a campaign push notification payload received via FCM/APNs. | +| **PushNotification** | Represents a campaign push notification payload received via FCM. | --- ## How Cards Render in the Notification Feed -Each `NotificationFeedItem` has a `content` field containing a `JSONObject` — this is the **Card Schema JSON**. This JSON follows the [CometChat Card Schema v1.0 specification](https://schema.cometchat.com/card/v1.0) and is passed directly to the **CometChat Cards** renderer library (`com.cometchat:cards-android`). +Each `NotificationFeedItem` has a `content` field containing a `JSONObject` — this is the **Card Schema JSON**. This JSON is passed directly to the **CometChat Cards** renderer library (`com.cometchat:cards-android`). The rendering flow: @@ -319,32 +319,6 @@ CometChat.markFeedItemAsDelivered(feedItem, object : CometChat.CallbackListener< -### Batch Delivery - -Mark multiple items as delivered at once: - - - -```java -CometChat.markFeedItemsAsDelivered(feedItems, new CometChat.CallbackListener() { - @Override - public void onSuccess(Void unused) { } - - @Override - public void onError(CometChatException e) { } -}); -``` - - -```kotlin -CometChat.markFeedItemsAsDelivered(feedItems, object : CometChat.CallbackListener() { - override fun onSuccess(result: Void?) { } - override fun onError(e: CometChatException) { } -}) -``` - - - --- ## Report Engagement @@ -455,7 +429,7 @@ CometChat.getNotificationFeedItem("item-id-123", object : CometChat.CallbackList ## Push Notification Tracking -When a campaign push notification arrives via FCM/APNs, use these methods to report delivery and click engagement. +When a campaign push notification arrives via FCM, use these methods to report delivery and click engagement. ### Mark Push Notification as Delivered From 2454af159427baf712264e0b0138b43b9cacd11c Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 15:37:13 +0530 Subject: [PATCH 16/45] style(campaigns-apis): Format JSON schema objects with consistent indentation --- campaigns-apis.json | 1913 +++++++++++++++++++++++++++++++------------ 1 file changed, 1403 insertions(+), 510 deletions(-) diff --git a/campaigns-apis.json b/campaigns-apis.json index 7fd1df173..594f56a5b 100644 --- a/campaigns-apis.json +++ b/campaigns-apis.json @@ -10,25 +10,37 @@ "in": "header", "description": "UID of user making client request", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "templateCategory", "required": true, "in": "query", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "" } }, + "responses": { + "200": { + "description": "" + } + }, "summary": "Get unread count for the requesting user", - "tags": ["Notification Feed"] + "tags": [ + "Notification Feed" + ] } }, "/notification-feed": { @@ -40,138 +52,207 @@ "in": "header", "description": "UID of user making client request", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "readState", "required": false, "in": "query", "description": "Filter by read state", - "schema": { "type": "string", "enum": ["read", "unread", "all"] } + "schema": { + "type": "string", + "enum": [ + "read", + "unread", + "all" + ] + } }, { "name": "dateFrom", "required": false, "in": "query", "description": "Start date filter (unix timestamp in seconds)", - "schema": { "type": "number" } + "schema": { + "type": "number" + } }, { "name": "dateTo", "required": false, "in": "query", "description": "End date filter (unix timestamp in seconds)", - "schema": { "type": "number" } + "schema": { + "type": "number" + } }, { "name": "tags", "required": false, "in": "query", "description": "Comma-separated tags to filter by", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "tagMatch", "required": false, "in": "query", "description": "Tag matching strategy: 'any' (OR) or 'all' (AND)", - "schema": { "type": "string", "enum": ["any", "all"] } + "schema": { + "type": "string", + "enum": [ + "any", + "all" + ] + } }, { "name": "templateCategory", "required": false, "in": "query", "description": "Filter by templateCategory (per-app TemplateCategory.name)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "channelId", "required": false, "in": "query", "description": "Filter by in-app channel instance ID", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "includeDeleted", "required": false, "in": "query", "description": "Include soft-deleted feed items", - "schema": { "default": false, "type": "boolean" } + "schema": { + "default": false, + "type": "boolean" + } }, { "name": "includeExpired", "required": false, "in": "query", "description": "Include expired feed items", - "schema": { "default": false, "type": "boolean" } + "schema": { + "default": false, + "type": "boolean" + } }, { "name": "sentAt", "required": false, "in": "query", "description": "Cursor: sentAt unix timestamp of last item from previous page", - "schema": { "type": "number" } + "schema": { + "type": "number" + } }, { "name": "id", "required": false, "in": "query", "description": "Cursor: id of last item from previous page", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "affix", "required": false, "in": "query", "description": "Cursor direction", - "schema": { "type": "string", "enum": ["append", "prepend"] } + "schema": { + "type": "string", + "enum": [ + "append", + "prepend" + ] + } }, { "name": "limit", "required": false, "in": "query", "description": "Number of items per page", - "schema": { "minimum": 1, "maximum": 100, "type": "number" } + "schema": { + "minimum": 1, + "maximum": 100, + "type": "number" + } }, { "name": "sort", "required": false, "in": "query", "description": "Field to sort by", - "schema": { "type": "string", "enum": ["sentAt", "createdAt"] } + "schema": { + "type": "string", + "enum": [ + "sentAt", + "createdAt" + ] + } }, { "name": "order", "required": false, "in": "query", "description": "Sort direction", - "schema": { "type": "string", "enum": ["asc", "desc"] } + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } }, { "name": "receiver", "required": false, "in": "query", "description": "Admin-only: scope to a specific user. Ignored when onbehalfof is present.", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "templateOnly", "required": false, "in": "query", "description": "Pass \"true\" to opt out of server-side rendering. Default: false (server renders and returns content per item).", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "" } }, + "responses": { + "200": { + "description": "" + } + }, "summary": "Query feed with filters and cursor pagination", - "tags": ["Notification Feed"] + "tags": [ + "Notification Feed" + ] } }, "/notification-feed/{id}": { @@ -183,32 +264,46 @@ "in": "header", "description": "UID of user making client request", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "templateOnly", "required": false, "in": "query", "description": "Pass \"true\" to opt out of server-side rendering. Default: false (server renders and returns content).", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "" } }, + "responses": { + "200": { + "description": "" + } + }, "summary": "Get a feed item by ID", - "tags": ["Notification Feed"] + "tags": [ + "Notification Feed" + ] }, "delete": { "operationId": "NotificationFeedController_delete", @@ -218,25 +313,37 @@ "in": "header", "description": "UID of user making client request", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "204": { "description": "" } }, + "responses": { + "204": { + "description": "" + } + }, "summary": "Soft-delete a feed item (admin only)", - "tags": ["Notification Feed"] + "tags": [ + "Notification Feed" + ] } }, "/notification-feed/{id}/read": { @@ -248,25 +355,37 @@ "in": "header", "description": "UID of user making client request", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "" } }, + "responses": { + "200": { + "description": "" + } + }, "summary": "Mark a feed item as read (idempotent)", - "tags": ["Notification Feed"] + "tags": [ + "Notification Feed" + ] } }, "/notification-feed/{id}/delivered": { @@ -278,25 +397,37 @@ "in": "header", "description": "UID of user making client request", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "" } }, + "responses": { + "200": { + "description": "" + } + }, "summary": "Mark a feed item as delivered (idempotent)", - "tags": ["Notification Feed"] + "tags": [ + "Notification Feed" + ] } }, "/notification-feed/{id}/engagement": { @@ -308,93 +439,47 @@ "in": "header", "description": "UID of user making client request", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/EngagementEventDto" } + "schema": { + "type": "string" } } - }, - "responses": { "200": { "description": "" } }, - "summary": "Report an interacted engagement event with optional topic discriminator", - "tags": ["Notification Feed"] - } - }, - "/settings": { - "get": { - "operationId": "SettingsController_get", - "parameters": [ - { - "name": "x-internal-api-key", - "in": "header", - "description": "Internal API key for platform-level settings access", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "appId", - "required": true, - "in": "query", - "description": "Tenant application ID", - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Settings retrieved" }, - "404": { "description": "Not found or unauthorized" } - }, - "summary": "Get tenant settings (internal only)", - "tags": ["Settings"] - }, - "put": { - "operationId": "SettingsController_update", - "parameters": [ - { - "name": "x-internal-api-key", - "in": "header", - "description": "Internal API key for platform-level settings access", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "appId", - "required": true, - "in": "query", - "description": "Tenant application ID", - "schema": { "type": "string" } - } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/UpdateSettingsDto" } + "schema": { + "$ref": "#/components/schemas/EngagementEventDto" + } } } }, "responses": { - "200": { "description": "Settings updated" }, - "400": { "description": "Invalid input" } + "200": { + "description": "" + } }, - "summary": "Update tenant settings (internal only)", - "tags": ["Settings"] + "summary": "Report an interacted engagement event with optional topic discriminator", + "tags": [ + "Notification Feed" + ] } }, "/templates/categories": { @@ -406,24 +491,36 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CreateCategoryDto" } + "schema": { + "$ref": "#/components/schemas/CreateCategoryDto" + } } } }, "responses": { - "201": { "description": "Category created" }, - "400": { "description": "Invalid input" }, - "409": { "description": "Duplicate category name" } + "201": { + "description": "Category created" + }, + "400": { + "description": "Invalid input" + }, + "409": { + "description": "Duplicate category name" + } }, "summary": "Create a new template category (admin only)", - "tags": ["Template Categories"] + "tags": [ + "Template Categories" + ] }, "get": { "operationId": "CategoriesController_findAll", @@ -433,35 +530,51 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "updatedAt", "required": false, "in": "query", "description": "Cursor: updatedAt unix timestamp of last item", - "schema": { "type": "number" } + "schema": { + "type": "number" + } }, { "name": "id", "required": false, "in": "query", "description": "Cursor: id of last item", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "affix", "required": false, "in": "query", "description": "Cursor direction", - "schema": { "type": "string", "enum": ["append", "prepend"] } + "schema": { + "type": "string", + "enum": [ + "append", + "prepend" + ] + } }, { "name": "limit", "required": false, "in": "query", "description": "Items per page", - "schema": { "minimum": 1, "maximum": 100, "type": "number" } + "schema": { + "minimum": 1, + "maximum": 100, + "type": "number" + } }, { "name": "sort", @@ -470,7 +583,11 @@ "description": "Sort field", "schema": { "type": "string", - "enum": ["name", "createdAt", "updatedAt"] + "enum": [ + "name", + "createdAt", + "updatedAt" + ] } }, { @@ -478,12 +595,24 @@ "required": false, "in": "query", "description": "Sort direction", - "schema": { "type": "string", "enum": ["asc", "desc"] } + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } } ], - "responses": { "200": { "description": "Categories list" } }, + "responses": { + "200": { + "description": "Categories list" + } + }, "summary": "List all template categories", - "tags": ["Template Categories"] + "tags": [ + "Template Categories" + ] } }, "/templates/categories/{id}": { @@ -495,21 +624,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Category found" }, - "404": { "description": "Category not found" } + "200": { + "description": "Category found" + }, + "404": { + "description": "Category not found" + } }, "summary": "Get a template category by ID", - "tags": ["Template Categories"] + "tags": [ + "Template Categories" + ] }, "put": { "operationId": "CategoriesController_update", @@ -519,30 +658,44 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/UpdateCategoryDto" } + "schema": { + "$ref": "#/components/schemas/UpdateCategoryDto" + } } } }, "responses": { - "200": { "description": "Category updated" }, - "404": { "description": "Category not found" }, - "409": { "description": "Duplicate category name" } + "200": { + "description": "Category updated" + }, + "404": { + "description": "Category not found" + }, + "409": { + "description": "Duplicate category name" + } }, "summary": "Update a template category (admin only)", - "tags": ["Template Categories"] + "tags": [ + "Template Categories" + ] }, "delete": { "operationId": "CategoriesController_delete", @@ -552,21 +705,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Category deleted" }, - "404": { "description": "Category not found" } + "200": { + "description": "Category deleted" + }, + "404": { + "description": "Category not found" + } }, "summary": "Delete a template category (admin only)", - "tags": ["Template Categories"] + "tags": [ + "Template Categories" + ] } }, "/templates": { @@ -578,24 +741,36 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CreateTemplateDto" } + "schema": { + "$ref": "#/components/schemas/CreateTemplateDto" + } } } }, "responses": { - "201": { "description": "Template created" }, - "400": { "description": "Invalid input or variable schema" }, - "409": { "description": "Duplicate channel type" } + "201": { + "description": "Template created" + }, + "400": { + "description": "Invalid input or variable schema" + }, + "409": { + "description": "Duplicate channel type" + } }, "summary": "Create a new template (admin only)", - "tags": ["Templates"] + "tags": [ + "Templates" + ] }, "get": { "operationId": "TemplatesController_findAll", @@ -605,35 +780,51 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "updatedAt", "required": false, "in": "query", "description": "Cursor: updatedAt unix timestamp of last item", - "schema": { "type": "number" } + "schema": { + "type": "number" + } }, { "name": "id", "required": false, "in": "query", "description": "Cursor: id of last item", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "affix", "required": false, "in": "query", "description": "Cursor direction", - "schema": { "type": "string", "enum": ["append", "prepend"] } + "schema": { + "type": "string", + "enum": [ + "append", + "prepend" + ] + } }, { "name": "limit", "required": false, "in": "query", "description": "Items per page", - "schema": { "minimum": 1, "maximum": 100, "type": "number" } + "schema": { + "minimum": 1, + "maximum": 100, + "type": "number" + } }, { "name": "sort", @@ -642,7 +833,11 @@ "description": "Sort field", "schema": { "type": "string", - "enum": ["updatedAt", "createdAt", "name"] + "enum": [ + "updatedAt", + "createdAt", + "name" + ] } }, { @@ -650,47 +845,73 @@ "required": false, "in": "query", "description": "Sort direction", - "schema": { "type": "string", "enum": ["asc", "desc"] } + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } }, { "name": "status", "required": false, "in": "query", "description": "Filter by template status", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "search", "required": false, "in": "query", "description": "Search by name or templateId", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "tags", "required": false, "in": "query", "description": "Comma-separated tags to filter by", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "tagMatch", "required": false, "in": "query", "description": "Tag matching strategy: 'any' (OR) or 'all' (AND)", - "schema": { "type": "string", "enum": ["any", "all"] } + "schema": { + "type": "string", + "enum": [ + "any", + "all" + ] + } }, { "name": "templateCategory", "required": false, "in": "query", "description": "Filter by templateCategory name", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "Templates list" } }, + "responses": { + "200": { + "description": "Templates list" + } + }, "summary": "List all templates", - "tags": ["Templates"] + "tags": [ + "Templates" + ] } }, "/templates/{id}": { @@ -702,21 +923,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Template found" }, - "404": { "description": "Template not found" } + "200": { + "description": "Template found" + }, + "404": { + "description": "Template not found" + } }, "summary": "Get a template by ID", - "tags": ["Templates"] + "tags": [ + "Templates" + ] }, "put": { "operationId": "TemplatesController_update", @@ -726,30 +957,44 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/UpdateTemplateDto" } + "schema": { + "$ref": "#/components/schemas/UpdateTemplateDto" + } } } }, "responses": { - "200": { "description": "Template updated" }, - "400": { "description": "Invalid variable schema" }, - "404": { "description": "Template not found" } + "200": { + "description": "Template updated" + }, + "400": { + "description": "Invalid variable schema" + }, + "404": { + "description": "Template not found" + } }, "summary": "Update template metadata (admin only)", - "tags": ["Templates"] + "tags": [ + "Templates" + ] }, "delete": { "operationId": "TemplatesController_archive", @@ -759,21 +1004,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Template archived" }, - "404": { "description": "Template not found" } + "200": { + "description": "Template archived" + }, + "404": { + "description": "Template not found" + } }, "summary": "Archive a template (admin only)", - "tags": ["Templates"] + "tags": [ + "Templates" + ] } }, "/templates/{id}/channels/{channelType}": { @@ -785,19 +1040,25 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "channelType", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { @@ -811,11 +1072,17 @@ } }, "responses": { - "200": { "description": "Channel content updated" }, - "404": { "description": "Template not found" } + "200": { + "description": "Channel content updated" + }, + "404": { + "description": "Template not found" + } }, "summary": "Update channel content for a template (admin only)", - "tags": ["Templates"] + "tags": [ + "Templates" + ] } }, "/templates/{id}/versions": { @@ -827,21 +1094,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "201": { "description": "Version created" }, - "404": { "description": "Template not found" } + "201": { + "description": "Version created" + }, + "404": { + "description": "Template not found" + } }, "summary": "Create a new template version (admin only)", - "tags": ["Templates"] + "tags": [ + "Templates" + ] } }, "/channels": { @@ -853,25 +1130,39 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CreateChannelDto" } + "schema": { + "$ref": "#/components/schemas/CreateChannelDto" + } } } }, "responses": { - "201": { "description": "Channel created" }, - "400": { "description": "Called with onbehalfof" }, - "403": { "description": "Channel type restricted" }, - "409": { "description": "Duplicate channelId or limit reached" } + "201": { + "description": "Channel created" + }, + "400": { + "description": "Called with onbehalfof" + }, + "403": { + "description": "Channel type restricted" + }, + "409": { + "description": "Duplicate channelId or limit reached" + } }, "summary": "Create a new channel (admin only)", - "tags": ["Channels"] + "tags": [ + "Channels" + ] }, "get": { "operationId": "ChannelsController_list", @@ -881,35 +1172,51 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "updatedAt", "required": false, "in": "query", "description": "Cursor: updatedAt unix timestamp of last item", - "schema": { "type": "number" } + "schema": { + "type": "number" + } }, { "name": "id", "required": false, "in": "query", "description": "Cursor: id of last item", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "affix", "required": false, "in": "query", "description": "Cursor direction", - "schema": { "type": "string", "enum": ["append", "prepend"] } + "schema": { + "type": "string", + "enum": [ + "append", + "prepend" + ] + } }, { "name": "limit", "required": false, "in": "query", "description": "Items per page", - "schema": { "minimum": 1, "maximum": 100, "type": "number" } + "schema": { + "minimum": 1, + "maximum": 100, + "type": "number" + } }, { "name": "sort", @@ -918,7 +1225,11 @@ "description": "Sort field", "schema": { "type": "string", - "enum": ["channelType", "createdAt", "updatedAt"] + "enum": [ + "channelType", + "createdAt", + "updatedAt" + ] } }, { @@ -926,19 +1237,33 @@ "required": false, "in": "query", "description": "Sort direction", - "schema": { "type": "string", "enum": ["asc", "desc"] } + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } }, { "name": "search", "required": false, "in": "query", "description": "Search by name or channelId", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "Paginated channel list" } }, + "responses": { + "200": { + "description": "Paginated channel list" + } + }, "summary": "List channels for the app", - "tags": ["Channels"] + "tags": [ + "Channels" + ] } }, "/channels/availability": { @@ -950,15 +1275,23 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Channel type availability list" }, - "400": { "description": "Called with onbehalfof" } + "200": { + "description": "Channel type availability list" + }, + "400": { + "description": "Called with onbehalfof" + } }, "summary": "Get channel type availability (admin only)", - "tags": ["Channels"] + "tags": [ + "Channels" + ] } }, "/channels/{id}": { @@ -970,21 +1303,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Channel found" }, - "404": { "description": "Channel not found" } + "200": { + "description": "Channel found" + }, + "404": { + "description": "Channel not found" + } }, "summary": "Get a channel by ID", - "tags": ["Channels"] + "tags": [ + "Channels" + ] }, "put": { "operationId": "ChannelsController_update", @@ -994,30 +1337,44 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/UpdateChannelDto" } + "schema": { + "$ref": "#/components/schemas/UpdateChannelDto" + } } } }, "responses": { - "200": { "description": "Channel updated" }, - "400": { "description": "Called with onbehalfof" }, - "404": { "description": "Channel not found" } + "200": { + "description": "Channel updated" + }, + "400": { + "description": "Called with onbehalfof" + }, + "404": { + "description": "Channel not found" + } }, "summary": "Update a channel (admin only)", - "tags": ["Channels"] + "tags": [ + "Channels" + ] } }, "/push-notifications": { @@ -1029,25 +1386,37 @@ "in": "header", "description": "UID of the user", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "limit", "required": true, "in": "query", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "Push notifications list" } }, + "responses": { + "200": { + "description": "Push notifications list" + } + }, "summary": "List push notifications for a user", - "tags": ["Push Notifications"] + "tags": [ + "Push Notifications" + ] } }, "/push-notifications/{id}": { @@ -1059,25 +1428,37 @@ "in": "header", "description": "UID of the user", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "Push notification details" } }, + "responses": { + "200": { + "description": "Push notification details" + } + }, "summary": "Get a push notification by ID", - "tags": ["Push Notifications"] + "tags": [ + "Push Notifications" + ] } }, "/push-notifications/{id}/delivered": { @@ -1089,25 +1470,37 @@ "in": "header", "description": "UID of the user", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "Marked as delivered" } }, + "responses": { + "200": { + "description": "Marked as delivered" + } + }, "summary": "Mark push notification as delivered", - "tags": ["Push Notifications"] + "tags": [ + "Push Notifications" + ] } }, "/push-notifications/{id}/clicked": { @@ -1119,25 +1512,37 @@ "in": "header", "description": "UID of the user", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "Marked as clicked" } }, + "responses": { + "200": { + "description": "Marked as clicked" + } + }, "summary": "Mark push notification as clicked", - "tags": ["Push Notifications"] + "tags": [ + "Push Notifications" + ] } }, "/push-notifications/{id}/engagement": { @@ -1149,20 +1554,26 @@ "in": "header", "description": "UID of the user", "required": false, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "appid", "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { @@ -1175,9 +1586,15 @@ } } }, - "responses": { "200": { "description": "Engagement recorded" } }, + "responses": { + "200": { + "description": "Engagement recorded" + } + }, "summary": "Report push notification engagement event (interacted with optional topic)", - "tags": ["Push Notifications"] + "tags": [ + "Push Notifications" + ] } }, "/campaigns": { @@ -1189,20 +1606,30 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CreateCampaignDto" } + "schema": { + "$ref": "#/components/schemas/CreateCampaignDto" + } } } }, - "responses": { "201": { "description": "Campaign created" } }, + "responses": { + "201": { + "description": "Campaign created" + } + }, "summary": "Create a new campaign", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] }, "get": { "operationId": "CampaignsController_findAll", @@ -1212,35 +1639,51 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "updatedAt", "required": false, "in": "query", "description": "Cursor: updatedAt unix timestamp of last item", - "schema": { "type": "number" } + "schema": { + "type": "number" + } }, { "name": "id", "required": false, "in": "query", "description": "Cursor: id of last item", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "affix", "required": false, "in": "query", "description": "Cursor direction", - "schema": { "type": "string", "enum": ["append", "prepend"] } + "schema": { + "type": "string", + "enum": [ + "append", + "prepend" + ] + } }, { "name": "limit", "required": false, "in": "query", "description": "Items per page", - "schema": { "minimum": 1, "maximum": 100, "type": "number" } + "schema": { + "minimum": 1, + "maximum": 100, + "type": "number" + } }, { "name": "sort", @@ -1249,7 +1692,11 @@ "description": "Sort field", "schema": { "type": "string", - "enum": ["updatedAt", "createdAt", "name"] + "enum": [ + "updatedAt", + "createdAt", + "name" + ] } }, { @@ -1257,26 +1704,42 @@ "required": false, "in": "query", "description": "Sort direction", - "schema": { "type": "string", "enum": ["asc", "desc"] } + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } }, { "name": "status", "required": false, "in": "query", "description": "Filter by campaign status", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "search", "required": false, "in": "query", "description": "Search by name", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "Campaign list" } }, + "responses": { + "200": { + "description": "Campaign list" + } + }, "summary": "List campaigns (enriched with template + recipient summary)", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}": { @@ -1288,21 +1751,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Campaign found" }, - "404": { "description": "Campaign not found" } + "200": { + "description": "Campaign found" + }, + "404": { + "description": "Campaign not found" + } }, "summary": "Get a campaign by ID (enriched)", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] }, "put": { "operationId": "CampaignsController_update", @@ -1312,30 +1785,44 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/UpdateCampaignDto" } + "schema": { + "$ref": "#/components/schemas/UpdateCampaignDto" + } } } }, "responses": { - "200": { "description": "Campaign updated" }, - "400": { "description": "Campaign not in draft status" }, - "404": { "description": "Campaign not found" } + "200": { + "description": "Campaign updated" + }, + "400": { + "description": "Campaign not in draft status" + }, + "404": { + "description": "Campaign not found" + } }, "summary": "Update a campaign (draft only)", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] }, "delete": { "operationId": "CampaignsController_delete", @@ -1345,22 +1832,34 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Campaign deleted" }, - "400": { "description": "Campaign not in draft status" }, - "404": { "description": "Campaign not found" } + "200": { + "description": "Campaign deleted" + }, + "400": { + "description": "Campaign not in draft status" + }, + "404": { + "description": "Campaign not found" + } }, "summary": "Delete a campaign (draft only)", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}/recipients": { @@ -1372,29 +1871,41 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/AddRecipientsDto" } + "schema": { + "$ref": "#/components/schemas/AddRecipientsDto" + } } } }, "responses": { - "201": { "description": "Recipients added" }, - "400": { "description": "Campaign not in draft status" } + "201": { + "description": "Recipients added" + }, + "400": { + "description": "Campaign not in draft status" + } }, "summary": "Add recipients manually (user IDs)", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] }, "get": { "operationId": "CampaignsController_getRecipients", @@ -1404,55 +1915,85 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "updatedAt", "required": false, "in": "query", "description": "Cursor: updatedAt unix timestamp of last item", - "schema": { "type": "number" } + "schema": { + "type": "number" + } }, { "name": "id", "required": false, "in": "query", "description": "Cursor: id of last item", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "affix", "required": false, "in": "query", "description": "Cursor direction", - "schema": { "type": "string", "enum": ["append", "prepend"] } + "schema": { + "type": "string", + "enum": [ + "append", + "prepend" + ] + } }, { "name": "limit", "required": false, "in": "query", "description": "Items per page", - "schema": { "minimum": 1, "maximum": 100, "type": "number" } + "schema": { + "minimum": 1, + "maximum": 100, + "type": "number" + } }, { "name": "sort", "required": false, "in": "query", "description": "Sort field", - "schema": { "type": "string", "enum": ["updatedAt", "createdAt"] } + "schema": { + "type": "string", + "enum": [ + "updatedAt", + "createdAt" + ] + } }, { "name": "order", "required": false, "in": "query", "description": "Sort direction", - "schema": { "type": "string", "enum": ["asc", "desc"] } + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } }, { "name": "status", @@ -1461,13 +2002,24 @@ "description": "Filter by status", "schema": { "type": "string", - "enum": ["pending", "processing", "completed", "failed"] + "enum": [ + "pending", + "processing", + "completed", + "failed" + ] } } ], - "responses": { "200": { "description": "Recipient list" } }, + "responses": { + "200": { + "description": "Recipient list" + } + }, "summary": "List campaign recipients", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}/recipients/summary": { @@ -1479,18 +2031,28 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "Recipient summary" } }, + "responses": { + "200": { + "description": "Recipient summary" + } + }, "summary": "Get recipient status summary", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}/recipients/upload-url": { @@ -1502,21 +2064,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "201": { "description": "Presigned URL generated" }, - "400": { "description": "Campaign not in draft status" } + "201": { + "description": "Presigned URL generated" + }, + "400": { + "description": "Campaign not in draft status" + } }, "summary": "Get S3 presigned URL for CSV upload", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}/import-csv": { @@ -1528,30 +2100,44 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CsvUploadDto" } + "schema": { + "$ref": "#/components/schemas/CsvUploadDto" + } } } }, "responses": { - "202": { "description": "Import job created" }, - "400": { "description": "Campaign not in draft status" }, - "409": { "description": "Import already in progress" } + "202": { + "description": "Import job created" + }, + "400": { + "description": "Campaign not in draft status" + }, + "409": { + "description": "Import already in progress" + } }, "summary": "Start async CSV import for campaign recipients", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}/import-status": { @@ -1563,21 +2149,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Import status returned" }, - "404": { "description": "No active import found" } + "200": { + "description": "Import status returned" + }, + "404": { + "description": "No active import found" + } }, "summary": "Get current import job status for a campaign", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}/send": { @@ -1589,23 +2185,31 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Campaign send enqueued" }, + "200": { + "description": "Campaign send enqueued" + }, "400": { "description": "Campaign not in sendable status or no recipients" } }, "summary": "Trigger campaign send", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}/schedule": { @@ -1617,31 +2221,41 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/ScheduleCampaignDto" } + "schema": { + "$ref": "#/components/schemas/ScheduleCampaignDto" + } } } }, "responses": { - "200": { "description": "Campaign scheduled" }, + "200": { + "description": "Campaign scheduled" + }, "400": { "description": "Campaign not in draft status or invalid time" } }, "summary": "Schedule campaign send", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}/sequence-metrics": { @@ -1653,18 +2267,28 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], - "responses": { "200": { "description": "Sequence metrics returned" } }, + "responses": { + "200": { + "description": "Sequence metrics returned" + } + }, "summary": "Get per-step sequence delivery metrics for a campaign", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/campaigns/{id}/cancel": { @@ -1676,26 +2300,36 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "id", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "responses": { - "200": { "description": "Campaign cancelled" }, - "400": { "description": "Campaign not in cancellable status" } + "200": { + "description": "Campaign cancelled" + }, + "400": { + "description": "Campaign not in cancellable status" + } }, "summary": "Cancel a campaign", - "tags": ["Campaigns"] + "tags": [ + "Campaigns" + ] } }, "/notifications/messages": { "post": { - "description": "Sendbird-equivalent POST /notifications/messages. 1–10 receivers = realtime (synchronous, returns notificationId). 11–10,000 receivers = inline batch (asynchronous, returns batchId). Larger sends should use the Campaigns CSV flow.", + "description": "Sendbird-equivalent POST /notifications/messages. 1\u201310 receivers = realtime (synchronous, returns notificationId). 11\u201310,000 receivers = inline batch (asynchronous, returns batchId). Larger sends should use the Campaigns CSV flow.", "operationId": "NotificationSendController_send", "parameters": [ { @@ -1703,14 +2337,18 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/SendNotificationDto" } + "schema": { + "$ref": "#/components/schemas/SendNotificationDto" + } } } }, @@ -1722,39 +2360,13 @@ "description": "Template not found, template not approved, or recipient count exceeds limits" }, "403": { - "description": "Admin-only — onbehalfof header must not be present" + "description": "Admin-only \u2014 onbehalfof header must not be present" } }, "summary": "Send a notification using a template", - "tags": ["Notifications"] - } - }, - "/analytics/events": { - "post": { - "operationId": "AnalyticsController_recordEvent", - "parameters": [ - { - "name": "appid", - "in": "header", - "description": "Tenant application ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/RecordEventDto" } - } - } - }, - "responses": { - "201": { "description": "Event recorded" }, - "400": { "description": "Invalid input" } - }, - "summary": "Record an analytics event", - "tags": ["Analytics"] + "tags": [ + "Notifications" + ] } }, "/analytics/overview": { @@ -1766,7 +2378,9 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "dimension", @@ -1775,7 +2389,11 @@ "description": "Rollup dimension to query", "schema": { "type": "string", - "enum": ["campaign", "template", "channel"] + "enum": [ + "campaign", + "template", + "channel" + ] } }, { @@ -1783,40 +2401,61 @@ "required": false, "in": "query", "description": "Dimension ID (campaign ID, template ID, or channel type)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "period", "required": false, "in": "query", "description": "Rollup period", - "schema": { "type": "string", "enum": ["hourly", "daily"] } + "schema": { + "type": "string", + "enum": [ + "hourly", + "daily" + ] + } }, { "name": "startDate", "required": false, "in": "query", "description": "Start date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "endDate", "required": false, "in": "query", "description": "End date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "limit", "required": false, "in": "query", "description": "Maximum number of results", - "schema": { "default": 50, "type": "number" } + "schema": { + "default": 50, + "type": "number" + } } ], - "responses": { "200": { "description": "Analytics overview" } }, + "responses": { + "200": { + "description": "Analytics overview" + } + }, "summary": "Get aggregated analytics overview", - "tags": ["Analytics"] + "tags": [ + "Analytics" + ] } }, "/analytics/campaigns/{campaignId}": { @@ -1828,13 +2467,17 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "campaignId", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "dimension", @@ -1843,7 +2486,11 @@ "description": "Rollup dimension to query", "schema": { "type": "string", - "enum": ["campaign", "template", "channel"] + "enum": [ + "campaign", + "template", + "channel" + ] } }, { @@ -1851,40 +2498,61 @@ "required": false, "in": "query", "description": "Dimension ID (campaign ID, template ID, or channel type)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "period", "required": false, "in": "query", "description": "Rollup period", - "schema": { "type": "string", "enum": ["hourly", "daily"] } + "schema": { + "type": "string", + "enum": [ + "hourly", + "daily" + ] + } }, { "name": "startDate", "required": false, "in": "query", "description": "Start date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "endDate", "required": false, "in": "query", "description": "End date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "limit", "required": false, "in": "query", "description": "Maximum number of results", - "schema": { "default": 50, "type": "number" } + "schema": { + "default": 50, + "type": "number" + } } ], - "responses": { "200": { "description": "Campaign analytics" } }, + "responses": { + "200": { + "description": "Campaign analytics" + } + }, "summary": "Get campaign analytics drill-down", - "tags": ["Analytics"] + "tags": [ + "Analytics" + ] } }, "/analytics/templates/{templateId}": { @@ -1896,13 +2564,17 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "templateId", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "dimension", @@ -1911,7 +2583,11 @@ "description": "Rollup dimension to query", "schema": { "type": "string", - "enum": ["campaign", "template", "channel"] + "enum": [ + "campaign", + "template", + "channel" + ] } }, { @@ -1919,40 +2595,61 @@ "required": false, "in": "query", "description": "Dimension ID (campaign ID, template ID, or channel type)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "period", "required": false, "in": "query", "description": "Rollup period", - "schema": { "type": "string", "enum": ["hourly", "daily"] } + "schema": { + "type": "string", + "enum": [ + "hourly", + "daily" + ] + } }, { "name": "startDate", "required": false, "in": "query", "description": "Start date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "endDate", "required": false, "in": "query", "description": "End date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "limit", "required": false, "in": "query", "description": "Maximum number of results", - "schema": { "default": 50, "type": "number" } + "schema": { + "default": 50, + "type": "number" + } } ], - "responses": { "200": { "description": "Template analytics" } }, + "responses": { + "200": { + "description": "Template analytics" + } + }, "summary": "Get template analytics drill-down", - "tags": ["Analytics"] + "tags": [ + "Analytics" + ] } }, "/analytics/channels": { @@ -1964,7 +2661,9 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "dimension", @@ -1973,7 +2672,11 @@ "description": "Rollup dimension to query", "schema": { "type": "string", - "enum": ["campaign", "template", "channel"] + "enum": [ + "campaign", + "template", + "channel" + ] } }, { @@ -1981,40 +2684,61 @@ "required": false, "in": "query", "description": "Dimension ID (campaign ID, template ID, or channel type)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "period", "required": false, "in": "query", "description": "Rollup period", - "schema": { "type": "string", "enum": ["hourly", "daily"] } + "schema": { + "type": "string", + "enum": [ + "hourly", + "daily" + ] + } }, { "name": "startDate", "required": false, "in": "query", "description": "Start date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "endDate", "required": false, "in": "query", "description": "End date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "limit", "required": false, "in": "query", "description": "Maximum number of results", - "schema": { "default": 50, "type": "number" } + "schema": { + "default": 50, + "type": "number" + } } ], - "responses": { "200": { "description": "Channel breakdown" } }, + "responses": { + "200": { + "description": "Channel breakdown" + } + }, "summary": "Get channel breakdown analytics", - "tags": ["Analytics"] + "tags": [ + "Analytics" + ] } }, "/analytics/users/{userId}": { @@ -2026,13 +2750,17 @@ "in": "header", "description": "Tenant application ID", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "userId", "required": true, "in": "path", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "dimension", @@ -2041,7 +2769,11 @@ "description": "Rollup dimension to query", "schema": { "type": "string", - "enum": ["campaign", "template", "channel"] + "enum": [ + "campaign", + "template", + "channel" + ] } }, { @@ -2049,35 +2781,50 @@ "required": false, "in": "query", "description": "Dimension ID (campaign ID, template ID, or channel type)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "period", "required": false, "in": "query", "description": "Rollup period", - "schema": { "type": "string", "enum": ["hourly", "daily"] } + "schema": { + "type": "string", + "enum": [ + "hourly", + "daily" + ] + } }, { "name": "startDate", "required": false, "in": "query", "description": "Start date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "endDate", "required": false, "in": "query", "description": "End date filter (ISO 8601)", - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "limit", "required": false, "in": "query", "description": "Maximum number of results", - "schema": { "default": 50, "type": "number" } + "schema": { + "default": 50, + "type": "number" + } } ], "responses": { @@ -2086,35 +2833,9 @@ } }, "summary": "Get user-level engagement insights", - "tags": ["Analytics"] - } - }, - "/analytics/rollup": { - "post": { - "operationId": "AnalyticsController_triggerRollup", - "parameters": [ - { - "name": "appid", - "in": "header", - "description": "Tenant application ID", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { "200": { "description": "Rollup triggered" } }, - "summary": "Trigger rollup computation", - "tags": ["Analytics"] - } - }, - "/health": { - "get": { - "operationId": "HealthController_check", - "parameters": [], - "responses": { - "200": { "description": "All dependencies healthy." }, - "503": { "description": "At least one dependency is down." } - }, - "tags": ["Health"] + "tags": [ + "Analytics" + ] } } }, @@ -2125,29 +2846,66 @@ "contact": {} }, "tags": [ - { "name": "Notification Feed", "description": "Operations on the per-user in-app notification feed." }, - { "name": "Channels", "description": "Manage channel instances and per-type availability." }, - { "name": "Templates", "description": "Manage templates and their versions." }, - { "name": "Template Categories", "description": "Manage template categories for feed filtering." }, - { "name": "Campaigns", "description": "Create, schedule, and manage notification campaigns." }, - { "name": "Notifications", "description": "Send notifications directly via API." }, - { "name": "Push Notifications", "description": "Manage push notification delivery and engagement." }, - { "name": "Analytics", "description": "Delivery and engagement analytics." }, - { "name": "Settings", "description": "App-level campaign settings (internal)." }, - { "name": "Health", "description": "Service health check." } + { + "name": "Notification Feed", + "description": "Operations on the per-user in-app notification feed." + }, + { + "name": "Channels", + "description": "Manage channel instances and per-type availability." + }, + { + "name": "Templates", + "description": "Manage templates and their versions." + }, + { + "name": "Template Categories", + "description": "Manage template categories for feed filtering." + }, + { + "name": "Campaigns", + "description": "Create, schedule, and manage notification campaigns." + }, + { + "name": "Notifications", + "description": "Send notifications directly via API." + }, + { + "name": "Push Notifications", + "description": "Manage push notification delivery and engagement." + }, + { + "name": "Analytics", + "description": "Delivery and engagement analytics." + } ], "servers": [ { "url": "https://{appId}.api-{region}.cometchat.io/v3/campaigns", "variables": { - "appId": { "default": "appId", "description": "(Required) App ID" }, - "region": { "enum": ["us", "eu", "in"], "default": "us", "description": "Select Region" } + "appId": { + "default": "appId", + "description": "(Required) App ID" + }, + "region": { + "enum": [ + "us", + "eu", + "in" + ], + "default": "us", + "description": "Select Region" + } } } ], "components": { "securitySchemes": { - "appid": { "type": "apiKey", "in": "header", "name": "appid" }, + "appid": { + "type": "apiKey", + "in": "header", + "name": "appid" + }, "basic-auth": { "type": "http", "scheme": "basic", @@ -2160,7 +2918,7 @@ "properties": { "topic": { "type": "string", - "description": "Freeform topic discriminator for the interacted event. Well-known value: \"clicked\". Custom values: any alphanumeric + underscore/hyphen string ≤64 chars.", + "description": "Freeform topic discriminator for the interacted event. Well-known value: \"clicked\". Custom values: any alphanumeric + underscore/hyphen string \u226464 chars.", "example": "clicked", "maxLength": 64 } @@ -2177,19 +2935,32 @@ }, "defaultChannels": { "description": "Default channel types for new campaigns", - "example": ["push", "in_app"], + "example": [ + "push", + "in_app" + ], "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "realtimeFanout": { "type": "array", "description": "Realtime fanout policy for in_app FeedItem deliveries. Empty array = feed_only (default). Allowed values: 'websocket', 'push'. Consumers of after_feed_item_sent (WebSocket fanout svc, push wake-up svc) self-filter on this array.", - "example": ["websocket"], - "items": { "type": "string", "enum": ["websocket", "push"] } + "example": [ + "websocket" + ], + "items": { + "type": "string", + "enum": [ + "websocket", + "push" + ] + } }, "config": { "type": "object", - "description": "Additional service-level configuration. Supports: deliveryMechanisms: { websocketEnabled: boolean, pushEnabled: boolean } — controls default delivery mechanisms for announcements. Defaults: { websocketEnabled: true, pushEnabled: false }. Feed is always enabled.", + "description": "Additional service-level configuration. Supports: deliveryMechanisms: { websocketEnabled: boolean, pushEnabled: boolean } \u2014 controls default delivery mechanisms for announcements. Defaults: { websocketEnabled: true, pushEnabled: false }. Feed is always enabled.", "example": { "timezone": "UTC", "deliveryMechanisms": { @@ -2203,18 +2974,26 @@ "CreateCategoryDto": { "type": "object", "properties": { - "name": { "type": "string", "description": "Category name" }, + "name": { + "type": "string", + "description": "Category name" + }, "description": { "type": "string", "description": "Category description" } }, - "required": ["name"] + "required": [ + "name" + ] }, "UpdateCategoryDto": { "type": "object", "properties": { - "name": { "type": "string", "description": "Category name" }, + "name": { + "type": "string", + "description": "Category name" + }, "description": { "type": "string", "description": "Category description" @@ -2239,7 +3018,10 @@ "dataType": { "type": "string", "description": "Data type", - "enum": ["ui_template", "data_template"] + "enum": [ + "ui_template", + "data_template" + ] }, "categoryFilterEnabled": { "type": "boolean", @@ -2264,15 +3046,27 @@ "waitMinutes": { "type": "number", "description": "Wait time in minutes before advancing to next step", - "enum": [5, 10, 30, 60, 240, 1440] + "enum": [ + 5, + 10, + 30, + 60, + 240, + 1440 + ] } }, - "required": ["channelType"] + "required": [ + "channelType" + ] }, "CreateTemplateDto": { "type": "object", "properties": { - "name": { "type": "string", "description": "Template name" }, + "name": { + "type": "string", + "description": "Template name" + }, "templateId": { "type": "string", "description": "Human-readable slug (auto-generated from name if omitted)" @@ -2301,35 +3095,52 @@ "tags": { "description": "First-class tags for filtering/segmentation", "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "status": { "type": "string", "description": "Template status", - "enum": ["draft", "approved", "archived"] + "enum": [ + "draft", + "approved", + "archived" + ] }, "channels": { "description": "Channel configurations", "type": "array", - "items": { "$ref": "#/components/schemas/ChannelContentDto" } + "items": { + "$ref": "#/components/schemas/ChannelContentDto" + } }, "variableSchema": { "description": "Variable schema definitions", "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "config": { "type": "object", "description": "Additional configuration" } }, - "required": ["name", "channels"] + "required": [ + "name", + "channels" + ] }, "UpdateTemplateDto": { "type": "object", "properties": { - "name": { "type": "string" }, - "templateCategory": { "type": "string" }, + "name": { + "type": "string" + }, + "templateCategory": { + "type": "string" + }, "label": { "type": "string", "description": "Display label shown on notification" @@ -2341,11 +3152,22 @@ "tags": { "description": "First-class tags for filtering/segmentation", "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } + }, + "status": { + "type": "string" + }, + "variableSchema": { + "type": "array", + "items": { + "type": "string" + } }, - "status": { "type": "string" }, - "variableSchema": { "type": "array", "items": { "type": "string" } }, - "config": { "type": "object" } + "config": { + "type": "object" + } } }, "UpdateChannelContentDto": { @@ -2356,7 +3178,9 @@ "description": "Channel content payload" } }, - "required": ["content"] + "required": [ + "content" + ] }, "CreateChannelDto": { "type": "object", @@ -2369,7 +3193,14 @@ "type": { "type": "string", "description": "Channel type", - "enum": ["in_app", "push", "sms", "email", "whatsapp", "custom"], + "enum": [ + "in_app", + "push", + "sms", + "email", + "whatsapp", + "custom" + ], "example": "push" }, "channelId": { @@ -2385,15 +3216,24 @@ "metadata": { "type": "object", "description": "Channel-specific metadata", - "example": { "apiKey": "xxx", "senderId": "yyy" } + "example": { + "apiKey": "xxx", + "senderId": "yyy" + } } }, - "required": ["name", "type"] + "required": [ + "name", + "type" + ] }, "UpdateChannelDto": { "type": "object", "properties": { - "name": { "type": "string", "description": "Channel display name" }, + "name": { + "type": "string", + "description": "Channel display name" + }, "enabled": { "type": "boolean", "description": "Whether the channel is enabled" @@ -2401,7 +3241,10 @@ "metadata": { "type": "object", "description": "Channel-specific metadata", - "example": { "apiKey": "xxx", "senderId": "yyy" } + "example": { + "apiKey": "xxx", + "senderId": "yyy" + } } } }, @@ -2410,7 +3253,7 @@ "properties": { "topic": { "type": "string", - "description": "Freeform topic discriminator for the interacted event. Custom values: any alphanumeric + underscore/hyphen string ≤64 chars.", + "description": "Freeform topic discriminator for the interacted event. Custom values: any alphanumeric + underscore/hyphen string \u226464 chars.", "example": "dismissed", "maxLength": 64 } @@ -2419,7 +3262,10 @@ "CreateCampaignDto": { "type": "object", "properties": { - "name": { "type": "string", "description": "Campaign name" }, + "name": { + "type": "string", + "description": "Campaign name" + }, "templateId": { "type": "string", "description": "Template ID (CUID or templateId slug)" @@ -2431,20 +3277,29 @@ }, "variables": { "type": "object", - "description": "Campaign-level default variables — applied to every recipient as a fallback layer below per-user CSV values and above template variableSchema defaults. Example: `{ \"promoCode\": \"SUMMER25\", \"supportEmail\": \"help@acme.io\" }`.", - "example": { "promoCode": "SUMMER25" } + "description": "Campaign-level default variables \u2014 applied to every recipient as a fallback layer below per-user CSV values and above template variableSchema defaults. Example: `{ \"promoCode\": \"SUMMER25\", \"supportEmail\": \"help@acme.io\" }`.", + "example": { + "promoCode": "SUMMER25" + } }, "config": { "type": "object", "description": "Additional campaign configuration (free-form)" } }, - "required": ["name", "templateId", "templateVersion"] + "required": [ + "name", + "templateId", + "templateVersion" + ] }, "UpdateCampaignDto": { "type": "object", "properties": { - "name": { "type": "string", "description": "Campaign name" }, + "name": { + "type": "string", + "description": "Campaign name" + }, "config": { "type": "object", "description": "Additional campaign configuration" @@ -2459,15 +3314,23 @@ "minItems": 1, "maxItems": 10000, "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "userVariables": { "type": "object", "description": "Per-user variables, keyed by userId. Persisted on each `CampaignRecipient.variables` row at insert time. Renderer substitutes these into template content per recipient. Example: `{ \"user_42\": { \"name\": \"Ajay\" }, \"user_43\": { \"name\": \"Sam\" } }`.", - "example": { "user_42": { "name": "Ajay" } } + "example": { + "user_42": { + "name": "Ajay" + } + } } }, - "required": ["userIds"] + "required": [ + "userIds" + ] }, "CsvUploadDto": { "type": "object", @@ -2477,7 +3340,9 @@ "description": "S3 object key of the uploaded CSV file" } }, - "required": ["s3Key"] + "required": [ + "s3Key" + ] }, "ScheduleCampaignDto": { "type": "object", @@ -2488,7 +3353,9 @@ "example": 1714000000 } }, - "required": ["scheduledAt"] + "required": [ + "scheduledAt" + ] }, "SendNotificationDto": { "type": "object", @@ -2499,18 +3366,26 @@ "example": "order_update" }, "receivers": { - "description": "Array of target user IDs. 1–10 = realtime (synchronous, returns notificationId immediately). 11–10,000 = batch (asynchronous, returns batchId; processing happens via queue).", + "description": "Array of target user IDs. 1\u201310 = realtime (synchronous, returns notificationId immediately). 11\u201310,000 = batch (asynchronous, returns batchId; processing happens via queue).", "minItems": 1, "maxItems": 10000, "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "variables": { "type": "object", "description": "Per-user variables. Keyed by userId; values are { variableName: value } objects. Variables are applied to the template content at delivery time.", "example": { - "user_42": { "user_name": "John", "order_id": "12345" }, - "user_43": { "user_name": "Sarah", "order_id": "12346" } + "user_42": { + "user_name": "John", + "order_id": "12345" + }, + "user_43": { + "user_name": "Sarah", + "order_id": "12346" + } } }, "tag": { @@ -2518,7 +3393,10 @@ "description": "Optional analytics tag attached to the send (passes through to delivery records)." } }, - "required": ["templateId", "receivers"] + "required": [ + "templateId", + "receivers" + ] }, "RecordEventDto": { "type": "object", @@ -2554,16 +3432,31 @@ "eventType": { "type": "string", "description": "Engagement event type", - "enum": ["sent", "delivered", "clicked", "interacted", "failed"] + "enum": [ + "sent", + "delivered", + "clicked", + "interacted", + "failed" + ] }, "topic": { "type": "string", - "description": "Topic discriminator for interacted events (≤64 chars)" + "description": "Topic discriminator for interacted events (\u226464 chars)" } }, - "required": ["notificationId", "userId", "channelType", "eventType"] + "required": [ + "notificationId", + "userId", + "channelType", + "eventType" + ] } } }, - "security": [{ "basic-auth": [] }] -} + "security": [ + { + "basic-auth": [] + } + ] +} \ No newline at end of file From 1da3b48fc3a0c6ed7bd8cd7cd599b0d20903ec50 Mon Sep 17 00:00:00 2001 From: Ashfaq Ali Date: Wed, 27 May 2026 15:51:12 +0530 Subject: [PATCH 17/45] update doc --- sdk/android/v5/campaigns.mdx | 26 ++++++++++++++++++++++++- ui-kit/android/v6/campaigns.mdx | 14 +++++++++++++ ui-kit/android/v6/notification-feed.mdx | 6 +++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/sdk/android/v5/campaigns.mdx b/sdk/android/v5/campaigns.mdx index ef021183f..1353c3166 100644 --- a/sdk/android/v5/campaigns.mdx +++ b/sdk/android/v5/campaigns.mdx @@ -510,7 +510,31 @@ CometChat.markPushNotificationClicked(pushNotification, object : CometChat.Callb ## Rendering Cards -The `content` field of each `NotificationFeedItem` is a Card Schema JSON object. To render it natively, use the CometChat Cards library: +The `content` field of each `NotificationFeedItem` is a Card Schema JSON object. To render it natively, use the CometChat Cards library. + +### Add the Cards Dependency + +Add the Cloudsmith repository and the cards library to your project: + +```groovy +// settings.gradle or project-level build.gradle +repositories { + maven { url "https://dl.cloudsmith.io/public/cometchat/cometchat/maven/" } +} +``` + +```groovy +// app/build.gradle +dependencies { + implementation "com.cometchat:cards-android:1.0.0-beta.1" +} +``` + + +Requires `minSdk 24`, Kotlin, and internet permission in your AndroidManifest.xml. + + +### Render a Card from a Feed Item diff --git a/ui-kit/android/v6/campaigns.mdx b/ui-kit/android/v6/campaigns.mdx index 627de71d2..66d71665a 100644 --- a/ui-kit/android/v6/campaigns.mdx +++ b/ui-kit/android/v6/campaigns.mdx @@ -67,6 +67,20 @@ The schema supports **20 element types** (text, image, icon, avatar, badge, divi --- +## How Cards Work in the UI Kit + +The `CometChatNotificationFeed` component uses the **CometChat Cards** library internally to render each notification. Here's what happens under the hood: + +1. The component fetches `NotificationFeedItem` objects from the SDK +2. For each item, it extracts the `content` field (Card Schema JSON) +3. It passes the JSON to `CometChatCardComposable` (Compose) or `CometChatCardView` (XML) from the Cards library +4. The Cards renderer produces native UI — text, images, buttons, layouts — directly from the JSON +5. When users tap buttons/links inside a card, the action is emitted back to the component which handles navigation (open URL, navigate to chat, etc.) + +You don't need to interact with the Cards library directly when using `CometChatNotificationFeed` — it's all wired up. But if you want to render cards outside the feed (e.g., a standalone card in a dialog), you can use the Cards library directly. See the [SDK Campaigns documentation](/sdk/android/v5/campaigns#rendering-cards) for standalone usage. + +--- + ## Handling Push Notifications for Campaigns When a campaign push notification arrives via FCM, you should: diff --git a/ui-kit/android/v6/notification-feed.mdx b/ui-kit/android/v6/notification-feed.mdx index 641430bfa..54f962efc 100644 --- a/ui-kit/android/v6/notification-feed.mdx +++ b/ui-kit/android/v6/notification-feed.mdx @@ -1,5 +1,5 @@ --- -title: "CometChatNotificationFeed" +title: "Notification Feed" description: "Full-screen notification feed component with category filtering, card rendering, real-time updates, and engagement reporting." --- @@ -41,7 +41,7 @@ fun NotificationsScreen(onBack: () -> Unit) { ```kotlin val feed = findViewById(R.id.notificationFeed) -feed.init(this) // ViewModelStoreOwner (Activity or Fragment) +feed.init(this) // Pass Activity or Fragment — needed to create the ViewModel feed.onItemClick = { item -> /* navigate */ } feed.onBackPress = { finish() } ``` @@ -69,7 +69,7 @@ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val feed = CometChatNotificationFeed(this) setContentView(feed) - feed.init(this) + feed.init(this) // Required — binds the ViewModel to this Activity's lifecycle } ``` From c8b9145c9a8d1bea74d4772dc06e4b361e52123f Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 15:54:42 +0530 Subject: [PATCH 18/45] docs(campaigns): Clarify recent campaigns table includes all statuses --- campaigns/analytics.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/campaigns/analytics.mdx b/campaigns/analytics.mdx index 517b74dd2..fcdb86ea9 100644 --- a/campaigns/analytics.mdx +++ b/campaigns/analytics.mdx @@ -24,7 +24,7 @@ A visual breakdown showing the progression from sent to engaged: ## Recent Campaigns -A table showing your most recent campaigns (excluding drafts) with the following columns: +A table showing your most recent campaigns with the following columns: | Column | Description | |--------|-------------| From eec1a12f708ab80dbbc21f11f2093f6035525461 Mon Sep 17 00:00:00 2001 From: Ashfaq Ali Date: Wed, 27 May 2026 15:55:38 +0530 Subject: [PATCH 19/45] update version --- sdk/android/v5/campaigns.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/android/v5/campaigns.mdx b/sdk/android/v5/campaigns.mdx index 1353c3166..5028fc574 100644 --- a/sdk/android/v5/campaigns.mdx +++ b/sdk/android/v5/campaigns.mdx @@ -526,7 +526,7 @@ repositories { ```groovy // app/build.gradle dependencies { - implementation "com.cometchat:cards-android:1.0.0-beta.1" + implementation "com.cometchat:cards-android:1.0.0" } ``` From 5eacd9c4fcdfef21d44c8f5a567f600086338a22 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 15:58:32 +0530 Subject: [PATCH 20/45] docs(campaigns): Simplify Recent Campaigns section --- campaigns/analytics.mdx | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/campaigns/analytics.mdx b/campaigns/analytics.mdx index fcdb86ea9..e723d6b2d 100644 --- a/campaigns/analytics.mdx +++ b/campaigns/analytics.mdx @@ -24,19 +24,7 @@ A visual breakdown showing the progression from sent to engaged: ## Recent Campaigns -A table showing your most recent campaigns with the following columns: - -| Column | Description | -|--------|-------------| -| Campaign name | Name of the campaign | -| Status | Current campaign status | -| Sent | Number of notifications sent | -| Delivered | Number confirmed delivered | -| Read | Number marked as read | -| Engagement | Number of interactions | -| Last activity | Timestamp of last activity | - -Clicking a campaign row opens the campaign drill-down page with a detailed breakdown of that campaign's metrics. +A table showing your most recent campaigns. Clicking a campaign row opens the campaign details page. ## Campaign Details From 444200d376c90cf5e663cb54f5c75dfd6521ab99 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Wed, 27 May 2026 19:12:36 +0530 Subject: [PATCH 21/45] docs: add CometChat Campaigns documentation for React Native MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add sdk/react-native/campaigns.mdx — SDK API reference for notification feed, categories, engagement, push tracking, and card rendering - Add ui-kit/react-native/campaigns.mdx — Feature overview with architecture flow and card rendering explanation - Add ui-kit/react-native/notification-feed.mdx — CometChatNotificationFeed component page with props, callbacks, styling, and patterns - Update docs.json navigation: campaigns in Features, notification-feed in Components, campaigns in SDK --- docs.json | 7 +- sdk/react-native/campaigns.mdx | 516 ++++++++++++++++++++ ui-kit/react-native/campaigns.mdx | 150 ++++++ ui-kit/react-native/notification-feed.mdx | 544 ++++++++++++++++++++++ 4 files changed, 1215 insertions(+), 2 deletions(-) create mode 100644 sdk/react-native/campaigns.mdx create mode 100644 ui-kit/react-native/campaigns.mdx create mode 100644 ui-kit/react-native/notification-feed.mdx diff --git a/docs.json b/docs.json index 172825868..96b5e88ac 100644 --- a/docs.json +++ b/docs.json @@ -956,7 +956,8 @@ "ui-kit/react-native/extensions" ] }, - "ui-kit/react-native/call-features" + "ui-kit/react-native/call-features", + "ui-kit/react-native/campaigns" ] }, { @@ -987,7 +988,8 @@ "ui-kit/react-native/outgoing-call", "ui-kit/react-native/call-buttons", "ui-kit/react-native/call-logs", - "ui-kit/react-native/ai-assistant-chat-history" + "ui-kit/react-native/ai-assistant-chat-history", + "ui-kit/react-native/notification-feed" ] }, { @@ -3375,6 +3377,7 @@ }, "sdk/react-native/ai-moderation", "sdk/react-native/ai-agents", + "sdk/react-native/campaigns", { "group": "Resources", "pages": [ diff --git a/sdk/react-native/campaigns.mdx b/sdk/react-native/campaigns.mdx new file mode 100644 index 000000000..8e3d5d4f9 --- /dev/null +++ b/sdk/react-native/campaigns.mdx @@ -0,0 +1,516 @@ +--- +title: "Campaigns" +description: "Fetch notification feed items, listen for real-time delivery, mark items as read/delivered, report engagement, and track push notifications using the CometChat React Native SDK." +--- + +CometChat Campaigns lets you deliver targeted, rich notifications to users via an in-app notification feed. Each notification is a **Card Schema JSON** — a structured layout rendered natively by the CometChat Cards library. + +The SDK provides APIs to fetch feed items, listen for real-time delivery, mark items as read/delivered, report engagement, and retrieve unread counts. + +--- + +## Key Concepts + +| Concept | Description | +| --- | --- | +| **NotificationFeedItem** | A single notification in the feed. Contains Card Schema JSON in its `content` field, a `category` for filtering, timestamps, and metadata. | +| **NotificationCategory** | A category label used for filter chips (e.g., "Promotions", "Updates"). | +| **Card Schema JSON** | The fully rendered card layout (images, text, buttons) inside `NotificationFeedItem.getContent()`. Passed directly to the CometChat Cards renderer. | +| **PushNotification** | Represents a campaign push notification payload received via FCM/APNs. | + +--- + +## How Cards Render in the Notification Feed + +Each `NotificationFeedItem` has a `content` field containing an object — this is the **Card Schema JSON**. This JSON is passed directly to the **CometChat Cards** renderer library (`@cometchat/cards-react-native`). + +The rendering flow: + +1. Fetch feed items via `NotificationFeedRequestBuilder` +2. For each item, extract `item.getContent()` — this is the Card Schema JSON +3. Convert to string: `JSON.stringify(item.getContent())` +4. Pass to the Cards renderer (`CometChatCardView`) +5. The renderer produces a native React Native view from the JSON + +### Card Schema JSON Structure + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://...", "height": 200 }, + { "type": "text", "id": "txt_1", "content": "Flash Sale!", "variant": "heading2" }, + { "type": "button", "id": "btn_1", "label": "Shop Now", "action": { "type": "openUrl", "url": "https://..." } } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale! Shop Now: https://..." +} +``` + +The `body` array contains elements (text, image, button, row, column, etc.) rendered top-to-bottom. Interactive elements like buttons emit actions via a callback — the consumer handles navigation, deep links, or API calls. + +--- + +## Retrieve Notification Feed Items + +Use `NotificationFeedRequestBuilder` to fetch a paginated list of feed items. Uses cursor-based pagination internally. + +### Build the Request + + + +```javascript +const request = new CometChat.NotificationFeedRequestBuilder() + .setLimit(20) + .build(); +``` + + +```typescript +const request: CometChat.NotificationFeedRequest = new CometChat.NotificationFeedRequestBuilder() + .setLimit(20) + .build(); +``` + + + +### Builder Parameters + +| Method | Type | Default | Description | +| --- | --- | --- | --- | +| `setLimit(limit)` | number | 20 | Items per page (max 100) | +| `setReadState(state)` | FeedReadState | `FeedReadState.ALL` | Filter by `READ`, `UNREAD`, or `ALL` | +| `setCategory(category)` | string | null | Filter by category label | +| `setChannelId(channelId)` | string | null | Filter by channel | +| `setTags(tags)` | string[] | null | Filter by tags | +| `setDateFrom(date)` | string | null | ISO 8601 date — items sent on or after | +| `setDateTo(date)` | string | null | ISO 8601 date — items sent on or before | + +### Fetch Items + + + +```javascript +request.fetchNext().then( + (items) => { + for (const item of items) { + const cardJson = JSON.stringify(item.getContent()); + // Pass cardJson to CometChatCardView + } + }, + (error) => { + console.error("Feed fetch error:", error.message); + } +); +``` + + +```typescript +request.fetchNext().then( + (items: CometChat.NotificationFeedItem[]) => { + for (const item of items) { + const cardJson: string = JSON.stringify(item.getContent()); + // Pass cardJson to CometChatCardView + } + }, + (error: CometChat.CometChatException) => { + console.error("Feed fetch error:", error.message); + } +); +``` + + + +Call `fetchNext()` repeatedly for pagination. When the server has no more items, subsequent calls return an empty array. + +### NotificationFeedItem Fields + +| Field | Type | Description | +| --- | --- | --- | +| `getId()` | string | Unique item identifier | +| `getCategory()` | string | Notification category (e.g., "promotions") | +| `getContent()` | object | Card Schema JSON — pass to CometChat Cards renderer | +| `getReadAt()` | number \| null | Unix timestamp when read, or null if unread | +| `getDeliveredAt()` | number \| null | Unix timestamp when delivered, or null | +| `getSentAt()` | number | Unix timestamp when sent | +| `getMetadata()` | `Record` | Custom key-value metadata | +| `getTags()` | string[] | Tags for filtering | +| `getSender()` | string | Sender identifier | +| `getReceiver()` | string | Receiver identifier | +| `getReceiverType()` | string | Receiver type | +| `getIsRead()` | boolean | Whether the item has been read | + +--- + +## Retrieve Notification Categories + +Use `NotificationCategoriesRequestBuilder` to fetch available categories for filter chips. + + + +```javascript +const categoriesRequest = new CometChat.NotificationCategoriesRequestBuilder() + .setLimit(50) + .build(); + +categoriesRequest.fetchNext().then( + (categories) => { + for (const category of categories) { + console.log("Category:", category.getLabel()); + } + }, + (error) => { + console.error("Categories fetch error:", error.message); + } +); +``` + + +```typescript +const categoriesRequest: CometChat.NotificationCategoriesRequest = + new CometChat.NotificationCategoriesRequestBuilder() + .setLimit(50) + .build(); + +categoriesRequest.fetchNext().then( + (categories: CometChat.NotificationCategory[]) => { + for (const category of categories) { + console.log("Category:", category.getLabel()); + } + }, + (error: CometChat.CometChatException) => { + console.error("Categories fetch error:", error.message); + } +); +``` + + + +### NotificationCategory Fields + +| Field | Type | Description | +| --- | --- | --- | +| `getId()` | string | Category identifier | +| `getLabel()` | string | Display name for filter UI | + +--- + +## Real-Time Notification Feed Listener + +Listen for new feed items arriving via WebSocket. This listener is independent from `MessageListener`, `GroupListener`, and `CallListener`. + + + +```javascript +CometChat.addNotificationFeedListener("feedListener", { + onFeedItemReceived: (feedItem) => { + console.log("New item:", feedItem.getId()); + const cardJson = JSON.stringify(feedItem.getContent()); + // Insert at top of feed and render + }, +}); +``` + + +```typescript +CometChat.addNotificationFeedListener("feedListener", { + onFeedItemReceived: (feedItem: CometChat.NotificationFeedItem) => { + console.log("New item:", feedItem.getId()); + const cardJson: string = JSON.stringify(feedItem.getContent()); + // Insert at top of feed and render + }, +}); +``` + + + +Remove the listener when no longer needed: + +```javascript +CometChat.removeNotificationFeedListener("feedListener"); +``` + +--- + +## Mark Feed Item as Read + +Mark a single item as read. Idempotent — safe to call multiple times. + + + +```javascript +CometChat.markFeedItemAsRead(feedItem).then( + () => { console.log("Marked as read"); }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.markFeedItemAsRead(feedItem).then( + () => { console.log("Marked as read"); }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +--- + +## Mark Feed Item as Delivered + +Mark a single item as delivered. Idempotent. + + + +```javascript +CometChat.markFeedItemAsDelivered(feedItem).then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.markFeedItemAsDelivered(feedItem).then( + () => { /* Success */ }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +### Mark Multiple Items as Delivered (Batch) + +```javascript +CometChat.markFeedItemsAsDelivered(feedItems).then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + +--- + +## Report Engagement + +Report that a user engaged with a feed item (e.g., viewed, clicked, interacted). Idempotent. + + + +```javascript +CometChat.reportFeedEngagement(feedItem, "clicked").then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.reportFeedEngagement(feedItem, "clicked").then( + () => { /* Success */ }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +The `interactionString` parameter is a free-form string describing the engagement (e.g., `"viewed"`, `"clicked"`, `"interacted"`). + +--- + +## Get Unread Count + +Fetch the total number of unread notification feed items. + + + +```javascript +CometChat.getNotificationFeedUnreadCount().then( + (result) => { console.log("Unread:", result.count); }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.getNotificationFeedUnreadCount().then( + (result: { count: number }) => { console.log("Unread:", result.count); }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +--- + +## Fetch Single Feed Item + +Fetch a specific item by ID — useful for deep linking from push notifications. + + + +```javascript +CometChat.getNotificationFeedItem("item-id-123").then( + (item) => { + const cardJson = JSON.stringify(item.getContent()); + // Render the card + }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.getNotificationFeedItem("item-id-123").then( + (item: CometChat.NotificationFeedItem) => { + const cardJson: string = JSON.stringify(item.getContent()); + // Render the card + }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +--- + +## Push Notification Tracking + +When a campaign push notification arrives via FCM/APNs, use these methods to report delivery and click engagement. + +### Mark Push Notification as Delivered + +Call this when the push notification is received: + + + +```javascript +const pushNotification = new CometChat.PushNotification(pushPayloadJson); + +CometChat.markPushNotificationDelivered(pushNotification).then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +const pushNotification = new CometChat.PushNotification(pushPayloadJson); + +CometChat.markPushNotificationDelivered(pushNotification).then( + () => { /* Success */ }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +### Mark Push Notification as Clicked + +Call this when the user taps the push notification: + + + +```javascript +CometChat.markPushNotificationClicked(pushNotification).then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.markPushNotificationClicked(pushNotification).then( + () => { /* Success */ }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +### PushNotification Fields + +| Field | Type | Description | +| --- | --- | --- | +| `getId()` | string | Announcement ID from the push payload | +| `getAnnouncementId()` | string | Same as id (for clarity) | +| `getCampaignId()` | string \| null | Campaign ID if from a campaign | +| `getSource()` | string | Always `"campaign"` for notification feed pushes | + +--- + +## FeedReadState Enum + +| Value | Description | +| --- | --- | +| `FeedReadState.READ` | Only read items | +| `FeedReadState.UNREAD` | Only unread items | +| `FeedReadState.ALL` | All items (default) | + +--- + +## Rendering Cards + +The `content` field of each `NotificationFeedItem` is a Card Schema JSON object. To render it natively, use the CometChat Cards library. + +### Add the Cards Dependency + +```bash +npm install @cometchat/cards-react-native +``` + + +If you're using `@cometchat/chat-uikit-react-native` v5.3.6+, the cards library is included automatically as a dependency. + + +### Render a Card from a Feed Item + +```tsx lines +import { CometChatCardView } from "@cometchat/cards-react-native"; + +function NotificationCard({ item }) { + const cardJson = JSON.stringify(item.getContent()); + + return ( + { + switch (event.type) { + case "openUrl": + // Open URL in browser + break; + case "chatWithUser": + // Navigate to chat with event.params.uid + break; + case "chatWithGroup": + // Navigate to group chat with event.params.guid + break; + } + }} + /> + ); +} +``` + + +The Cards library is a pure renderer — it does not execute actions. Your code must handle action callbacks (opening URLs, navigating to chats, making API calls, etc.). + + +--- + +## Supported Card Actions + +When a user taps a button or link inside a card, the action callback receives one of these action types: + +| Action Type | Parameters | Description | +| --- | --- | --- | +| `openUrl` | url, openIn | Open a URL in browser or webview | +| `chatWithUser` | uid | Navigate to 1:1 chat | +| `chatWithGroup` | guid | Navigate to group chat | +| `sendMessage` | text, receiverUid, receiverGuid | Send a text message | +| `copyToClipboard` | value | Copy text to clipboard | +| `downloadFile` | url, filename | Download a file | +| `initiateCall` | callType (audio/video), uid, guid | Start a call | +| `apiCall` | url, method, headers, body | Make an HTTP request | +| `customCallback` | callbackId, payload | App-specific logic | diff --git a/ui-kit/react-native/campaigns.mdx b/ui-kit/react-native/campaigns.mdx new file mode 100644 index 000000000..498ebfc5e --- /dev/null +++ b/ui-kit/react-native/campaigns.mdx @@ -0,0 +1,150 @@ +--- +title: "Campaigns" +description: "Deliver targeted, rich notifications to users via an in-app notification feed powered by the CometChat Cards renderer." +--- + +CometChat Campaigns enables you to send rich, interactive notifications to users through an in-app notification feed. Each notification is rendered as a native card using the **CometChat Cards** library — supporting images, text, buttons, layouts, and interactive actions. + +--- + +## Overview + +Campaigns delivers notifications as **Card Schema JSON** — a structured format that defines the visual layout of each notification card. The system consists of three layers: + +1. **CometChat Chat SDK** — Fetches feed items, manages read/delivered state, provides real-time listeners, handles push notification tracking +2. **CometChat Cards Library** — Renders Card Schema JSON into native React Native views +3. **CometChat UI Kit** — Provides the ready-to-use `CometChatNotificationFeed` component that wires everything together + +### Architecture Flow + +``` +Dashboard / API → Campaign Created → Push + WebSocket Delivery + ↓ + SDK: NotificationFeedRequestBuilder.fetchNext() + ↓ + NotificationFeedItem.getContent() → Card Schema JSON + ↓ + Cards Library: CometChatCardView + ↓ + Native Rendered Card (images, text, buttons, layouts) + ↓ + User taps button → onAction callback → Your code handles it +``` + +--- + +## How Cards Work + +Each `NotificationFeedItem` from the SDK contains a `content` field — an object holding the Card Schema JSON. This JSON is passed directly to the CometChat Cards renderer which produces a native view. + +The Cards library is a **pure renderer**: +- **Input**: Card Schema JSON string + theme mode + optional action callback +- **Output**: Native React Native view hierarchy + +It does not execute actions, manage message state, or call any SDK methods. When users tap interactive elements (buttons, links), the library emits the action to your callback. You decide what happens — open a URL, navigate to a chat, make an API call, etc. + +### Card Schema JSON Example + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://cdn.example.com/sale.jpg", "height": 180, "fit": "cover", "borderRadius": 8 }, + { "type": "text", "id": "txt_1", "content": "🎉 Flash Sale — 40% Off!", "variant": "heading2" }, + { "type": "text", "id": "txt_2", "content": "Limited time offer on all premium plans.", "variant": "body" }, + { "type": "button", "id": "btn_1", "label": "Claim Offer", "action": { "type": "openUrl", "url": "https://example.com/offer" }, "fullWidth": true } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale — 40% Off! Claim your offer: https://example.com/offer" +} +``` + +The schema supports **20 element types** (text, image, icon, avatar, badge, divider, spacer, chip, progressBar, codeBlock, markdown, row, column, grid, accordion, tabs, button, iconButton, link, table) and **9 action types** (openUrl, chatWithUser, chatWithGroup, sendMessage, copyToClipboard, downloadFile, initiateCall, apiCall, customCallback). + +--- + +## How Cards Work in the UI Kit + +The `CometChatNotificationFeed` component uses the **CometChat Cards** library internally to render each notification. Here's what happens under the hood: + +1. The component fetches `NotificationFeedItem` objects from the SDK +2. For each item, it extracts the `content` field (Card Schema JSON) +3. It passes the JSON to `CometChatCardView` from the Cards library +4. The Cards renderer produces native UI — text, images, buttons, layouts — directly from the JSON +5. When users tap buttons/links inside a card, the action is emitted back to the component which handles navigation (open URL, navigate to chat, etc.) + +You don't need to interact with the Cards library directly when using `CometChatNotificationFeed` — it's all wired up. But if you want to render cards outside the feed (e.g., a standalone card in a modal), you can use the Cards library directly. See the [SDK Campaigns documentation](/sdk/react-native/campaigns#rendering-cards) for standalone usage. + +--- + +## Handling Push Notifications for Campaigns + +When a campaign push notification arrives via FCM/APNs, you should: + +1. **Report delivery** — Call `CometChat.markPushNotificationDelivered()` when the notification is received +2. **Report click** — Call `CometChat.markPushNotificationClicked()` when the user taps the notification +3. **Deep link** — Use the announcement ID from the push payload to fetch the full item via `CometChat.getNotificationFeedItem(id)` and display it + +```tsx lines +import { CometChat } from "@cometchat/chat-sdk-react-native"; + +// When push notification is received +const pushNotification = new CometChat.PushNotification(pushPayloadData); +CometChat.markPushNotificationDelivered(pushNotification); + +// When user taps the notification +CometChat.markPushNotificationClicked(pushNotification); + +// Navigate to feed or show specific item +const item = await CometChat.getNotificationFeedItem(pushNotification.getId()); +``` + +See the [SDK Campaigns documentation](/sdk/react-native/campaigns) for the complete push notification tracking API. + +--- + +## Sending Campaigns + +Campaigns are created and managed from the **CometChat Dashboard** or via the **REST API**. The SDK and UI Kit are consumer-side — they display and interact with campaigns, not create them. + +To send campaigns: +- **Dashboard**: Navigate to Campaigns → Create Campaign → Define audience, content (Card Schema), and delivery channel +- **REST API**: Use the Campaigns API to programmatically create and schedule campaigns + +--- + +## Using the UI Kit Component + +The easiest way to add a notification feed to your app is the `CometChatNotificationFeed` component. It handles fetching, rendering, pagination, filtering, real-time updates, and engagement reporting out of the box. + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react-native"; + +function NotificationsScreen() { + return ( + { + // Handle item tap + }} + onBackPress={() => { + // Navigate back + }} + /> + ); +} +``` + +See the full [CometChatNotificationFeed component documentation](/ui-kit/react-native/notification-feed) for all configuration options, styling, and customization. + +--- + +## Next Steps + + + + Full API reference for feed items, categories, engagement, and push tracking + + + Ready-to-use component with filtering, real-time updates, and styling + + diff --git a/ui-kit/react-native/notification-feed.mdx b/ui-kit/react-native/notification-feed.mdx new file mode 100644 index 000000000..33dbd9c32 --- /dev/null +++ b/ui-kit/react-native/notification-feed.mdx @@ -0,0 +1,544 @@ +--- +title: "Notification Feed" +description: "Full-screen notification feed component with category filtering, card rendering, real-time updates, and engagement reporting." +--- + + +```json +{ + "component": "CometChatNotificationFeed", + "package": "@cometchat/chat-uikit-react-native", + "import": "import { CometChatNotificationFeed } from \"@cometchat/chat-uikit-react-native\";", + "description": "Full-screen notification feed with category filtering, timestamp grouping, card rendering via @cometchat/cards-react-native, real-time updates, and automatic engagement reporting.", + "props": { + "data": { + "title": { "type": "string", "default": "\"Notifications\"" }, + "scrollToItemId": { "type": "string", "default": "undefined", "note": "Deep link to a specific feed item" }, + "notificationFeedRequestBuilder": { "type": "NotificationFeedRequestBuilder", "default": "SDK default (20 per page)" }, + "notificationCategoriesRequestBuilder": { "type": "NotificationCategoriesRequestBuilder", "default": "SDK default (50 per page)" } + }, + "callbacks": { + "onItemClick": "(feedItem: NotificationFeedItem) => void", + "onActionClick": "(feedItem: NotificationFeedItem, action: { type: string, params: object, elementId: string }) => void", + "onError": "(error: CometChat.CometChatException) => void", + "onBackPress": "() => void" + }, + "visibility": { + "showHeader": { "type": "boolean", "default": true }, + "showBackButton": { "type": "boolean", "default": false }, + "showFilterChips": { "type": "boolean", "default": true } + }, + "viewSlots": { + "HeaderView": "() => JSX.Element", + "EmptyView": "() => JSX.Element", + "ErrorView": "() => JSX.Element", + "LoadingView": "() => JSX.Element" + }, + "cards": { + "cardThemeMode": { "type": "\"auto\" | \"light\" | \"dark\"", "default": "\"auto\"" }, + "cardThemeOverride": { "type": "CometChatCardThemeOverride", "default": "undefined" } + } + }, + "automaticBehaviors": [ + "Real-time updates via WebSocket listener", + "Delivery reporting on fetch", + "Read reporting on viewport visibility", + "Unread count polling every 30 seconds", + "Infinite scroll pagination", + "Pull-to-refresh", + "Timestamp grouping (Today, Yesterday, day name, date)", + "Category filter chips" + ] +} +``` + + +`CometChatNotificationFeed` displays a scrollable notification feed where each item is rendered as a native card using the CometChat Cards library. It handles fetching, pagination, category filtering, timestamp grouping, real-time updates, and read/delivered/engagement reporting automatically. + +--- + +## Where It Fits + +`CometChatNotificationFeed` is a full-screen component. Drop it into a screen or navigation destination. It manages its own data fetching, state, and real-time listeners — you just handle navigation callbacks. + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react-native"; + +function NotificationsScreen({ navigation }) { + return ( + navigation.goBack()} + onItemClick={(item) => { + // Handle item tap (e.g., open detail or deep link) + }} + /> + ); +} +``` + +--- + +## Minimal Render + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react-native"; + +function NotificationsDemo() { + return ; +} + +export default NotificationsDemo; +``` + +Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()` and a user logged in. + +--- + +## Filtering Feed Items + +Control what loads using custom request builders: + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react-native"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; + +function UnreadNotifications() { + return ( + + ); +} +``` + +### Filter Options + +| Builder Method | Description | +| --- | --- | +| `.setLimit(number)` | Items per page (default 20, max 100) | +| `.setReadState(FeedReadState)` | `READ`, `UNREAD`, or `ALL` | +| `.setCategory(string)` | Filter by category label | +| `.setChannelId(string)` | Filter by channel | +| `.setTags(string[])` | Filter by tags | +| `.setDateFrom(string)` | ISO 8601 date lower bound | +| `.setDateTo(string)` | ISO 8601 date upper bound | + +--- + +## Actions and Events + +### Callback Props + +#### onItemClick + +Fires when a feed item card is tapped. + +```tsx lines + { + console.log("Item tapped:", item.getId()); + }} +/> +``` + +#### onActionClick + +Fires when an interactive element (button, link) inside a card is tapped. + +```tsx lines + { + const { type, params, elementId } = action; + switch (type) { + case "openUrl": + // Open params.url in browser + break; + case "chatWithUser": + // Navigate to chat with params.uid + break; + } + }} +/> +``` + +#### onError + +Fires when an internal error occurs (network failure, SDK exception). + +```tsx lines + { + console.error("Feed error:", error.message); + }} +/> +``` + +#### onBackPress + +Fires when the back button in the header is tapped. + +```tsx lines + navigation.goBack()} +/> +``` + +### Automatic Behaviors + +The component handles these automatically — no manual setup needed: + +| Behavior | Description | +| --- | --- | +| Real-time updates | New items appear at the top via WebSocket listener | +| Delivery reporting | Items are reported as delivered when fetched | +| Read reporting | Items are reported as read when visible in viewport | +| Unread count polling | Polls unread count every 30 seconds to update badges | +| Infinite scroll | Fetches next page when scrolling near the bottom | +| Pull-to-refresh | Resets and fetches fresh data on pull | +| Timestamp grouping | Groups items as "Today", "Yesterday", day name, or date | +| Category filtering | Filter chips row for category-based filtering | + +--- + +## Custom View Slots + +### HeaderView + +Replace the entire header: + +```tsx lines + ( + + My Notifications + + )} +/> +``` + +### State Views + +```tsx lines + ( + + No notifications yet + + )} + ErrorView={() => ( + + Something went wrong + + )} + LoadingView={() => ( + + + + )} +/> +``` + +--- + +## Styling + +The component uses the theme system from `CometChatThemeProvider`. Pass a `style` prop to customize the appearance. + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react-native"; + +function StyledNotifications() { + return ( + + ); +} +``` + +### Style Properties + +| Property | Description | +| --- | --- | +| `containerStyle` | Root container style | +| `headerContainerStyle` | Header bar style | +| `titleStyle` | Header title text style | +| `chipActiveStyle` | Selected filter chip (containerStyle + textStyle) | +| `chipInactiveStyle` | Unselected filter chip (containerStyle + textStyle) | +| `chipBadgeStyle` | Active chip badge (containerStyle + textStyle) | +| `chipInactiveBadgeStyle` | Inactive chip badge (containerStyle + textStyle) | +| `sectionHeaderStyle` | Timestamp section header (containerStyle + textStyle) | +| `itemHeaderStyle` | Per-item header (containerStyle + categoryTextStyle + timestampTextStyle) | +| `cardContainerStyle` | Card container style | +| `cardBorderColor` | Card border color | +| `cardBorderRadius` | Card corner radius | +| `cardBorderWidth` | Card border width | +| `unreadIndicatorStyle` | Unread dot style | +| `unreadIndicatorColor` | Unread dot color | +| `emptyStateStyle` | Empty state (containerStyle + titleStyle + subTitleStyle) | +| `errorStateStyle` | Error state (containerStyle + titleStyle + subTitleStyle) | +| `separatorStyle` | Separator between cards | + +--- + +## Props + +All props are optional. + +### cardThemeMode + +Theme mode for the card renderer. + +| | | +| --- | --- | +| Type | `"auto" \| "light" \| "dark"` | +| Default | `"auto"` | + +--- + +### cardThemeOverride + +Custom theme override for the card renderer. + +| | | +| --- | --- | +| Type | `CometChatCardThemeOverride` | +| Default | `undefined` | + +--- + +### EmptyView + +Custom component displayed when there are no notifications. + +| | | +| --- | --- | +| Type | `() => JSX.Element` | +| Default | Built-in empty state | + +--- + +### ErrorView + +Custom component displayed when an error occurs. + +| | | +| --- | --- | +| Type | `() => JSX.Element` | +| Default | Built-in error state | + +--- + +### HeaderView + +Custom component replacing the entire header. + +| | | +| --- | --- | +| Type | `() => JSX.Element` | +| Default | Built-in header | + +--- + +### LoadingView + +Custom component displayed during the loading state. + +| | | +| --- | --- | +| Type | `() => JSX.Element` | +| Default | Built-in loading indicator | + +--- + +### notificationCategoriesRequestBuilder + +Custom request builder for fetching categories. + +| | | +| --- | --- | +| Type | `NotificationCategoriesRequestBuilder` | +| Default | SDK default (50 per page) | + +--- + +### notificationFeedRequestBuilder + +Custom request builder for fetching feed items. + +| | | +| --- | --- | +| Type | `NotificationFeedRequestBuilder` | +| Default | SDK default (20 per page) | + +--- + +### onActionClick + +Callback fired when an interactive element inside a card is tapped. + +| | | +| --- | --- | +| Type | `(feedItem: NotificationFeedItem, action: { type: string, params: object, elementId: string }) => void` | +| Default | `undefined` | + +--- + +### onBackPress + +Callback fired when the back button is pressed. + +| | | +| --- | --- | +| Type | `() => void` | +| Default | `undefined` | + +--- + +### onError + +Callback fired when the component encounters an error. + +| | | +| --- | --- | +| Type | `(error: CometChat.CometChatException) => void` | +| Default | `undefined` | + +--- + +### onItemClick + +Callback fired when a feed item card is tapped. + +| | | +| --- | --- | +| Type | `(feedItem: NotificationFeedItem) => void` | +| Default | `undefined` | + +--- + +### scrollToItemId + +Deep link to a specific feed item by ID. + +| | | +| --- | --- | +| Type | `string` | +| Default | `undefined` | + +--- + +### showBackButton + +Shows/hides the back button in the header. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### showFilterChips + +Shows/hides the category filter chips row. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `true` | + +--- + +### showHeader + +Shows/hides the entire header. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `true` | + +--- + +### title + +Header title text. + +| | | +| --- | --- | +| Type | `string` | +| Default | `"Notifications"` | + +--- + +## Common Patterns + +### Show only unread items + +```tsx lines + +``` + +### Hide filter chips and header + +```tsx lines + +``` + +### Deep link to a specific notification + +```tsx lines + +``` + +--- + +## Next Steps + + + + Overview of how campaigns work end-to-end + + + Low-level SDK APIs for feed items, categories, and engagement + + + Full styling reference for all components + + + Customize colors, fonts, and appearance + + From f0d7aa29254491c15265cfa19518f6372e86441c Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 20:11:15 +0530 Subject: [PATCH 22/45] docs(campaigns): Add draft status to campaigns and templates --- campaigns.mdx | 2 +- campaigns/campaigns.mdx | 1 + campaigns/templates.mdx | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/campaigns.mdx b/campaigns.mdx index 6c50acc54..ca561bbf7 100644 --- a/campaigns.mdx +++ b/campaigns.mdx @@ -75,7 +75,7 @@ Maintenance windows, policy updates, account changes. Dispatched to a broad audi Create reusable notification templates with variables and versioning - + Create and manage targeted notification campaigns diff --git a/campaigns/campaigns.mdx b/campaigns/campaigns.mdx index cc6cfa6f9..ef3664e80 100644 --- a/campaigns/campaigns.mdx +++ b/campaigns/campaigns.mdx @@ -42,6 +42,7 @@ You can click "Send Now" on a scheduled campaign to override the schedule and se | Status | Description | |--------|-------------| +| `draft` | Campaign created but not yet sent or scheduled | | `scheduled` | Queued for future delivery | | `sending` | Currently dispatching to recipients | | `completed` | All recipients processed successfully | diff --git a/campaigns/templates.mdx b/campaigns/templates.mdx index 4dd38857b..885744fb2 100644 --- a/campaigns/templates.mdx +++ b/campaigns/templates.mdx @@ -16,7 +16,7 @@ A template is a reusable notification design that defines the content, delivery | `label` | string | No | Display label shown on notification (e.g., "Promo") | | `alternativeText` | string | No | Plain-text fallback when rich content can't render | | `tags` | string[] | No | Tags for filtering and segmentation | -| `status` | enum | Yes | `approved` \| `archived` | +| `status` | enum | Yes | `draft` \| `approved` \| `archived` | | `channels` | array | Yes | Channel configurations (at least one) | | `variableSchema` | array | No | Variable definitions | @@ -71,6 +71,7 @@ Each template has one or more channel configurations: | Status | Description | |--------|-------------| +| `draft` | Work in progress — cannot be used to send | | `approved` | Ready to use — can be selected for campaigns and notifications | | `archived` | Soft-deleted — hidden from lists, not usable | From 4b923a70fdeee18bb13da93a1f4d5df74826a8b6 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 20:14:43 +0530 Subject: [PATCH 23/45] docs(campaigns): Clarify sequence stop conditions by channel type --- campaigns/sequences.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/campaigns/sequences.mdx b/campaigns/sequences.mdx index c086648d7..23f596046 100644 --- a/campaigns/sequences.mdx +++ b/campaigns/sequences.mdx @@ -18,7 +18,9 @@ Sequences are configured at the template level. When you create or edit a templa 2. Enable the **Sequence** toggle in the template settings. 3. Arrange the channels in the desired delivery order. The first channel fires immediately at send time. 4. For each subsequent step, configure: - - **Stop condition.** The engagement signal that halts the sequence: `delivered` or `clicked`. + - **Stop condition.** The engagement signal that halts the sequence. Available conditions depend on the channel: + - **In-app:** `delivered` | `read` | `engaged` + - **Push:** `delivered` | `clicked` - **Wait window.** How long to wait for the stop condition before moving to the next step: 5, 10, 30, 60, 240, or 1440 minutes. If the stop condition is met within the wait window, the remaining steps are skipped. If the window expires without the condition being met, the next channel fires. From 2f919a045fa8258c47c1e89026cda88c82dea5e3 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 20:24:23 +0530 Subject: [PATCH 24/45] fix(campaigns-apis): Make onbehalfof header required and fix variable schema types - Mark onbehalfof header parameter as required across three API endpoints - Change variableSchema items type from string to object in template definitions - Update variable naming regex pattern to exclude dots and add end anchor - Align API schema with actual implementation requirements --- campaigns-apis.json | 10 +++++----- campaigns/templates.mdx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/campaigns-apis.json b/campaigns-apis.json index 594f56a5b..598426ed6 100644 --- a/campaigns-apis.json +++ b/campaigns-apis.json @@ -354,7 +354,7 @@ "name": "onbehalfof", "in": "header", "description": "UID of user making client request", - "required": false, + "required": true, "schema": { "type": "string" } @@ -396,7 +396,7 @@ "name": "onbehalfof", "in": "header", "description": "UID of user making client request", - "required": false, + "required": true, "schema": { "type": "string" } @@ -438,7 +438,7 @@ "name": "onbehalfof", "in": "header", "description": "UID of user making client request", - "required": false, + "required": true, "schema": { "type": "string" } @@ -3119,7 +3119,7 @@ "description": "Variable schema definitions", "type": "array", "items": { - "type": "string" + "type": "object" } }, "config": { @@ -3162,7 +3162,7 @@ "variableSchema": { "type": "array", "items": { - "type": "string" + "type": "object" } }, "config": { diff --git a/campaigns/templates.mdx b/campaigns/templates.mdx index 885744fb2..16bdbb77a 100644 --- a/campaigns/templates.mdx +++ b/campaigns/templates.mdx @@ -31,7 +31,7 @@ A template is a reusable notification design that defines the content, delivery Variables allow per-recipient personalization in notification content. - **Syntax**: `{{variable_name}}` in template content -- **Naming**: Letters, numbers, dots, and underscores only (`^[a-zA-Z_][a-zA-Z0-9_.]*`) +- **Naming**: Letters, numbers, and underscores only (`^[a-zA-Z_][a-zA-Z0-9_]*$`) - **Type**: Selected from a dropdown with the following options: | Type | Label | Hint | From 45bb02f66de3b280eafb87874bc0237eb30a36fd Mon Sep 17 00:00:00 2001 From: Aditya Date: Wed, 27 May 2026 20:26:51 +0530 Subject: [PATCH 25/45] added campaigns in UI kit and SKD --- docs.json | 7 +- sdk/ios/campaigns.mdx | 499 +++++++++++++++++++++++++++++++ ui-kit/ios/campaigns.mdx | 187 ++++++++++++ ui-kit/ios/notification-feed.mdx | 288 ++++++++++++++++++ 4 files changed, 979 insertions(+), 2 deletions(-) create mode 100644 sdk/ios/campaigns.mdx create mode 100644 ui-kit/ios/campaigns.mdx create mode 100644 ui-kit/ios/notification-feed.mdx diff --git a/docs.json b/docs.json index 172825868..ee570524d 100644 --- a/docs.json +++ b/docs.json @@ -1268,7 +1268,8 @@ "ui-kit/ios/ai-features" ] }, - "ui-kit/ios/call-features" + "ui-kit/ios/call-features", + "ui-kit/ios/campaigns" ] }, { @@ -1301,7 +1302,8 @@ "ui-kit/ios/call-buttons", "ui-kit/ios/call-logs", "ui-kit/ios/ai-assistant-chat-history", - "ui-kit/ios/search" + "ui-kit/ios/search", + "ui-kit/ios/notification-feed" ] }, { @@ -3680,6 +3682,7 @@ }, "sdk/ios/ai-moderation", "sdk/ios/ai-agents", + "sdk/ios/campaigns", { "group": "Resources", "pages": [ diff --git a/sdk/ios/campaigns.mdx b/sdk/ios/campaigns.mdx new file mode 100644 index 000000000..d1d08743a --- /dev/null +++ b/sdk/ios/campaigns.mdx @@ -0,0 +1,499 @@ +--- +title: "Campaigns" +description: "CometChat Campaigns SDK APIs — fetching feed items, managing engagement state, reporting interactions, and receiving real-time notifications." +--- + +CometChat Campaigns lets you deliver targeted, rich notifications to users via an in-app notification feed. Each notification is a Card Schema JSON — a structured layout rendered natively by the CometChat Cards library. The SDK provides APIs to fetch feed items, listen for real-time delivery, mark items as read/delivered, report engagement, and retrieve unread counts. + +--- + +## CometChatCardsSwift + +`CometChatCardsSwift` is the rendering library for Campaigns. The `NotificationFeedItem.content` field contains a Card Schema JSON — a structured layout definition. `CometChatCardsSwift` takes this JSON and produces a native UIKit view. Without it, `content` is just a raw dictionary. + +The library: + +- Parses the JSON schema into typed Swift models. +- Renders native UIKit views (no web views) for each element (text, images, buttons, rows, etc.). +- Handles light/dark mode color resolution from themed color pairs. +- Manages interactive actions — button taps emit structured `CometChatCardActionEvent` objects with the action type and element ID. +- Supports dynamic content — accordions expand/collapse, tabs switch panels, all with proper auto-layout height changes. + +If you use `CometChatNotificationFeed` (UI Kit), the Cards library is used internally and you don't interact with it directly. If you're building a custom feed UI, you import `CometChatCardsSwift` and use `CometChatCardView` to render each item's content. + +--- + +## Setting Up CometChatCardsSwift + +### Installation + +**Swift Package Manager** + +In Xcode: **File → Add Package Dependencies** → enter: + +``` +https://github.com/cometchat/cometchat-cards-sdk-ios +``` + +Add `CometChatCardsSwift` to your target. + +**CocoaPods** + +```ruby +pod 'CometChatCardsSwift' +``` + +```bash +pod install +``` + +No initialization is required. The library is stateless — import it and start rendering. + +### Rendering Feed Items as Cards + +```swift +import CometChatSDK +import CometChatCardsSwift + +// After fetching items via NotificationFeedRequest +request.fetchNext(onSuccess: { items in + for item in items { + guard let jsonData = try? JSONSerialization.data(withJSONObject: item.content), + let jsonString = String(data: jsonData, encoding: .utf8) else { continue } + + let cardView = CometChatCardView(frame: .zero) + cardView.translatesAutoresizingMaskIntoConstraints = false + cardView.themeMode = .auto + cardView.cardJson = jsonString + + cardView.actionCallback = { actionEvent in + // Report engagement on every action + CometChat.reportFeedEngagement(item, interactionString: actionEvent.elementId, onSuccess: {}, onError: { _ in }) + + switch actionEvent.action { + case .openUrl(let url, _): + if let link = URL(string: url) { + UIApplication.shared.open(link) + } + case .chatWithUser(let uid): + // Navigate to 1-on-1 chat + break + case .chatWithGroup(let guid): + // Navigate to group chat + break + default: + break + } + } + + container.addSubview(cardView) + } +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + +### CometChatCardView Properties + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `cardJson` | `String?` | Set this to render a card. Triggers a full re-render. | +| `themeMode` | `CometChatCardThemeMode` | `.auto` (follows system), `.light`, or `.dark`. | +| `actionCallback` | `((CometChatCardActionEvent) -> Void)?` | Called when user taps a button, link, or icon button inside the card. | +| `themeOverride` | `CometChatCardThemeOverride?` | Override default theme colors without modifying card JSON. | + +### Theme Override + +Override default card theme colors to match your app's brand: + +```swift +var override = CometChatCardThemeOverride() +override.textColor = CometChatCardColorValue(light: "#1A1A1A", dark: "#F0F0F0") +override.buttonFilledBg = CometChatCardColorValue(light: "#6852D6", dark: "#8B7AE8") +override.buttonFilledText = CometChatCardColorValue(light: "#FFFFFF", dark: "#FFFFFF") +override.fontFamily = "Avenir" + +cardView.themeOverride = override +``` + +### Content Size Changes + +Cards with expandable content (accordions, tabs) change height dynamically. Observe size changes to update your layout: + +```swift +NotificationCenter.default.addObserver( + forName: CometChatCardView.contentSizeDidChangeNotification, + object: cardView, + queue: .main +) { _ in + tableView.beginUpdates() + tableView.endUpdates() +} +``` + +--- + +## Key Concepts + +| Concept | Description | +| ------- | ----------- | +| NotificationFeedItem | A single notification in the feed. Contains Card Schema JSON in its `content` field, a category for filtering, timestamps, and metadata. | +| NotificationCategory | A category label used for filter chips (e.g., "Promotions", "Updates"). | +| Card Schema JSON | The fully rendered card layout (images, text, buttons) inside `NotificationFeedItem.content`. Passed directly to the CometChat Cards renderer. | +| PushNotification | Represents a campaign push notification payload received via APNs. | + +--- + +## How Cards Render in the Notification Feed + +Each `NotificationFeedItem` has a `content` field containing a `[String: Any]` dictionary — this is the Card Schema JSON. This JSON is passed directly to the CometChat Cards renderer library. + +The rendering flow: + +1. Fetch feed items via `NotificationFeedRequest` +2. For each item, extract `item.content` — this is the Card Schema JSON +3. Pass to the Cards renderer (`CometChatCardView`) +4. The renderer produces a native UIKit view from the JSON + +### Card Schema JSON Structure + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://...", "height": 200 }, + { "type": "text", "id": "txt_1", "content": "Flash Sale!", "variant": "heading2" }, + { "type": "button", "id": "btn_1", "label": "Shop Now", "action": { "type": "openUrl", "url": "https://..." } } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale! Shop Now: https://..." +} +``` + +The `body` array contains elements (text, image, button, row, column, etc.) rendered top-to-bottom. Interactive elements like buttons emit actions via a callback — the consumer handles navigation, deep links, or API calls. + +--- + +## Retrieve Notification Feed Items + +Use `NotificationFeedRequest` to fetch a paginated list of feed items. Uses cursor-based pagination internally. + +### Build the Request + +```swift +let request = NotificationFeedRequest.NotificationFeedRequestBuilder() + .set(limit: 20) + .set(readState: .unread) + .set(category: "Updates") + .build() +``` + +### Builder Parameters + +| Method | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| `set(limit:)` | `Int` | 20 | Items per page. | +| `set(readState:)` | `FeedReadState` | All | Filter by `.read` or `.unread`. Omit for all. | +| `set(category:)` | `String` | nil | Filter by category name. Sent as `templateCategory` query param. If `categoryId` is also set, `categoryId` takes priority. | +| `set(categoryId:)` | `String` | nil | Filter by category ID. Sent as `templateCategory` query param. Takes priority over `category` if both are set. | +| `set(channelId:)` | `String` | nil | Filter by channel. | +| `set(tags:)` | `[String]` | nil | Filter by tags (comma-joined). | +| `set(dateFrom:)` | `String` | nil | ISO 8601 date — items sent on or after. | +| `set(dateTo:)` | `String` | nil | ISO 8601 date — items sent on or before. | + + +`set(category:)` and `set(categoryId:)` both map to the same server-side filter (`templateCategory`). Use one or the other — not both. If both are provided, `categoryId` silently overwrites `category`. + + +### Fetch Items + +```swift +request.fetchNext(onSuccess: { items in + for item in items { + let cardJson = item.content + // Pass cardJson to CometChatCardView + } +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + +Call `fetchNext()` repeatedly for pagination. When the server has no more items, subsequent calls return an empty array. + +### NotificationFeedItem Fields + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `id` | `String` | Unique item identifier. | +| `category` | `String` | Notification category (e.g., "promotions"). | +| `categoryId` | `String` | Category ID. | +| `content` | `[String: Any]` | Card Schema JSON — pass to CometChat Cards renderer. | +| `readAt` | `Double` | Unix timestamp when read, or 0 if unread. | +| `deliveredAt` | `Double` | Unix timestamp when delivered, or 0. | +| `sentAt` | `Double` | Unix timestamp when sent. | +| `metadata` | `[String: Any]` | Custom key-value metadata. | +| `tags` | `[String]` | Tags for filtering. | +| `sender` | `String` | Sender identifier. | +| `receiver` | `String` | Receiver identifier. | +| `receiverType` | `String` | Receiver type. | +| `isRead` | `Bool` | Computed — `true` if `readAt != 0`. | + +--- + +## Retrieve Notification Categories + +Use `NotificationCategoriesRequest` to fetch available categories for filter chips. + +```swift +let categoriesRequest = NotificationCategoriesRequest.NotificationCategoriesRequestBuilder() + .set(limit: 50) + .build() + +categoriesRequest.fetchNext(onSuccess: { categories in + for category in categories { + print("Category: \(category.label)") + } +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + +### NotificationCategory Fields + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `id` | `String` | Category identifier. | +| `label` | `String` | Display label for filter UI. | + +--- + +## Real-Time Notification Feed Listener + +Listen for new feed items arriving via WebSocket. This listener is independent from `CometChatMessageDelegate`, `CometChatGroupDelegate`, and `CometChatCallDelegate`. + +```swift +CometChat.addNotificationFeedListener("feedListener", self) + +// Implement CometChatNotificationFeedDelegate +extension MyViewController: CometChatNotificationFeedDelegate { + func onFeedItemReceived(feedItem: NotificationFeedItem) { + print("New item: \(feedItem.id)") + let cardJson = feedItem.content + // Insert at top of feed and render + } +} +``` + +Remove the listener when no longer needed: + +```swift +CometChat.removeNotificationFeedListener("feedListener") +``` + +--- + +## Mark Feed Item as Read + +Mark a single item as read. Idempotent — safe to call multiple times. + +```swift +CometChat.markFeedItemAsRead(feedItem, onSuccess: { + print("Marked as read") +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + +--- + +## Mark Feed Item as Delivered + +Mark a single item as delivered. Idempotent. + +```swift +CometChat.markFeedItemAsDelivered(feedItem, onSuccess: { + // Success +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + +--- + +## Report Engagement + +Report that a user engaged with a feed item (e.g., viewed, clicked, interacted). Idempotent per topic. + +```swift +CometChat.reportFeedEngagement(feedItem, interactionString: "clicked", onSuccess: { + // Success +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + +The `interactionString` parameter is a free-form string describing the engagement (e.g., "viewed", "clicked", "interacted"). + +--- + +## Get Unread Count + +Fetch the total number of unread notification feed items. Optionally filter by category. + +```swift +CometChat.getNotificationFeedUnreadCount(category: nil, onSuccess: { count in + print("Unread: \(count)") +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + +--- + +## Fetch Single Feed Item + +Fetch a specific item by ID — useful for deep linking from push notifications. + +```swift +CometChat.getNotificationFeedItem(id: "item-id-123", onSuccess: { item in + let cardJson = item.content + // Render the card +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + +--- + +## Push Notification Tracking + +When a campaign push notification arrives via APNs, use these methods to report delivery and click engagement. + +### Setting Up Notification Service Extension + +1. In Xcode: **File → New → Target** → select **Notification Service Extension**. +2. Enable **App Groups** on both your main app target and the extension target with the same group ID (e.g., `group.com.yourapp.cometchat`). +3. Add `CometChatSDK` as a dependency to the extension target. + +### Identifying Campaign Push Notifications + +Campaign pushes contain a `cometchat` dictionary with `"type": "business_messaging"`: + +```swift +func isCampaignNotification(userInfo: [AnyHashable: Any]) -> Bool { + if let cometchat = userInfo["cometchat"] as? [String: Any], + let type = cometchat["type"] as? String, + type == "business_messaging" { + return true + } + return false +} +``` + +### Push Payload Structure + +```json +{ + "aps": { "alert": { "title": "...", "body": "..." }, "badge": 1, "mutable-content": 1 }, + "cometchat": { + "type": "business_messaging", + "appId": "your-app-id", + "campaignId": "campaign-cuid", + "notificationId": "notification-cuid", + "pushNotificationId": "uuid-for-push-tracking", + "uid": "target-user-id", + "sentAt": "2026-05-21T12:43:55.206Z" + } +} +``` + +### Mark Push Notification as Delivered + +Call this in your Notification Service Extension: + +```swift +import UserNotifications +import CometChatSDK + +class NotificationService: UNNotificationServiceExtension { + override func didReceive(_ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + let bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)! + + // Required for extensions to share auth state via App Group container + CometChat.setExtensionGroupID(id: "group.your.app.group") + + let userInfo = bestAttemptContent.userInfo + if let cometchat = userInfo["cometchat"] as? [String: Any], + let type = cometchat["type"] as? String, + type == "business_messaging" { + let push = PushNotification.fromPayload(cometchat) + CometChat.markPushNotificationDelivered(push, onSuccess: {}, onError: { _ in }) + } + + contentHandler(bestAttemptContent) + } +} +``` + +### Mark Push Notification as Clicked + +Call this when the user taps the push notification: + +```swift +func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + let userInfo = response.notification.request.content.userInfo + + if let cometchat = userInfo["cometchat"] as? [String: Any], + let type = cometchat["type"] as? String, + type == "business_messaging" { + let push = PushNotification.fromPayload(cometchat) + CometChat.markPushNotificationClicked(push, onSuccess: {}, onError: { _ in }) + navigateToNotificationsTab() + completionHandler() + return + } + + completionHandler() +} +``` + +### PushNotification Fields + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `id` | `String` | Push notification ID from the push payload. | +| `announcementId` | `String` | Notification/announcement ID. | +| `campaignId` | `String?` | Campaign ID if from a campaign. | +| `source` | `String` | Always "campaign" for notification feed pushes. | + +--- + +## FeedReadState Enum + +| Case | Value | Description | +| ---- | ----- | ----------- | +| `.read` | `"read"` | Only read items. | +| `.unread` | `"unread"` | Only unread items. | + +If not set, returns all items (default). + +--- + +## Supported Card Actions + +When a user taps a button or link inside a card, the action callback receives one of these action types: + +| Action Type | Parameters | Description | +| ----------- | ---------- | ----------- | +| `openUrl` | url, openIn | Open a URL in browser or webview. | +| `chatWithUser` | uid | Navigate to 1:1 chat. | +| `chatWithGroup` | guid | Navigate to group chat. | +| `sendMessage` | text, receiverUid, receiverGuid | Send a text message. | +| `copyToClipboard` | value | Copy text to clipboard. | +| `downloadFile` | url, filename | Download a file. | +| `initiateCall` | callType (audio/video), uid, guid | Start a call. | +| `apiCall` | url, method, headers, body | Make an HTTP request. | +| `customCallback` | callbackId, payload | App-specific logic. | diff --git a/ui-kit/ios/campaigns.mdx b/ui-kit/ios/campaigns.mdx new file mode 100644 index 000000000..fdf2f743d --- /dev/null +++ b/ui-kit/ios/campaigns.mdx @@ -0,0 +1,187 @@ +--- +title: "Campaigns" +description: "Deliver targeted, rich notifications to users via an in-app notification feed powered by the CometChat Cards renderer." +--- + +CometChat Campaigns enables you to send rich, interactive notifications to users through an in-app notification feed. Each notification is rendered as a native card using the CometChat Cards library — supporting images, text, buttons, layouts, and interactive actions. + +--- + +## Overview + +Campaigns delivers notifications as Card Schema JSON — a structured format that defines the visual layout of each notification card. The system consists of three layers: + +- **CometChat Chat SDK** — Fetches feed items, manages read/delivered state, provides real-time listeners, handles push notification tracking +- **CometChat Cards Library** — Renders Card Schema JSON into native UIKit views +- **CometChat UI Kit** — Provides the ready-to-use `CometChatNotificationFeed` component that wires everything together + +--- + +## Architecture Flow + +``` +Dashboard / API → Campaign Created → Push + WebSocket Delivery + ↓ + SDK: NotificationFeedRequest.fetchNext() + ↓ + NotificationFeedItem.content → Card Schema JSON + ↓ + Cards Library: CometChatCardView + ↓ + Native Rendered Card (images, text, buttons, layouts) + ↓ + User taps button → actionCallback → Your code handles it +``` + +--- + +## How Cards Work + +Each `NotificationFeedItem` from the SDK contains a `content` field — a `[String: Any]` dictionary holding the Card Schema JSON. This JSON is passed directly to the CometChat Cards renderer which produces a native UIKit view. + +The Cards library is a pure renderer: + +- **Input:** Card Schema JSON string + theme mode + optional action callback +- **Output:** Native UIKit view hierarchy + +It does not execute actions, manage message state, or call any SDK methods. When users tap interactive elements (buttons, links), the library emits the action to your callback. You decide what happens — open a URL, navigate to a chat, make an API call, etc. + +--- + +## Card Schema JSON Example + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://cdn.example.com/sale.jpg", "height": 180, "fit": "cover", "borderRadius": 8 }, + { "type": "text", "id": "txt_1", "content": "🎉 Flash Sale — 40% Off!", "variant": "heading2" }, + { "type": "text", "id": "txt_2", "content": "Limited time offer on all premium plans.", "variant": "body" }, + { "type": "button", "id": "btn_1", "label": "Claim Offer", "action": { "type": "openUrl", "url": "https://example.com/offer" }, "fullWidth": true } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale — 40% Off! Claim your offer: https://example.com/offer" +} +``` + +The schema supports 20 element types (text, image, icon, avatar, badge, divider, spacer, chip, progressBar, codeBlock, markdown, row, column, grid, accordion, tabs, button, iconButton, link, table) and 9 action types (openUrl, chatWithUser, chatWithGroup, sendMessage, copyToClipboard, downloadFile, initiateCall, apiCall, customCallback). + +--- + +## Notification Feed Component + +The `CometChatNotificationFeed` component renders campaign notifications as a full-screen scrollable feed. It handles fetching, card rendering, pagination, filtering, real-time updates, and engagement reporting automatically. + + + +```swift +let notificationFeed = CometChatNotificationFeed() +let navController = UINavigationController(rootViewController: notificationFeed) +self.present(navController, animated: true) +``` + + + +See the full [CometChatNotificationFeed](/ui-kit/ios/notification-feed) documentation for all configuration options, actions, filtering, and customization. + +--- + +## Setting Up Notification Service Extension + +To track push notification delivery for campaigns, you need a Notification Service Extension. This runs when a push arrives on the device (even if the app is in the background). + +### Step 1: Create the Extension + +1. In Xcode, go to **File → New → Target**. +2. Select **Notification Service Extension**. +3. Name it (e.g., `NotificationService`). +4. Click **Finish**. + +### Step 2: Configure App Groups + +Both your main app and the extension need to share data via an App Group: + +1. Select your **main app target** → Signing & Capabilities → **+ Capability** → **App Groups**. +2. Add a group (e.g., `group.com.yourapp.cometchat`). +3. Select your **extension target** → repeat the same steps with the same group ID. + +### Step 3: Add CometChatSDK to the Extension + +Add `CometChatSDK` as a dependency to the extension target (via SPM or CocoaPods). + +### Step 4: Implement the Extension + + + +```swift +import UserNotifications +import CometChatSDK + +class NotificationService: UNNotificationServiceExtension { + override func didReceive(_ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + let bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)! + + // Share auth state via App Group + CometChat.setExtensionGroupID(id: "group.com.yourapp.cometchat") + + let userInfo = bestAttemptContent.userInfo + if let cometchat = userInfo["cometchat"] as? [String: Any], + let type = cometchat["type"] as? String, + type == "business_messaging" { + let push = PushNotification.fromPayload(cometchat) + CometChat.markPushNotificationDelivered(push, onSuccess: {}, onError: { _ in }) + } + + contentHandler(bestAttemptContent) + } +} +``` + + + +--- + +## Handling Push Notification Clicks + +When the user taps a campaign push notification, report the click and navigate to the feed: + + + +```swift +func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + let userInfo = response.notification.request.content.userInfo + + if let cometchat = userInfo["cometchat"] as? [String: Any], + let type = cometchat["type"] as? String, + type == "business_messaging" { + let push = PushNotification.fromPayload(cometchat) + CometChat.markPushNotificationClicked(push, onSuccess: {}, onError: { _ in }) + + // Navigate to notification feed + let notificationFeed = CometChatNotificationFeed() + let navController = UINavigationController(rootViewController: notificationFeed) + window?.rootViewController?.present(navController, animated: true) + + completionHandler() + return + } + + completionHandler() +} +``` + + + +See the [SDK Campaigns documentation](/sdk/ios/campaigns) for the complete push notification tracking API. + +--- + +## Sending Campaigns + +Campaigns are created and managed from the CometChat Dashboard or via the REST API. The SDK and UI Kit are consumer-side — they display and interact with campaigns, not create them. + +- **Dashboard:** Navigate to Campaigns → Create Campaign → Define audience, content (Card Schema), and delivery channel. +- **REST API:** Use the Campaigns API to programmatically create and schedule campaigns. diff --git a/ui-kit/ios/notification-feed.mdx b/ui-kit/ios/notification-feed.mdx new file mode 100644 index 000000000..923461846 --- /dev/null +++ b/ui-kit/ios/notification-feed.mdx @@ -0,0 +1,288 @@ +--- +title: "Notification Feed" +description: "Full-screen notification feed component with category filtering, card rendering, real-time updates, and engagement reporting." +--- + +`CometChatNotificationFeed` displays a scrollable notification feed where each item is rendered as a native card using the CometChat Cards library. It handles fetching, pagination, category filtering, real-time updates, and read/delivered/engagement reporting automatically. Present it in a `UINavigationController`, push it onto an existing navigation stack, or embed it in a tab bar. + +Each notification gets its own section header with the category label on the left and a relative timestamp on the right: + +| Condition | Display | +| --------- | ------- | +| Today (< 24 hours ago) | Time only — 2:35 PM | +| Yesterday (24–48 hours ago) | Yesterday | +| This week (2–7 days ago) | Day name — Monday, Tuesday, etc. | +| Older (> 7 days) | Full date — 25/05/2026 | + + + +```swift +let notificationFeed = CometChatNotificationFeed() +notificationFeed.set(onItemClick: { feedItem in + // Handle item tap (e.g., open detail or deep link) +}) + +let navController = UINavigationController(rootViewController: notificationFeed) +self.present(navController, animated: true) +``` + + + +--- + +## Quick Start + + + +```swift +let notificationFeed = CometChatNotificationFeed() +let navController = UINavigationController(rootViewController: notificationFeed) +self.present(navController, animated: true) +``` + + + +Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()` and a user logged in. + +--- + +## Filtering Feed Items + +Control what loads using custom request builders: + + + +```swift +let notificationFeed = CometChatNotificationFeed() +notificationFeed.set(notificationFeedRequestBuilder: + NotificationFeedRequest.NotificationFeedRequestBuilder() + .set(limit: 30) + .set(readState: .unread) + .set(category: "promotions") +) +``` + + + +### Filter Options + +| Builder Method | Description | +| -------------- | ----------- | +| `.set(limit: Int)` | Items per page (default 20). | +| `.set(readState: FeedReadState)` | `.read`, `.unread`, or omit for all. | +| `.set(category: String)` | Filter by category name. | +| `.set(categoryId: String)` | Filter by category ID. | +| `.set(channelId: String)` | Filter by channel. | +| `.set(tags: [String])` | Filter by tags. | +| `.set(dateFrom: String)` | ISO 8601 date lower bound. | +| `.set(dateTo: String)` | ISO 8601 date upper bound. | + +Pass the builder object, not the result of `.build()`. The component calls `.build()` internally. + +--- + +## Actions and Events + +### onItemClick + +Fires when a feed item card is tapped. + + + +```swift +notificationFeed.set(onItemClick: { feedItem in + // feedItem.id, feedItem.content (Card JSON), feedItem.category + print("Item tapped: \(feedItem.id)") +}) +``` + + + +--- + +### onActionClick + +Fires when an interactive element (button, link) inside a card is tapped. + + + +```swift +notificationFeed.set(onActionClick: { feedItem, actionEvent in + // Report engagement on every action + CometChat.reportFeedEngagement(feedItem, interactionString: "button_clicked", onSuccess: {}, onError: { _ in }) + + // Handle the action + switch actionEvent.action { + case .openUrl(let url, _): + if let link = URL(string: url) { + UIApplication.shared.open(link) + } + case .chatWithUser(let uid): + // Navigate to 1-on-1 chat + break + case .chatWithGroup(let guid): + // Navigate to group chat + break + case .customCallback(let callbackId, let payload): + // Custom app logic + break + default: + break + } +}) +``` + + + +--- + +### onError + +Fires when an internal error occurs (network failure, SDK exception). + + + +```swift +notificationFeed.set(onError: { error in + print("Feed error: \(error.errorDescription)") +}) +``` + + + +--- + +## Automatic Behaviors + +The component handles these automatically — no manual setup needed: + +| Behavior | Description | +| -------- | ----------- | +| Real-time updates | New items appear at the top via WebSocket listener. | +| Delivery reporting | Items are reported as delivered when fetched. | +| Read reporting | Items are reported as read after 1 second of visibility. | +| Unread count polling | Polls unread count every 30 seconds to update badges. | +| Infinite scroll | Fetches next page when scrolling near the bottom. | +| Pull-to-refresh | Resets and fetches fresh data on pull. | +| Timestamp grouping | Groups items as "Today", "Yesterday", day name, or date. | +| Category filtering | Filter chips row for category-based filtering. | +| Connection recovery | Automatically refreshes when the app returns from background. | + +--- + +## Functionality + +| Method | Description | +| ------ | ----------- | +| `set(title: String)` | Header title text. Default "Notifications". | +| `set(showBackButton: Bool)` | Toggle back button visibility. Default `true`. | +| `set(showFilterChips: Bool)` | Toggle category filter chips. Default `true`. | +| `set(cardThemeMode: String)` | Card rendering theme: "auto", "light", or "dark". | +| `set(notificationFeedRequestBuilder:)` | Custom feed request builder. | +| `set(notificationCategoriesRequestBuilder:)` | Custom categories request builder. | + + + +```swift +let notificationFeed = CometChatNotificationFeed() +notificationFeed.set(title: "Activity") +notificationFeed.set(showBackButton: false) +notificationFeed.set(showFilterChips: true) +notificationFeed.set(cardThemeMode: "dark") +``` + + + +--- + +## Common Patterns + +### Show only unread items + + + +```swift +notificationFeed.set(notificationFeedRequestBuilder: + NotificationFeedRequest.NotificationFeedRequestBuilder() + .set(readState: .unread) +) +``` + + + +### Hide filter chips + + + +```swift +notificationFeed.set(showFilterChips: false) +``` + + + +### Custom categories request + + + +```swift +notificationFeed.set(notificationCategoriesRequestBuilder: + NotificationCategoriesRequest.NotificationCategoriesRequestBuilder() + .set(limit: 10) +) +``` + + + +### Embed in a Tab Bar + + + +```swift +let notificationFeed = CometChatNotificationFeed() +notificationFeed.set(showBackButton: false) +notificationFeed.tabBarItem = UITabBarItem( + title: "Notifications", + image: UIImage(systemName: "bell"), + selectedImage: UIImage(systemName: "bell.fill") +) + +// Add to tab bar controller +let navController = UINavigationController(rootViewController: notificationFeed) +tabBarController.viewControllers?.append(navController) +``` + + + +### Handle action + report engagement + + + +```swift +notificationFeed.set(onActionClick: { feedItem, actionEvent in + // Always report engagement + CometChat.reportFeedEngagement(feedItem, interactionString: actionEvent.elementId, onSuccess: {}, onError: { _ in }) + + switch actionEvent.action { + case .openUrl(let url, _): + if let link = URL(string: url) { + UIApplication.shared.open(link) + } + case .chatWithUser(let uid): + // Open CometChat messages with user + break + default: + break + } +}) +``` + + + +--- + +## Next Steps + +| Topic | Description | +| ----- | ----------- | +| [Campaigns Feature](/ui-kit/ios/campaigns) | Overview of how campaigns work end-to-end. | +| [SDK Campaigns API](/sdk/ios/campaigns) | Low-level SDK APIs for feed items, categories, and engagement. | From f1962abb14d30dbcf9c8c656044b3ca14ff24017 Mon Sep 17 00:00:00 2001 From: shagun-cometchat Date: Wed, 27 May 2026 20:40:37 +0530 Subject: [PATCH 26/45] docs(campaigns): add JavaScript SDK and React UI Kit documentation --- docs.json | 7 +- sdk/javascript/campaigns.mdx | 517 ++++++++++++++++++++ ui-kit/react/campaigns.mdx | 151 ++++++ ui-kit/react/notification-feed.mdx | 754 +++++++++++++++++++++++++++++ 4 files changed, 1427 insertions(+), 2 deletions(-) create mode 100644 sdk/javascript/campaigns.mdx create mode 100644 ui-kit/react/campaigns.mdx create mode 100644 ui-kit/react/notification-feed.mdx diff --git a/docs.json b/docs.json index 172825868..20262b9d1 100644 --- a/docs.json +++ b/docs.json @@ -509,7 +509,8 @@ "ui-kit/react/ai-features" ] }, - "ui-kit/react/call-features" + "ui-kit/react/call-features", + "ui-kit/react/campaigns" ] }, { @@ -541,7 +542,8 @@ "ui-kit/react/call-buttons", "ui-kit/react/call-logs", "ui-kit/react/search", - "ui-kit/react/ai-assistant-chat" + "ui-kit/react/ai-assistant-chat", + "ui-kit/react/notification-feed" ] }, { @@ -3057,6 +3059,7 @@ "sdk/javascript/ai-moderation", "sdk/javascript/ai-agents", "sdk/javascript/ai-copilot", + "sdk/javascript/campaigns", "sdk/javascript/webhooks", { "group": "Resources", diff --git a/sdk/javascript/campaigns.mdx b/sdk/javascript/campaigns.mdx new file mode 100644 index 000000000..dd57fd71c --- /dev/null +++ b/sdk/javascript/campaigns.mdx @@ -0,0 +1,517 @@ +--- +title: "Campaigns" +description: "Fetch notification feed items, listen for real-time delivery, mark items as read/delivered, report engagement, and track push notifications using the CometChat JavaScript SDK." +--- + +CometChat Campaigns lets you deliver targeted, rich notifications to users via an in-app notification feed. Each notification is a **Card Schema JSON** — a structured layout rendered natively by the CometChat Cards library. + +The SDK provides APIs to fetch feed items, listen for real-time delivery, mark items as read/delivered, report engagement, and retrieve unread counts. + +--- + +## Key Concepts + +| Concept | Description | +| --- | --- | +| **NotificationFeedItem** | A single notification in the feed. Contains Card Schema JSON in its `content` field, a `category` for filtering, timestamps, and metadata. | +| **NotificationCategory** | A category label used for filter chips (e.g., "Promotions", "Updates"). | +| **Card Schema JSON** | The fully rendered card layout (images, text, buttons) inside `NotificationFeedItem.getContent()`. Passed directly to the CometChat Cards renderer. | +| **PushNotification** | Represents a campaign push notification payload received via FCM/APNs or Web Push. | + +--- + +## How Cards Render in the Notification Feed + +Each `NotificationFeedItem` has a `content` field containing an object — this is the **Card Schema JSON**. This JSON is passed directly to the **CometChat Cards** renderer library (`@cometchat/cards-react`). + +The rendering flow: + +1. Fetch feed items via `NotificationFeedRequestBuilder` +2. For each item, extract `item.getContent()` — this is the Card Schema JSON +3. Convert to string: `JSON.stringify(item.getContent())` +4. Pass to the Cards renderer (`CometChatCardView`) +5. The renderer produces a native DOM element from the JSON + +### Card Schema JSON Structure + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://...", "height": 200 }, + { "type": "text", "id": "txt_1", "content": "Flash Sale!", "variant": "heading2" }, + { "type": "button", "id": "btn_1", "label": "Shop Now", "action": { "type": "openUrl", "url": "https://..." } } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale! Shop Now: https://..." +} +``` + +The `body` array contains elements (text, image, button, row, column, etc.) rendered top-to-bottom. Interactive elements like buttons emit actions via a callback — the consumer handles navigation, deep links, or API calls. + +--- + +## Retrieve Notification Feed Items + +Use `NotificationFeedRequestBuilder` to fetch a paginated list of feed items. Uses cursor-based pagination internally. + +### Build the Request + + + +```javascript +const request = new CometChat.NotificationFeedRequestBuilder() + .setLimit(20) + .build(); +``` + + +```typescript +const request: CometChat.NotificationFeedRequest = new CometChat.NotificationFeedRequestBuilder() + .setLimit(20) + .build(); +``` + + + +### Builder Parameters + +| Method | Type | Default | Description | +| --- | --- | --- | --- | +| `setLimit(limit)` | number | 20 | Items per page (max 100) | +| `setReadState(state)` | FeedReadState | `"all"` | Filter by `"read"`, `"unread"`, or `"all"` | +| `setCategory(category)` | string | null | Filter by category label | +| `setChannelId(channelId)` | string | null | Filter by channel | +| `setTags(tags)` | string[] | null | Filter by tags | +| `setDateFrom(date)` | string | null | ISO 8601 date — items sent on or after | +| `setDateTo(date)` | string | null | ISO 8601 date — items sent on or before | + +### Fetch Items + + + +```javascript +request.fetchNext().then( + (items) => { + for (const item of items) { + const cardJson = JSON.stringify(item.getContent()); + // Pass cardJson to CometChatCardView + } + }, + (error) => { + console.error("Feed fetch error:", error.message); + } +); +``` + + +```typescript +request.fetchNext().then( + (items: CometChat.NotificationFeedItem[]) => { + for (const item of items) { + const cardJson: string = JSON.stringify(item.getContent()); + // Pass cardJson to CometChatCardView + } + }, + (error: CometChat.CometChatException) => { + console.error("Feed fetch error:", error.message); + } +); +``` + + + +Call `fetchNext()` repeatedly for pagination. When the server has no more items, subsequent calls return an empty array. + +### NotificationFeedItem Fields + +| Field | Type | Description | +| --- | --- | --- | +| `getId()` | string | Unique item identifier | +| `getCategory()` | string | Notification category (e.g., "promotions") | +| `getContent()` | object | Card Schema JSON — pass to CometChat Cards renderer | +| `getReadAt()` | number \| null | Unix timestamp when read, or null if unread | +| `getDeliveredAt()` | number \| null | Unix timestamp when delivered, or null | +| `getSentAt()` | number | Unix timestamp when sent | +| `getMetadata()` | `Record` | Custom key-value metadata | +| `getTags()` | string[] | Tags for filtering | +| `getSender()` | string | Sender identifier | +| `getReceiver()` | string | Receiver identifier | +| `getReceiverType()` | string | Receiver type | +| `getIsRead()` | boolean | Whether the item has been read | + +--- + +## Retrieve Notification Categories + +Use `NotificationCategoriesRequestBuilder` to fetch available categories for filter chips. + + + +```javascript +const categoriesRequest = new CometChat.NotificationCategoriesRequestBuilder() + .setLimit(50) + .build(); + +categoriesRequest.fetchNext().then( + (categories) => { + for (const category of categories) { + console.log("Category:", category.getLabel()); + } + }, + (error) => { + console.error("Categories fetch error:", error.message); + } +); +``` + + +```typescript +const categoriesRequest: CometChat.NotificationCategoriesRequest = + new CometChat.NotificationCategoriesRequestBuilder() + .setLimit(50) + .build(); + +categoriesRequest.fetchNext().then( + (categories: CometChat.NotificationCategory[]) => { + for (const category of categories) { + console.log("Category:", category.getLabel()); + } + }, + (error: CometChat.CometChatException) => { + console.error("Categories fetch error:", error.message); + } +); +``` + + + +### NotificationCategory Fields + +| Field | Type | Description | +| --- | --- | --- | +| `getId()` | string | Category identifier | +| `getLabel()` | string | Display name for filter UI | + +--- + +## Real-Time Notification Feed Listener + +Listen for new feed items arriving via WebSocket. This listener is independent from `MessageListener`, `GroupListener`, and `CallListener`. + + + +```javascript +CometChat.addNotificationFeedListener("feedListener", { + onFeedItemReceived: (feedItem) => { + console.log("New item:", feedItem.getId()); + const cardJson = JSON.stringify(feedItem.getContent()); + // Insert at top of feed and render + }, +}); +``` + + +```typescript +CometChat.addNotificationFeedListener("feedListener", { + onFeedItemReceived: (feedItem: CometChat.NotificationFeedItem) => { + console.log("New item:", feedItem.getId()); + const cardJson: string = JSON.stringify(feedItem.getContent()); + // Insert at top of feed and render + }, +}); +``` + + + +Remove the listener when no longer needed: + +```javascript +CometChat.removeNotificationFeedListener("feedListener"); +``` + +--- + +## Mark Feed Item as Read + +Mark a single item as read. Idempotent — safe to call multiple times. + + + +```javascript +CometChat.markFeedItemAsRead(feedItem).then( + () => { console.log("Marked as read"); }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.markFeedItemAsRead(feedItem).then( + () => { console.log("Marked as read"); }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +--- + +## Mark Feed Item as Delivered + +Mark a single item as delivered. Idempotent. + + + +```javascript +CometChat.markFeedItemAsDelivered(feedItem).then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.markFeedItemAsDelivered(feedItem).then( + () => { /* Success */ }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +### Mark Multiple Items as Delivered (Batch) + +```javascript +CometChat.markFeedItemsAsDelivered(feedItems).then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + +--- + +## Report Engagement + +Report that a user engaged with a feed item (e.g., viewed, clicked, interacted). Idempotent. + + + +```javascript +CometChat.reportFeedEngagement(feedItem, "clicked").then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.reportFeedEngagement(feedItem, "clicked").then( + () => { /* Success */ }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +The `interactionString` parameter is a free-form string describing the engagement (e.g., `"viewed"`, `"clicked"`, `"interacted"`). + +--- + +## Get Unread Count + +Fetch the total number of unread notification feed items. + + + +```javascript +CometChat.getNotificationFeedUnreadCount().then( + (result) => { console.log("Unread:", result.count); }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.getNotificationFeedUnreadCount().then( + (result: { count: number }) => { console.log("Unread:", result.count); }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +--- + +## Fetch Single Feed Item + +Fetch a specific item by ID — useful for deep linking from push notifications. + + + +```javascript +CometChat.getNotificationFeedItem("item-id-123").then( + (item) => { + const cardJson = JSON.stringify(item.getContent()); + // Render the card + }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.getNotificationFeedItem("item-id-123").then( + (item: CometChat.NotificationFeedItem) => { + const cardJson: string = JSON.stringify(item.getContent()); + // Render the card + }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +--- + +## Push Notification Tracking + +When a campaign push notification arrives via Web Push or FCM, use these methods to report delivery and click engagement. + +### Mark Push Notification as Delivered + +Call this when the push notification is received: + + + +```javascript +const pushNotification = new CometChat.PushNotification(pushPayloadJson); + +CometChat.markPushNotificationDelivered(pushNotification).then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +const pushNotification = new CometChat.PushNotification(pushPayloadJson); + +CometChat.markPushNotificationDelivered(pushNotification).then( + () => { /* Success */ }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +### Mark Push Notification as Clicked + +Call this when the user taps the push notification: + + + +```javascript +CometChat.markPushNotificationClicked(pushNotification).then( + () => { /* Success */ }, + (error) => { console.error("Error:", error.message); } +); +``` + + +```typescript +CometChat.markPushNotificationClicked(pushNotification).then( + () => { /* Success */ }, + (error: CometChat.CometChatException) => { console.error("Error:", error.message); } +); +``` + + + +### PushNotification Fields + +| Field | Type | Description | +| --- | --- | --- | +| `getId()` | string | Announcement ID from the push payload | +| `getAnnouncementId()` | string | Same as id (for clarity) | +| `getCampaignId()` | string \| null | Campaign ID if from a campaign | +| `getSource()` | string | Always `"campaign"` for notification feed pushes | + +--- + +## FeedReadState + +| Value | Description | +| --- | --- | +| `"read"` | Only read items | +| `"unread"` | Only unread items | +| `"all"` | All items (default) | + +--- + +## Rendering Cards + +The `content` field of each `NotificationFeedItem` is a Card Schema JSON object. To render it natively, use the CometChat Cards library. + +### Add the Cards Dependency + +```bash +npm install @cometchat/cards-react +``` + + +If you're using `@cometchat/chat-uikit-react` v6.5.0+, the cards library is included automatically as a dependency. + + +### Render a Card from a Feed Item + +```tsx lines +import { CometChatCardView } from "@cometchat/cards-react"; + +function NotificationCard({ item }) { + const cardJson = JSON.stringify(item.getContent()); + + return ( + { + switch (event.type) { + case "openUrl": + // Open URL in browser + window.open(event.params.url, "_blank"); + break; + case "chatWithUser": + // Navigate to chat with event.params.uid + break; + case "chatWithGroup": + // Navigate to group chat with event.params.guid + break; + } + }} + /> + ); +} +``` + + +The Cards library is a pure renderer — it does not execute actions. Your code must handle action callbacks (opening URLs, navigating to chats, making API calls, etc.). + + +--- + +## Supported Card Actions + +When a user taps a button or link inside a card, the action callback receives one of these action types: + +| Action Type | Parameters | Description | +| --- | --- | --- | +| `openUrl` | url, openIn | Open a URL in browser or webview | +| `chatWithUser` | uid | Navigate to 1:1 chat | +| `chatWithGroup` | guid | Navigate to group chat | +| `sendMessage` | text, receiverUid, receiverGuid | Send a text message | +| `copyToClipboard` | value | Copy text to clipboard | +| `downloadFile` | url, filename | Download a file | +| `initiateCall` | callType (audio/video), uid, guid | Start a call | +| `apiCall` | url, method, headers, body | Make an HTTP request | +| `customCallback` | callbackId, payload | App-specific logic | diff --git a/ui-kit/react/campaigns.mdx b/ui-kit/react/campaigns.mdx new file mode 100644 index 000000000..923cd7480 --- /dev/null +++ b/ui-kit/react/campaigns.mdx @@ -0,0 +1,151 @@ +--- +title: "Campaigns" +description: "Deliver targeted, rich notifications to users via an in-app notification feed powered by the CometChat Cards renderer." +--- + +CometChat Campaigns enables you to send rich, interactive notifications to users through an in-app notification feed. Each notification is rendered as a native card using the **CometChat Cards** library — supporting images, text, buttons, layouts, and interactive actions. + +--- + +## Overview + +Campaigns delivers notifications as **Card Schema JSON** — a structured format that defines the visual layout of each notification card. The system consists of three layers: + +1. **CometChat Chat SDK** — Fetches feed items, manages read/delivered state, provides real-time listeners, handles push notification tracking +2. **CometChat Cards Library** (`@cometchat/cards-react`) — Renders Card Schema JSON into native DOM elements +3. **CometChat UI Kit** — Provides the ready-to-use `CometChatNotificationFeed` component that wires everything together + +### Architecture Flow + +``` +Dashboard / API → Campaign Created → Push + WebSocket Delivery + ↓ + SDK: NotificationFeedRequestBuilder.fetchNext() + ↓ + NotificationFeedItem.getContent() → Card Schema JSON + ↓ + Cards Library: CometChatCardView + ↓ + Native Rendered Card (images, text, buttons, layouts) + ↓ + User clicks button → onAction callback → Your code handles it +``` + +--- + +## How Cards Work + +Each `NotificationFeedItem` from the SDK contains a `content` field — an object holding the Card Schema JSON. This JSON is passed directly to the CometChat Cards renderer which produces a DOM element. + +The Cards library is a **pure renderer**: +- **Input**: Card Schema JSON string + theme mode + optional action callback +- **Output**: Native DOM element hierarchy + +It does not execute actions, manage message state, or call any SDK methods. When users click interactive elements (buttons, links), the library emits the action to your callback. You decide what happens — open a URL, navigate to a chat, make an API call, etc. + +### Card Schema JSON Example + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://cdn.example.com/sale.jpg", "height": 180, "fit": "cover", "borderRadius": 8 }, + { "type": "text", "id": "txt_1", "content": "🎉 Flash Sale — 40% Off!", "variant": "heading2" }, + { "type": "text", "id": "txt_2", "content": "Limited time offer on all premium plans.", "variant": "body" }, + { "type": "button", "id": "btn_1", "label": "Claim Offer", "action": { "type": "openUrl", "url": "https://example.com/offer" }, "fullWidth": true } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale — 40% Off! Claim your offer: https://example.com/offer" +} +``` + +The schema supports **20 element types** (text, image, icon, avatar, badge, divider, spacer, chip, progressBar, codeBlock, markdown, row, column, grid, accordion, tabs, button, iconButton, link, table) and **9 action types** (openUrl, chatWithUser, chatWithGroup, sendMessage, copyToClipboard, downloadFile, initiateCall, apiCall, customCallback). + +--- + +## How Cards Work in the UI Kit + +The `CometChatNotificationFeed` component uses the **CometChat Cards** library internally to render each notification. Here's what happens under the hood: + +1. The component fetches `NotificationFeedItem` objects from the SDK +2. For each item, it extracts the `content` field (Card Schema JSON) +3. It passes the JSON to `CometChatCardView` from `@cometchat/cards-react` +4. The Cards renderer produces native UI — text, images, buttons, layouts — directly from the JSON +5. When users click buttons/links inside a card, the action is emitted back to the component which handles navigation (open URL, navigate to chat, etc.) + +You don't need to interact with the Cards library directly when using `CometChatNotificationFeed` — it's all wired up. But if you want to render cards outside the feed (e.g., a standalone card in a modal), you can use the Cards library directly. See the [SDK Campaigns documentation](/sdk/javascript/campaigns#rendering-cards) for standalone usage. + +--- + +## Handling Push Notifications for Campaigns + +When a campaign push notification arrives via Web Push or FCM, you should: + +1. **Report delivery** — Call `CometChat.markPushNotificationDelivered()` when the notification is received +2. **Report click** — Call `CometChat.markPushNotificationClicked()` when the user clicks the notification +3. **Deep link** — Use the announcement ID from the push payload to fetch the full item via `CometChat.getNotificationFeedItem(id)` and display it + +```tsx lines +import { CometChat } from "@cometchat/chat-sdk-javascript"; + +// When push notification is received (e.g., in service worker) +const pushNotification = new CometChat.PushNotification(pushPayloadData); +CometChat.markPushNotificationDelivered(pushNotification); + +// When user clicks the notification +CometChat.markPushNotificationClicked(pushNotification); + +// Navigate to feed or show specific item +const item = await CometChat.getNotificationFeedItem(pushNotification.getId()); +``` + +See the [SDK Campaigns documentation](/sdk/javascript/campaigns) for the complete push notification tracking API. + +--- + +## Sending Campaigns + +Campaigns are created and managed from the **CometChat Dashboard** or via the **REST API**. The SDK and UI Kit are consumer-side — they display and interact with campaigns, not create them. + +To send campaigns: +- **Dashboard**: Navigate to Campaigns → Create Campaign → Define audience, content (Card Schema), and delivery channel +- **REST API**: Use the Campaigns API to programmatically create and schedule campaigns + +--- + +## Using the UI Kit Component + +The easiest way to add a notification feed to your app is the `CometChatNotificationFeed` component. It handles fetching, rendering, pagination, filtering, real-time updates, and engagement reporting out of the box. + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react"; +import "@cometchat/chat-uikit-react/css-variables.css"; + +function NotificationsScreen() { + return ( + { + // Handle item click + }} + onBackPress={() => { + // Navigate back + }} + /> + ); +} +``` + +See the full [CometChatNotificationFeed component documentation](/ui-kit/react/notification-feed) for all configuration options, styling, and customization. + +--- + +## Next Steps + + + + Full API reference for feed items, categories, engagement, and push tracking + + + Ready-to-use component with filtering, real-time updates, and styling + + diff --git a/ui-kit/react/notification-feed.mdx b/ui-kit/react/notification-feed.mdx new file mode 100644 index 000000000..443e52b6a --- /dev/null +++ b/ui-kit/react/notification-feed.mdx @@ -0,0 +1,754 @@ +--- +title: "Notification Feed" +description: "Full-screen notification feed component with category filtering, card rendering, real-time updates, and engagement reporting." +--- + + +```json +{ + "component": "CometChatNotificationFeed", + "package": "@cometchat/chat-uikit-react", + "import": "import { CometChatNotificationFeed } from \"@cometchat/chat-uikit-react\";", + "cssImport": "import \"@cometchat/chat-uikit-react/css-variables.css\";", + "description": "Full-screen notification feed with category filtering, timestamp grouping, card rendering via @cometchat/cards-react, real-time updates, and automatic engagement reporting.", + "cssRootClass": ".cometchat-notification-feed", + "props": { + "data": { + "title": { "type": "string", "default": "\"Notifications\"" }, + "scrollToItemId": { "type": "string", "default": "undefined", "note": "Deep link to a specific feed item" }, + "notificationFeedRequestBuilder": { "type": "NotificationFeedRequestBuilder", "default": "SDK default (20 per page)" }, + "notificationCategoriesRequestBuilder": { "type": "NotificationCategoriesRequestBuilder", "default": "SDK default (50 per page)" } + }, + "callbacks": { + "onItemClick": "(feedItem: NotificationFeedItem) => void", + "onActionClick": "(feedItem: NotificationFeedItem, action: CardAction) => void", + "onError": "(error: CometChat.CometChatException) => void", + "onBackPress": "() => void" + }, + "visibility": { + "showHeader": { "type": "boolean", "default": true }, + "showBackButton": { "type": "boolean", "default": false }, + "showFilterChips": { "type": "boolean", "default": true } + }, + "viewSlots": { + "headerView": "React.ReactNode", + "emptyStateView": "React.ReactNode", + "errorStateView": "React.ReactNode", + "loadingStateView": "React.ReactNode" + }, + "cards": { + "cardThemeMode": { "type": "\"auto\" | \"light\" | \"dark\"", "default": "\"auto\"" }, + "cardThemeOverride": { "type": "Record", "default": "undefined" } + }, + "style": { + "type": "CometChatNotificationFeedStyle", + "properties": { + "backgroundColor": "string", + "width": "string", + "height": "string", + "headerTitleColor": "string", + "headerTitleFont": "string", + "chipActiveBackgroundColor": "string", + "chipActiveTextColor": "string", + "chipInactiveBackgroundColor": "string", + "chipInactiveTextColor": "string", + "chipBorderColor": "string", + "badgeBackgroundColor": "string", + "badgeTextColor": "string", + "separatorColor": "string", + "timestampTextColor": "string", + "timestampFont": "string", + "cardBackgroundColor": "string", + "cardBorderColor": "string", + "cardBorderRadius": "number", + "cardBorderWidth": "number", + "unreadIndicatorColor": "string" + } + } + }, + "automaticBehaviors": [ + "Real-time updates via WebSocket listener", + "Delivery reporting on fetch", + "Read reporting on viewport visibility (IntersectionObserver)", + "Unread count polling every 30 seconds", + "Infinite scroll pagination", + "Timestamp grouping (Today, Yesterday, day name, date)", + "Category filter chips with unread badges", + "Mark all read button", + "Offline connectivity banner" + ], + "additionalExports": { + "CometChatNotificationBadge": "Standalone unread count badge component", + "useNotificationUnreadCount": "Hook for tracking unread count with shared polling" + } +} +``` + + +`CometChatNotificationFeed` displays a scrollable notification feed where each item is rendered as a card using `@cometchat/cards-react`. It handles fetching, pagination, category filtering, timestamp grouping, real-time updates, and read/delivered/engagement reporting automatically. + +--- + +## Where It Fits + +`CometChatNotificationFeed` is a full-screen component. Drop it into a page or route. It manages its own data fetching, state, and real-time listeners — you just handle navigation callbacks. + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react"; +import "@cometchat/chat-uikit-react/css-variables.css"; + +function NotificationsPage() { + return ( + window.history.back()} + onItemClick={(item) => { + // Handle item click (e.g., open detail or deep link) + }} + /> + ); +} +``` + +--- + +## Minimal Render + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react"; +import "@cometchat/chat-uikit-react/css-variables.css"; + +function NotificationsDemo() { + return ( +
+ +
+ ); +} + +export default NotificationsDemo; +``` + +Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()` and a user logged in. + +Root CSS class: `.cometchat-notification-feed` + +--- + +## Filtering Feed Items + +Control what loads using custom request builders: + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react"; +import { CometChat } from "@cometchat/chat-sdk-javascript"; + +function UnreadNotifications() { + return ( + + ); +} +``` + +### Filter Options + +| Builder Method | Description | +| --- | --- | +| `.setLimit(number)` | Items per page (default 20, max 100) | +| `.setReadState(state)` | `"read"`, `"unread"`, or `"all"` | +| `.setCategory(string)` | Filter by category label | +| `.setChannelId(string)` | Filter by channel | +| `.setTags(string[])` | Filter by tags | +| `.setDateFrom(string)` | ISO 8601 date lower bound | +| `.setDateTo(string)` | ISO 8601 date upper bound | + +--- + +## Actions and Events + +### Callback Props + +#### onItemClick + +Fires when a feed item card is clicked. + +```tsx lines + { + console.log("Item clicked:", item.getId()); + }} +/> +``` + +#### onActionClick + +Fires when an interactive element (button, link) inside a card is clicked. The `action` object contains the action type, parameters, and the element ID that triggered it. + +```tsx lines + { + const { type, params, elementId } = action; + switch (type) { + case "openUrl": + window.open(params.url, "_blank"); + break; + case "chatWithUser": + // Navigate to chat with params.uid + break; + case "chatWithGroup": + // Navigate to group chat with params.guid + break; + } + }} +/> +``` + +#### onError + +Fires when an internal error occurs (network failure, SDK exception). + +```tsx lines + { + console.error("Feed error:", error.message); + }} +/> +``` + +#### onBackPress + +Fires when the back button in the header is clicked. + +```tsx lines + window.history.back()} +/> +``` + +### Automatic Behaviors + +The component handles these automatically — no manual setup needed: + +| Behavior | Description | +| --- | --- | +| Real-time updates | New items appear at the top via WebSocket `NotificationFeedListener` | +| Delivery reporting | Items are reported as delivered when fetched | +| Read reporting | Items are reported as read when visible in viewport (IntersectionObserver) | +| Unread count polling | Polls unread count every 30 seconds to update badges | +| Infinite scroll | Fetches next page when scrolling near the bottom | +| Timestamp grouping | Groups items as "Today", "Yesterday", day name, or date | +| Category filtering | Filter chips row with per-category unread badges | +| Mark all read | Header button to mark all notifications as read | +| Offline banner | Shows connectivity warning when offline | + +--- + +## Custom View Slots + +### headerView + +Replace the entire header: + +```tsx lines + +

My Notifications

+ + } +/> +``` + +### State Views + +```tsx lines + +

No notifications yet

+ + } + errorStateView={ +
+

Something went wrong

+
+ } + loadingStateView={ +
+
+
+ } +/> +``` + +--- + +## Styling + +The component uses CSS variables from the CometChat theme system. Pass a `style` prop for programmatic overrides, or use CSS classes for full control. + +### Style Prop + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react"; + +function StyledNotifications() { + return ( + + ); +} +``` + +### Style Properties + +| Property | Type | Description | +| --- | --- | --- | +| `backgroundColor` | string | Root container background | +| `width` | string | Container width | +| `height` | string | Container height | +| `headerTitleColor` | string | Header title text color | +| `headerTitleFont` | string | Header title font | +| `chipActiveBackgroundColor` | string | Selected filter chip background | +| `chipActiveTextColor` | string | Selected filter chip text color | +| `chipInactiveBackgroundColor` | string | Unselected filter chip background | +| `chipInactiveTextColor` | string | Unselected filter chip text color | +| `chipBorderColor` | string | Inactive chip border color | +| `badgeBackgroundColor` | string | Badge background color | +| `badgeTextColor` | string | Badge text color | +| `separatorColor` | string | Separator between items | +| `timestampTextColor` | string | Timestamp text color | +| `timestampFont` | string | Timestamp font | +| `cardBackgroundColor` | string | Card container background | +| `cardBorderColor` | string | Card border color | +| `cardBorderRadius` | number | Card corner radius | +| `cardBorderWidth` | number | Card border width | +| `unreadIndicatorColor` | string | Unread dot color | + +### CSS Classes + +Override styles using these CSS classes: + +| Class | Description | +| --- | --- | +| `.cometchat-notification-feed` | Root container | +| `.cometchat-notification-feed__header` | Header bar | +| `.cometchat-notification-feed__header-title` | Header title text | +| `.cometchat-notification-feed__header-back` | Back button | +| `.cometchat-notification-feed__mark-all-read` | Mark all read button | +| `.cometchat-notification-feed__chips` | Filter chips container | +| `.cometchat-notification-feed__chip` | Individual filter chip | +| `.cometchat-notification-feed__chip--active` | Active filter chip | +| `.cometchat-notification-feed__chip--inactive` | Inactive filter chip | +| `.cometchat-notification-feed__chip--inactive-with-badge` | Inactive chip with unread badge | +| `.cometchat-notification-feed__chip-badge` | Chip unread badge | +| `.cometchat-notification-feed__chip-badge--active` | Active chip badge | +| `.cometchat-notification-feed__chip-badge--inactive` | Inactive chip badge | +| `.cometchat-notification-feed__content` | Scrollable content area | +| `.cometchat-notification-feed__item` | Feed item container | +| `.cometchat-notification-feed__item--unread` | Unread feed item | +| `.cometchat-notification-feed__unread-indicator` | Unread dot indicator | +| `.cometchat-notification-feed__item-meta` | Item metadata row (category + time) | +| `.cometchat-notification-feed__item-category` | Category label | +| `.cometchat-notification-feed__item-time` | Timestamp | +| `.cometchat-notification-feed__card-container` | Card wrapper | +| `.cometchat-notification-feed__loading` | Loading state | +| `.cometchat-notification-feed__empty` | Empty state | +| `.cometchat-notification-feed__error` | Error state | +| `.cometchat-notification-feed__connectivity-banner` | Offline banner | + +--- + +## Props + +All props are optional. + +### cardThemeMode + +Theme mode for the card renderer (`@cometchat/cards-react`). + +| | | +| --- | --- | +| Type | `"auto" \| "light" \| "dark"` | +| Default | `"auto"` | + +--- + +### cardThemeOverride + +Custom theme override passed to the card renderer. + +| | | +| --- | --- | +| Type | `Record` | +| Default | `undefined` | + +--- + +### emptyStateView + +Custom component displayed when there are no notifications. + +| | | +| --- | --- | +| Type | `React.ReactNode` | +| Default | Built-in empty state with illustration | + +--- + +### errorStateView + +Custom component displayed when an error occurs. + +| | | +| --- | --- | +| Type | `React.ReactNode` | +| Default | Built-in error state with retry button | + +--- + +### headerView + +Custom component replacing the entire header. + +| | | +| --- | --- | +| Type | `React.ReactNode` | +| Default | Built-in header with title and mark-all-read button | + +--- + +### loadingStateView + +Custom component displayed during the initial loading state. + +| | | +| --- | --- | +| Type | `React.ReactNode` | +| Default | Built-in loading spinner | + +--- + +### notificationCategoriesRequestBuilder + +Custom request builder for fetching categories. + +| | | +| --- | --- | +| Type | `CometChat.NotificationCategoriesRequestBuilder` | +| Default | SDK default (50 per page) | + +--- + +### notificationFeedRequestBuilder + +Custom request builder for fetching feed items. + +| | | +| --- | --- | +| Type | `CometChat.NotificationFeedRequestBuilder` | +| Default | SDK default (20 per page) | + +--- + +### onActionClick + +Callback fired when an interactive element inside a card is clicked. + +| | | +| --- | --- | +| Type | `(feedItem: NotificationFeedItem, action: CardAction) => void` | +| Default | `undefined` | + +The `CardAction` object contains: +- `type` — Action type (e.g., `"openUrl"`, `"chatWithUser"`) +- `params` — Action parameters (e.g., `{ url: "..." }`, `{ uid: "..." }`) +- `elementId` — ID of the element that triggered the action + +--- + +### onBackPress + +Callback fired when the back button is pressed. + +| | | +| --- | --- | +| Type | `() => void` | +| Default | `undefined` | + +--- + +### onError + +Callback fired when the component encounters an error. + +| | | +| --- | --- | +| Type | `(error: CometChat.CometChatException) => void` | +| Default | `undefined` | + +--- + +### onItemClick + +Callback fired when a feed item card is clicked. + +| | | +| --- | --- | +| Type | `(feedItem: NotificationFeedItem) => void` | +| Default | `undefined` | + +--- + +### scrollToItemId + +Deep link to a specific feed item by ID. The component scrolls to the item once loaded. + +| | | +| --- | --- | +| Type | `string` | +| Default | `undefined` | + +--- + +### showBackButton + +Shows/hides the back button in the header. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### showFilterChips + +Shows/hides the category filter chips row. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `true` | + +--- + +### showHeader + +Shows/hides the entire header. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `true` | + +--- + +### style + +Style customization object. + +| | | +| --- | --- | +| Type | `CometChatNotificationFeedStyle` | +| Default | `undefined` | + +--- + +### title + +Header title text. + +| | | +| --- | --- | +| Type | `string` | +| Default | `"Notifications"` | + +--- + +## Additional Exports + +### CometChatNotificationBadge + +A standalone badge component that displays the unread notification count. Uses a shared polling singleton — multiple badges share one interval and listener. + +```tsx lines +import { CometChatNotificationBadge } from "@cometchat/chat-uikit-react"; + +function NavBar() { + return ( + + ); +} +``` + +#### Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `category` | string | undefined | Filter count by category | +| `max` | number | 99 | Maximum count before showing "N+" | +| `style.backgroundColor` | string | `"#6852D6"` | Badge background color | +| `style.textColor` | string | `"#fff"` | Badge text color | +| `style.fontSize` | string | `"11px"` | Badge font size | +| `style.borderRadius` | string | `"9999px"` | Badge border radius | + +--- + +### useNotificationUnreadCount + +A React hook for tracking unread notification count. Uses a shared singleton — multiple components share one polling interval and WebSocket listener. + +```tsx lines +import { useNotificationUnreadCount } from "@cometchat/chat-uikit-react"; + +function NotificationIcon() { + const { count, refresh, isLoading } = useNotificationUnreadCount(); + + return ( + + ); +} +``` + +#### Return Value + +| Field | Type | Description | +| --- | --- | --- | +| `count` | number | Current unread count | +| `refresh` | `() => Promise` | Manually refresh the count | +| `isLoading` | boolean | Whether the initial fetch is in progress | + +#### Options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `category` | string | undefined | Filter count by category | +| `pollingInterval` | number | 30000 | Polling interval in milliseconds | + +--- + +## Common Patterns + +### Show only unread items + +```tsx lines + +``` + +### Hide filter chips and header + +```tsx lines + +``` + +### Deep link to a specific notification + +```tsx lines + +``` + +### Embed in a sidebar + +```tsx lines +import { CometChatNotificationFeed } from "@cometchat/chat-uikit-react"; +import "@cometchat/chat-uikit-react/css-variables.css"; + +function NotificationsSidebar() { + return ( +
+ +
+ ); +} +``` + +### Add notification badge to navigation + +```tsx lines +import { + CometChatNotificationFeed, + CometChatNotificationBadge, +} from "@cometchat/chat-uikit-react"; +import "@cometchat/chat-uikit-react/css-variables.css"; + +function App() { + const [showFeed, setShowFeed] = useState(false); + + return ( + <> + + {showFeed && ( + setShowFeed(false)} + showBackButton={true} + /> + )} + + ); +} +``` + +--- + +## Next Steps + + + + Overview of how campaigns work end-to-end + + + Low-level SDK APIs for feed items, categories, and engagement + + + Customize colors, fonts, and appearance + + + Browse all prebuilt UI components + + From 829fc5440cf21cae1b693a318c2bb5f04d08791b Mon Sep 17 00:00:00 2001 From: "Raj Shah(CometChat)" Date: Wed, 27 May 2026 20:44:33 +0530 Subject: [PATCH 27/45] Added Campaigns sdk and uikit docs --- docs.json | 12 +- sdk/flutter/campaigns.mdx | 428 ++++++++++++++++++++++++++ ui-kit/flutter/campaigns.mdx | 166 ++++++++++ ui-kit/flutter/notification-feed.mdx | 409 ++++++++++++++++++++++++ ui-kit/flutter/v5/getting-started.mdx | 4 +- 5 files changed, 1015 insertions(+), 4 deletions(-) create mode 100644 sdk/flutter/campaigns.mdx create mode 100644 ui-kit/flutter/campaigns.mdx create mode 100644 ui-kit/flutter/notification-feed.mdx diff --git a/docs.json b/docs.json index 172825868..09c96b75e 100644 --- a/docs.json +++ b/docs.json @@ -2032,7 +2032,8 @@ "ui-kit/flutter/extensions" ] }, - "ui-kit/flutter/call-features" + "ui-kit/flutter/call-features", + "ui-kit/flutter/campaigns" ] }, { @@ -2062,7 +2063,8 @@ "ui-kit/flutter/outgoing-call", "ui-kit/flutter/call-buttons", "ui-kit/flutter/call-logs", - "ui-kit/flutter/search" + "ui-kit/flutter/search", + "ui-kit/flutter/notification-feed" ] }, { @@ -4389,6 +4391,12 @@ "sdk/flutter/reactions" ] }, + { + "group": "Campaigns", + "pages": [ + "sdk/flutter/campaigns" + ] + }, { "group": "Users", "pages": [ diff --git a/sdk/flutter/campaigns.mdx b/sdk/flutter/campaigns.mdx new file mode 100644 index 000000000..f80c53a9f --- /dev/null +++ b/sdk/flutter/campaigns.mdx @@ -0,0 +1,428 @@ +--- +title: "Campaigns" +description: "Fetch notification feed items, listen for real-time delivery, mark items as read/delivered, report engagement, and retrieve unread counts in Flutter." +--- + +CometChat Campaigns lets you deliver targeted, rich notifications to users via an in-app notification feed. Each notification is a **Card Schema JSON** — a structured layout rendered natively by the CometChat Cards library. + +The SDK provides APIs to fetch feed items, listen for real-time delivery, mark items as read/delivered, report engagement, and retrieve unread counts. + +--- + +## Key Concepts + +| Concept | Description | +| --- | --- | +| **NotificationFeedItem** | A single notification in the feed. Contains Card Schema JSON in its `content` field, a `category` for filtering, timestamps, and metadata. | +| **NotificationCategory** | A category label used for filter chips (e.g., "Promotions", "Updates"). | +| **Card Schema JSON** | The fully rendered card layout (images, text, buttons) inside `NotificationFeedItem.content`. Passed directly to the CometChat Cards renderer. | +| **PushNotification** | Represents a campaign push notification payload received via FCM/APNs. | + +--- + +## How Cards Render in the Notification Feed + +Each `NotificationFeedItem` has a `content` field containing a `Map` — this is the **Card Schema JSON**. This JSON is passed directly to the **CometChat Cards** renderer package (`cometchat_cards`). + +The rendering flow: + +1. Fetch feed items via `NotificationFeedRequest` +2. For each item, extract `item.content` — this is the Card Schema JSON +3. Convert to string: `jsonEncode(item.content)` +4. Pass to the Cards renderer (`CometChatCardView`) +5. The renderer produces a native Flutter widget from the JSON + +### Card Schema JSON Structure + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://...", "height": 200 }, + { "type": "text", "id": "txt_1", "content": "Flash Sale!", "variant": "heading2" }, + { "type": "button", "id": "btn_1", "label": "Shop Now", "action": { "type": "openUrl", "url": "https://..." } } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale! Shop Now: https://..." +} +``` + +The `body` array contains elements (text, image, button, row, column, etc.) rendered top-to-bottom. Interactive elements like buttons emit actions via a callback — the consumer handles navigation, deep links, or API calls. + +--- + +## Retrieve Notification Feed Items + +Use `NotificationFeedRequest` to fetch a paginated list of feed items. Uses cursor-based pagination internally. + +### Build the Request + + + +```dart +final request = (NotificationFeedRequestBuilder() + ..setLimit(20)) + .build(); +``` + + + +### Builder Parameters + +| Method | Type | Default | Description | +| --- | --- | --- | --- | +| `setLimit(int)` | int | 20 | Items per page (max 100) | +| `setReadState(FeedReadState)` | enum | `FeedReadState.all` | Filter by `read`, `unread`, or `all` | +| `setCategory(String)` | String | null | Filter by category ID | +| `setChannelId(String)` | String | null | Filter by channel | +| `setTags(List)` | List | null | Filter by tags | +| `setDateFrom(String)` | String | null | ISO 8601 date — items sent on or after | +| `setDateTo(String)` | String | null | ISO 8601 date — items sent on or before | + +### Fetch Items + + + +```dart +request.fetchNext( + onSuccess: (List items) { + for (final item in items) { + final cardJson = jsonEncode(item.content); + // Pass cardJson to CometChatCardView + } + }, + onError: (CometChatException e) { + debugPrint("Error: ${e.message}"); + }, +); +``` + + + +Call `fetchNext()` repeatedly for pagination. When the server has no more items, subsequent calls return an empty list. + +### NotificationFeedItem Fields + +| Field | Type | Description | +| --- | --- | --- | +| `id` | String | Unique item identifier | +| `category` | String | Notification category (e.g., "promotions") | +| `categoryId` | String? | Category ID for programmatic filtering | +| `content` | Map\ | Card Schema JSON — pass to CometChat Cards renderer | +| `readAt` | int? | Unix timestamp when read, or null if unread | +| `deliveredAt` | int? | Unix timestamp when delivered, or null | +| `sentAt` | int | Unix timestamp when sent | +| `metadata` | Map\ | Custom key-value metadata | +| `tags` | List\ | Tags for filtering | +| `sender` | String | Sender identifier | +| `receiver` | String | Receiver identifier | +| `receiverType` | String | Receiver type | +| `isRead` | bool | Computed property — `true` if `readAt != null` | + +--- + +## Retrieve Notification Categories + +Use `NotificationCategoriesRequest` to fetch available categories for filter chips. + + + +```dart +final categoriesRequest = (NotificationCategoriesRequestBuilder() + ..setLimit(50)) + .build(); + +categoriesRequest.fetchNext( + onSuccess: (List categories) { + for (final category in categories) { + debugPrint("Category: ${category.name}"); + } + }, + onError: (CometChatException e) { + debugPrint("Error: ${e.message}"); + }, +); +``` + + + +### NotificationCategory Fields + +| Field | Type | Description | +| --- | --- | --- | +| `id` | String | Category identifier | +| `name` | String | Display name for filter UI | +| `description` | String | Category description | +| `appId` | String | Associated app ID | + +--- + +## Real-Time Notification Feed Listener + +Listen for new feed items arriving via WebSocket. This listener is independent from `MessageListener`, `GroupListener`, and `CallListener`. + + + +```dart +class MyNotificationFeedListener with NotificationFeedListener { + @override + void onFeedItemReceived(NotificationFeedItem feedItem) { + debugPrint("New item: ${feedItem.id}"); + final cardJson = jsonEncode(feedItem.content); + // Insert at top of feed and render + } +} + +CometChat.addNotificationFeedListener( + "feedListener", + MyNotificationFeedListener(), +); +``` + + + +Remove the listener when no longer needed: + + + +```dart +CometChat.removeNotificationFeedListener("feedListener"); +``` + + + +--- + +## Mark Feed Item as Read + +Mark a single item as read. Idempotent — safe to call multiple times. + + + +```dart +CometChat.markFeedItemAsRead( + feedItem, + onSuccess: (result) { + debugPrint("Marked as read"); + }, + onError: (CometChatException e) { + debugPrint("Error: ${e.message}"); + }, +); +``` + + + +--- + +## Mark Feed Item as Delivered + +Mark a single item as delivered. Idempotent. + + + +```dart +CometChat.markFeedItemAsDelivered( + feedItem, + onSuccess: (result) { + // Success + }, + onError: (CometChatException e) { + debugPrint("Error: ${e.message}"); + }, +); +``` + + + +--- + +## Report Engagement + +Report that a user engaged with a feed item (e.g., viewed, clicked, interacted). Idempotent. + + + +```dart +CometChat.reportFeedEngagement( + feedItem, + "clicked", + onSuccess: (result) {}, + onError: (CometChatException e) {}, +); +``` + + + +The `interactionString` parameter is a free-form string describing the engagement (e.g., `"viewed"`, `"clicked"`, `"interacted"`). + +--- + +## Get Unread Count + +Fetch the total number of unread notification feed items. + + + +```dart +CometChat.getNotificationFeedUnreadCount( + onSuccess: (int count) { + debugPrint("Unread: $count"); + }, + onError: (CometChatException e) { + debugPrint("Error: ${e.message}"); + }, +); +``` + + + +--- + +## Fetch Single Feed Item + +Fetch a specific item by ID — useful for deep linking from push notifications. + + + +```dart +CometChat.getNotificationFeedItem( + "item-id-123", + onSuccess: (NotificationFeedItem item) { + final cardJson = jsonEncode(item.content); + // Render the card + }, + onError: (CometChatException e) { + debugPrint("Error: ${e.message}"); + }, +); +``` + + + +--- + +## Push Notification Tracking + +When a campaign push notification arrives via FCM/APNs, use these methods to report delivery and click engagement. + +### Mark Push Notification as Delivered + +Call this when you receive the push notification payload: + + + +```dart +final pushNotification = PushNotification( + id: pushPayload['announcementId'], + announcementId: pushPayload['announcementId'], + campaignId: pushPayload['campaignId'], + source: "campaign", +); + +CometChat.markPushNotificationDelivered( + pushNotification, + onSuccess: (result) {}, + onError: (CometChatException e) {}, +); +``` + + + +### Mark Push Notification as Clicked + +Call this when the user taps the push notification: + + + +```dart +CometChat.markPushNotificationClicked( + pushNotification, + onSuccess: (result) {}, + onError: (CometChatException e) {}, +); +``` + + + +### PushNotification Fields + +| Field | Type | Description | +| --- | --- | --- | +| `id` | String | Announcement ID from the push payload | +| `announcementId` | String | Same as id (for clarity) | +| `campaignId` | String? | Campaign ID if from a campaign | +| `source` | String | Always `"campaign"` for notification feed pushes | + +--- + +## FeedReadState Enum + +| Value | Description | +| --- | --- | +| `FeedReadState.read` | Only read items | +| `FeedReadState.unread` | Only unread items | +| `FeedReadState.all` | All items (default) | + +--- + +## Rendering Cards + +The `content` field of each `NotificationFeedItem` is a Card Schema JSON map. To render it natively, use the CometChat Cards library. + +### Add the Cards Dependency + +Add the cards package to your `pubspec.yaml`: + +```yaml +dependencies: + cometchat_cards: ^1.0.0 +``` + +### Render a Card from a Feed Item + + + +```dart +import 'package:cometchat_cards/cometchat_cards.dart'; +import 'dart:convert'; + +Widget buildNotificationCard(NotificationFeedItem item) { + return CometChatCardView( + cardJson: jsonEncode(item.content), + themeMode: CometChatCardThemeMode.auto, + onAction: (CometChatCardActionEvent event) { + // Handle action: event.action, event.elementId + if (event.action is CometChatCardOpenUrlAction) { + // Open URL in browser + } else if (event.action is CometChatCardChatWithUserAction) { + // Navigate to chat + } + }, + ); +} +``` + + + + +The Cards library is a pure renderer — it does not execute actions. Your code must handle action callbacks (opening URLs, navigating to chats, making API calls, etc.). + + +--- + +## Supported Card Actions + +When a user taps a button or link inside a card, the action callback receives one of these action types: + +| Action Type | Parameters | Description | +| --- | --- | --- | +| `openUrl` | url, openIn | Open a URL in browser or webview | +| `chatWithUser` | uid | Navigate to 1:1 chat | +| `chatWithGroup` | guid | Navigate to group chat | +| `sendMessage` | text, receiverUid, receiverGuid | Send a text message | +| `copyToClipboard` | value | Copy text to clipboard | +| `downloadFile` | url, filename | Download a file | +| `initiateCall` | callType (audio/video), uid, guid | Start a call | +| `apiCall` | url, method, headers, body | Make an HTTP request | +| `customCallback` | callbackId, payload | App-specific logic | diff --git a/ui-kit/flutter/campaigns.mdx b/ui-kit/flutter/campaigns.mdx new file mode 100644 index 000000000..58f6dd004 --- /dev/null +++ b/ui-kit/flutter/campaigns.mdx @@ -0,0 +1,166 @@ +--- +title: "Campaigns" +description: "Deliver targeted, rich notifications to users via an in-app notification feed powered by the CometChat Cards renderer." +--- + +CometChat Campaigns enables you to send rich, interactive notifications to users through an in-app notification feed. Each notification is rendered as a native card using the **CometChat Cards** library — supporting images, text, buttons, layouts, and interactive actions. + + + + + +--- + +## Overview + +Campaigns delivers notifications as **Card Schema JSON** — a structured format that defines the visual layout of each notification card. The system consists of three layers: + +1. **CometChat Chat SDK** — Fetches feed items, manages read/delivered state, provides real-time listeners, handles push notification tracking +2. **CometChat Cards Library** — Renders Card Schema JSON into native Flutter widgets +3. **CometChat UI Kit** — Provides the ready-to-use `CometChatNotificationFeed` component that wires everything together + +### Architecture Flow + +``` +Dashboard / API → Campaign Created → Push + WebSocket Delivery + ↓ + SDK: NotificationFeedRequest.fetchNext() + ↓ + NotificationFeedItem.content → Card Schema JSON + ↓ + Cards Library: CometChatCardView + ↓ + Native Rendered Card (images, text, buttons, layouts) + ↓ + User taps button → ActionCallback → Your code handles it +``` + +--- + +## How Cards Work + +Each `NotificationFeedItem` from the SDK contains a `content` field — a `Map` holding the Card Schema JSON. This JSON is passed directly to the CometChat Cards renderer which produces a native Flutter widget. + +The Cards library is a **pure renderer**: +- **Input**: Card Schema JSON string + theme mode + optional action callback +- **Output**: Flutter widget tree + +It does not execute actions, manage message state, or call any SDK methods. When users tap interactive elements (buttons, links), the library emits the action to your callback. You decide what happens — open a URL, navigate to a chat, make an API call, etc. + +### Card Schema JSON Example + +```json +{ + "version": "1.0", + "body": [ + { "type": "image", "id": "img_1", "url": "https://cdn.example.com/sale.jpg", "height": 180, "fit": "cover", "borderRadius": 8 }, + { "type": "text", "id": "txt_1", "content": "🎉 Flash Sale — 40% Off!", "variant": "heading2" }, + { "type": "text", "id": "txt_2", "content": "Limited time offer on all premium plans.", "variant": "body" }, + { "type": "button", "id": "btn_1", "label": "Claim Offer", "action": { "type": "openUrl", "url": "https://example.com/offer" }, "fullWidth": true } + ], + "style": { "background": {"light": "#FFFFFF", "dark": "#1E1E1E"}, "borderRadius": 12, "padding": 16 }, + "fallbackText": "Flash Sale — 40% Off! Claim your offer: https://example.com/offer" +} +``` + +The schema supports **20 element types** (text, image, icon, avatar, badge, divider, spacer, chip, progressBar, codeBlock, markdown, row, column, grid, accordion, tabs, button, iconButton, link, table) and **9 action types** (openUrl, chatWithUser, chatWithGroup, sendMessage, copyToClipboard, downloadFile, initiateCall, apiCall, customCallback). + +--- + +## How Cards Work in the UI Kit + +The `CometChatNotificationFeed` component uses the **CometChat Cards** library internally to render each notification. Here's what happens under the hood: + +1. The component fetches `NotificationFeedItem` objects from the SDK +2. For each item, it extracts the `content` field (Card Schema JSON) +3. It passes the JSON to `CometChatCardView` from the Cards library +4. The Cards renderer produces native Flutter widgets — text, images, buttons, layouts — directly from the JSON +5. When users tap buttons/links inside a card, the action is emitted back to the component which handles navigation (open URL, navigate to chat, etc.) + +You don't need to interact with the Cards library directly when using `CometChatNotificationFeed` — it's all wired up. But if you want to render cards outside the feed (e.g., a standalone card in a dialog), you can use the Cards library directly. See the [SDK Campaigns documentation](/sdk/flutter/campaigns#rendering-cards) for standalone usage. + +--- + +## Handling Push Notifications for Campaigns + +When a campaign push notification arrives via FCM/APNs, you should: + +1. **Report delivery** — Call `CometChat.markPushNotificationDelivered()` in your push notification handler +2. **Report click** — Call `CometChat.markPushNotificationClicked()` when the user taps the notification +3. **Deep link** — Use the announcement ID from the push payload to fetch the full item via `CometChat.getNotificationFeedItem(id)` and display it + +```dart +// When push notification is received +final pushNotification = PushNotification( + id: payload['announcementId'], + announcementId: payload['announcementId'], + campaignId: payload['campaignId'], + source: "campaign", +); +CometChat.markPushNotificationDelivered(pushNotification, onSuccess: (_) {}, onError: (_) {}); + +// When user taps the notification +CometChat.markPushNotificationClicked(pushNotification, onSuccess: (_) {}, onError: (_) {}); + +// Navigate to feed or show specific item +CometChat.getNotificationFeedItem( + pushNotification.id, + onSuccess: (item) { /* render card */ }, + onError: (e) { /* handle error */ }, +); +``` + +See the [SDK Campaigns documentation](/sdk/flutter/campaigns) for the complete push notification tracking API. + +--- + +## Sending Campaigns + +Campaigns are created and managed from the **CometChat Dashboard** or via the **REST API**. The SDK and UI Kit are consumer-side — they display and interact with campaigns, not create them. + +To send campaigns: +- **Dashboard**: Navigate to Campaigns → Create Campaign → Define audience, content (Card Schema), and delivery channel +- **REST API**: Use the Campaigns API to programmatically create and schedule campaigns + +--- + +## Using the UI Kit Component + +The easiest way to add a notification feed to your app is the `CometChatNotificationFeed` component. It handles fetching, rendering, pagination, filtering, real-time updates, and engagement reporting out of the box. + + + +```dart +import 'package:cometchat_chat_uikit/cometchat_chat_uikit.dart'; + +class NotificationsScreen extends StatelessWidget { + const NotificationsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return CometChatNotificationFeed( + onItemClick: (NotificationFeedItem item) { + // Handle item tap + }, + onBackPress: () => Navigator.of(context).pop(), + ); + } +} +``` + + + +See the full [CometChatNotificationFeed component documentation](/ui-kit/flutter/notification-feed) for all configuration options, styling, and customization. + +--- + +## Next Steps + + + + Full API reference for feed items, categories, engagement, and push tracking + + + Ready-to-use component with filtering, real-time updates, and styling + + diff --git a/ui-kit/flutter/notification-feed.mdx b/ui-kit/flutter/notification-feed.mdx new file mode 100644 index 000000000..3fc356c40 --- /dev/null +++ b/ui-kit/flutter/notification-feed.mdx @@ -0,0 +1,409 @@ +--- +title: "Notification Feed" +description: "Full-screen notification feed component with category filtering, card rendering, real-time updates, and engagement reporting." +--- + +`CometChatNotificationFeed` displays a scrollable notification feed where each item is rendered as a native card using the CometChat Cards library. It handles fetching, pagination, category filtering, timestamp grouping, real-time updates, and read/delivered/engagement reporting automatically. + + + + + +--- + +## Where It Fits + +`CometChatNotificationFeed` is a full-screen component. Drop it into a route or screen. It manages its own data fetching, state, and real-time listeners — you just handle navigation callbacks. + + + +```dart +import 'package:cometchat_chat_uikit/cometchat_chat_uikit.dart'; + +class NotificationsScreen extends StatelessWidget { + const NotificationsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return CometChatNotificationFeed( + showBackButton: true, + onBackPress: () => Navigator.of(context).pop(), + onItemClick: (NotificationFeedItem item) { + // Handle item tap (e.g., open detail or deep link) + }, + ); + } +} +``` + + + +--- + +## Quick Start + + + +```dart +import 'package:cometchat_chat_uikit/cometchat_chat_uikit.dart'; + +@override +Widget build(BuildContext context) { + return const CometChatNotificationFeed(); +} +``` + + + +Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()` and a user logged in. + +--- + +## Filtering Feed Items + +Control what loads using custom request builders: + + + +```dart +CometChatNotificationFeed( + notificationFeedRequestBuilder: NotificationFeedRequestBuilder() + ..setLimit(30) + ..setReadState(FeedReadState.unread) + ..setCategory("promotions"), +) +``` + + + +### Filter Options + +| Builder Method | Description | +| --- | --- | +| `.setLimit(int)` | Items per page (default 20, max 100) | +| `.setReadState(FeedReadState)` | `read`, `unread`, or `all` | +| `.setCategory(String)` | Filter by category ID | +| `.setChannelId(String)` | Filter by channel | +| `.setTags(List)` | Filter by tags | +| `.setDateFrom(String)` | ISO 8601 date lower bound | +| `.setDateTo(String)` | ISO 8601 date upper bound | + + +Pass the builder object (without calling `.build()`). The component calls `.build()` internally. + + +--- + +## Actions and Events + +### Callback Methods + +#### `onItemClick` + +Fires when a feed item card is tapped. + + + +```dart +CometChatNotificationFeed( + onItemClick: (NotificationFeedItem item) { + // item.id, item.content (Card JSON), item.category + }, +) +``` + + + +#### `onActionClick` + +Fires when an interactive element (button, link) inside a card is tapped. + + + +```dart +CometChatNotificationFeed( + onActionClick: (NotificationFeedItem item, CometChatCardActionEvent action) { + if (action.action is CometChatCardOpenUrlAction) { + // Open URL in browser + } else if (action.action is CometChatCardChatWithUserAction) { + // Navigate to chat + } + }, +) +``` + + + +#### `onError` + +Fires when an internal error occurs (network failure, SDK exception). + + + +```dart +CometChatNotificationFeed( + onError: (String error) { + debugPrint("Feed error: $error"); + }, +) +``` + + + +#### `onBackPress` + +Fires when the back button in the header is tapped. + + + +```dart +CometChatNotificationFeed( + showBackButton: true, + onBackPress: () => Navigator.of(context).pop(), +) +``` + + + +### Automatic Behaviors + +The component handles these automatically — no manual setup needed: + +| Behavior | Description | +| --- | --- | +| Real-time updates | New items appear at the top via WebSocket listener | +| Delivery reporting | Items are reported as delivered when fetched | +| Read reporting | Items are reported as read after 1 second of visibility | +| Infinite scroll | Fetches next page when scrolling near the bottom | +| Pull-to-refresh | Resets and fetches fresh data on pull | +| Timestamp grouping | Groups items as "Today", "Yesterday", day name, or date | +| Category filtering | Filter chips row for category-based filtering | + +--- + +## Properties + +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| `title` | String | `"Notifications"` | Header title text | +| `showHeader` | bool | `true` | Toggle header visibility | +| `showBackButton` | bool | `false` | Toggle back button | +| `showFilterChips` | bool | `true` | Toggle category filter chips | +| `headerView` | Widget? | null | Custom header widget | +| `scrollToItemId` | String? | null | Deep link to a specific item | +| `notificationFeedRequestBuilder` | NotificationFeedRequestBuilder? | null | Custom feed request | +| `notificationCategoriesRequestBuilder` | NotificationCategoriesRequestBuilder? | null | Custom categories request | +| `cardThemeMode` | CometChatCardThemeMode? | null | Card renderer theme mode | +| `cardThemeOverride` | CometChatCardThemeOverride? | null | Card renderer theme override | + +--- + +## Custom View Slots + +### Header View + +Replace the entire header: + + + +```dart +CometChatNotificationFeed( + headerView: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Text( + "My Notifications", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ], + ), + ), +) +``` + + + +### State Views + + + +```dart +CometChatNotificationFeed( + emptyStateView: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.notifications_off, size: 64, color: Colors.grey), + SizedBox(height: 16), + Text("No notifications yet"), + ], + ), + ), + errorStateView: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Something went wrong"), + ElevatedButton( + onPressed: () { /* retry logic */ }, + child: Text("Retry"), + ), + ], + ), + ), + loadingStateView: const Center( + child: CircularProgressIndicator(), + ), +) +``` + + + +--- + +## Style + + + +```dart +CometChatNotificationFeed( + style: CometChatNotificationFeedStyle( + backgroundColor: Color(0xFFF5F5F5), + headerTitleColor: Color(0xFF141414), + chipActiveBackgroundColor: Color(0xFF3399FF), + chipActiveTextColor: Colors.white, + chipInactiveBackgroundColor: Colors.transparent, + chipInactiveTextColor: Color(0xFF727272), + chipBorderColor: Color(0xFFE0E0E0), + cardBackgroundColor: Colors.white, + cardBorderColor: Color(0xFFE0E0E0), + cardBorderRadius: 12, + unreadIndicatorColor: Color(0xFF3399FF), + ), +) +``` + + + +### Style Properties + +| Property | Type | Description | +| --- | --- | --- | +| `backgroundColor` | Color? | Screen background color | +| `headerTitleColor` | Color? | Header title text color | +| `headerTitleTextStyle` | TextStyle? | Header title text style | +| `backIconColor` | Color? | Back button icon color | +| `chipActiveBackgroundColor` | Color? | Selected filter chip background | +| `chipActiveTextColor` | Color? | Selected filter chip text | +| `chipInactiveBackgroundColor` | Color? | Unselected filter chip background | +| `chipInactiveTextColor` | Color? | Unselected filter chip text | +| `chipBorderColor` | Color? | Filter chip border | +| `chipTextStyle` | TextStyle? | Filter chip text style | +| `badgeBackgroundColor` | Color? | Badge background | +| `badgeTextColor` | Color? | Badge text | +| `badgeTextStyle` | TextStyle? | Badge text style | +| `timestampTextColor` | Color? | Item timestamp color | +| `timestampTextStyle` | TextStyle? | Item timestamp style | +| `timestampHeaderTextStyle` | TextStyle? | Section header timestamp style | +| `timestampHeaderTextColor` | Color? | Section header timestamp color | +| `cardBackgroundColor` | Color? | Card container background | +| `cardBorderColor` | Color? | Card container border | +| `cardBorderRadius` | double? | Card corner radius | +| `cardBorderWidth` | double? | Card border width | +| `unreadIndicatorColor` | Color? | Unread dot indicator color | +| `separatorColor` | Color? | Separator between cards | + +All colors default to `null` to inherit from `CometChatTheme`. Override individual values without losing theme support. + +--- + +## Deep Linking + +Navigate directly to a specific feed item using `scrollToItemId`: + + + +```dart +CometChatNotificationFeed( + scrollToItemId: "announcement-id-from-push", +) +``` + + + +If the item is already loaded, the feed scrolls to it. If not, it fetches the item by ID and inserts it at the top. + +--- + +## Common Patterns + +### Show only unread items + + + +```dart +CometChatNotificationFeed( + notificationFeedRequestBuilder: NotificationFeedRequestBuilder() + ..setReadState(FeedReadState.unread), +) +``` + + + +### Hide filter chips and header + + + +```dart +CometChatNotificationFeed( + showHeader: false, + showFilterChips: false, +) +``` + + + +### Custom categories request + + + +```dart +CometChatNotificationFeed( + notificationCategoriesRequestBuilder: NotificationCategoriesRequestBuilder() + ..setLimit(10), +) +``` + + + +### Card theme mode override + + + +```dart +CometChatNotificationFeed( + cardThemeMode: CometChatCardThemeMode.dark, +) +``` + + + +--- + +## Next Steps + + + + Overview of how campaigns work end-to-end + + + Low-level SDK APIs for feed items, categories, and engagement + + + Full styling reference for all components + + + Custom BLoCs, repositories, and data sources + + diff --git a/ui-kit/flutter/v5/getting-started.mdx b/ui-kit/flutter/v5/getting-started.mdx index 96032dc2b..2b9496dcf 100644 --- a/ui-kit/flutter/v5/getting-started.mdx +++ b/ui-kit/flutter/v5/getting-started.mdx @@ -86,8 +86,8 @@ Add to your `pubspec.yaml`: dependencies: flutter: sdk: flutter - cometchat_chat_uikit: ^5.2.14 - cometchat_calls_uikit: ^5.0.15 # Optional: for voice/video calling + cometchat_chat_uikit: ^5.2.16 + cometchat_calls_uikit: ^5.0.16 # Optional: for voice/video calling ``` Then run: From f024b508ba7e2079deb528bd27c702678b73ba8d Mon Sep 17 00:00:00 2001 From: shagun-cometchat Date: Wed, 27 May 2026 20:49:32 +0530 Subject: [PATCH 28/45] docs(campaigns): update JavaScript SDK campaign action handler examples --- sdk/javascript/campaigns.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sdk/javascript/campaigns.mdx b/sdk/javascript/campaigns.mdx index dd57fd71c..b38c51a6f 100644 --- a/sdk/javascript/campaigns.mdx +++ b/sdk/javascript/campaigns.mdx @@ -476,16 +476,17 @@ function NotificationCard({ item }) { cardJson={cardJson} themeMode="auto" onAction={(event) => { - switch (event.type) { + const { action, elementId } = event; + switch (action.type) { case "openUrl": // Open URL in browser - window.open(event.params.url, "_blank"); + window.open(action.url, "_blank"); break; case "chatWithUser": - // Navigate to chat with event.params.uid + // Navigate to chat with action.uid break; case "chatWithGroup": - // Navigate to group chat with event.params.guid + // Navigate to group chat with action.guid break; } }} From cf9f86431d76dfc60bdd87317a1dc528c737af92 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 20:52:32 +0530 Subject: [PATCH 29/45] docs(campaigns): Clarify API parameters and limits, improve variable documentation - Make templateCategory parameter optional in API schema - Change limit parameter type from string to number and make optional - Update recipients limit terminology from "per notification" to "per API call" across all docs - Simplify Delete campaign description by removing reference to cancelled campaigns - Add default state clarification for channel enabled property (defaults to disabled) - Expand variable type documentation with specific format examples for string, image, and action types - Add note clarifying defaultValue support is limited to string type variables only - Remove temporary temp.md file --- campaigns-apis.json | 6 +++--- campaigns.mdx | 2 +- campaigns/campaigns.mdx | 4 ++-- campaigns/channels.mdx | 4 ++-- campaigns/templates.mdx | 16 ++++++++++------ campaigns/users.mdx | 2 +- temp.md | 1 - 7 files changed, 19 insertions(+), 16 deletions(-) delete mode 100644 temp.md diff --git a/campaigns-apis.json b/campaigns-apis.json index 598426ed6..813fd4a8a 100644 --- a/campaigns-apis.json +++ b/campaigns-apis.json @@ -25,7 +25,7 @@ }, { "name": "templateCategory", - "required": true, + "required": false, "in": "query", "schema": { "type": "string" @@ -1401,10 +1401,10 @@ }, { "name": "limit", - "required": true, + "required": false, "in": "query", "schema": { - "type": "string" + "type": "number" } } ], diff --git a/campaigns.mdx b/campaigns.mdx index ca561bbf7..2230f5cd6 100644 --- a/campaigns.mdx +++ b/campaigns.mdx @@ -50,7 +50,7 @@ The typical setup to send your first notification: | Resource | Limit | |----------|-------| -| Recipients per notification | 10,000 | +| Recipients per API call | 10,000 | ## Common Use Cases diff --git a/campaigns/campaigns.mdx b/campaigns/campaigns.mdx index ef3664e80..d45f3cc4f 100644 --- a/campaigns/campaigns.mdx +++ b/campaigns/campaigns.mdx @@ -53,11 +53,11 @@ You can click "Send Now" on a scheduled campaign to override the schedule and se ## Cancel and Delete - **Cancel** — Cancels a scheduled or draft campaign. Cannot cancel once `sending` has started. -- **Delete** — Only draft campaigns can be deleted. Cancelled campaigns cannot be deleted. +- **Delete** — Only draft campaigns can be deleted. ## Limits | Limit | Value | |-------|-------| -| Max recipients per campaign | 10,000 | +| Recipients per API call | 10,000 | diff --git a/campaigns/channels.mdx b/campaigns/channels.mdx index b60e2decf..675337bde 100644 --- a/campaigns/channels.mdx +++ b/campaigns/channels.mdx @@ -20,7 +20,7 @@ Channels define where notifications are delivered. Each template references one 3. Configure the channel: - **Name** — Display name (e.g., "Default Feed", "Promotions Feed") - **Channel ID** — Auto-generated slug: `cc-notification-channel-` (immutable after creation) - - **Enabled** — Toggle on/off + - **Enabled** — Toggle on/off (defaults to disabled) 4. Click **Create Channel** ## Channel Properties @@ -30,7 +30,7 @@ Channels define where notifications are delivered. Each template references one | `name` | string | Display name | | `channelId` | string | Unique slug (immutable after creation) | | `type` | enum | `in_app` \| `push` | -| `enabled` | boolean | Whether channel is available for template assignment | +| `enabled` | boolean | Whether channel is available for template assignment (defaults to `false`) | Push notification configuration (FCM/APNs keys) is managed at the CometChat app level, not per-channel in Campaigns. diff --git a/campaigns/templates.mdx b/campaigns/templates.mdx index 16bdbb77a..2cdee5430 100644 --- a/campaigns/templates.mdx +++ b/campaigns/templates.mdx @@ -11,7 +11,7 @@ A template is a reusable notification design that defines the content, delivery | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Display name | -| `templateId` | string | Yes | Slug: `cc-template-` (immutable) | +| `templateId` | string | No (auto-generated) | Slug: `cc-template-` (immutable) | | `templateCategory` | string | No | Category name (e.g., "Marketing") | | `label` | string | No | Display label shown on notification (e.g., "Promo") | | `alternativeText` | string | No | Plain-text fallback when rich content can't render | @@ -34,11 +34,15 @@ Variables allow per-recipient personalization in notification content. - **Naming**: Letters, numbers, and underscores only (`^[a-zA-Z_][a-zA-Z0-9_]*$`) - **Type**: Selected from a dropdown with the following options: -| Type | Label | Hint | -|------|-------|------| -| `string` | String | Text content | -| `image` | Image | Image URL | -| `action` | Action | Action URL | +| Type | Value Format | +|------|-------------| +| `string` | Plain text string (e.g., `"Hello World"`) | +| `image` | Object: `{ "url": "https://...", "width": 300, "height": 200 }` — all fields required | +| `action` | Object: `{ "type": "web" or "custom", "data": "https://..." }` — both fields required | + + +`defaultValue` is only supported for `string` type variables. Image and action variables do not support defaults. + - **Resolution**: Per-user values are passed at send time in the `variables` field ## Template Versioning diff --git a/campaigns/users.mdx b/campaigns/users.mdx index 25e794335..eb55c06d2 100644 --- a/campaigns/users.mdx +++ b/campaigns/users.mdx @@ -37,4 +37,4 @@ For the API reference, see [Send Notification](/rest-api/campaigns-apis/notifica | Limit | Value | |-------|-------| -| Max recipients per campaign | 10,000 | +| Recipients per API call | 10,000 | diff --git a/temp.md b/temp.md deleted file mode 100644 index e2abe9f08..000000000 --- a/temp.md +++ /dev/null @@ -1 +0,0 @@ -{"openapi":"3.0.0","paths":{"/notification-feed/unread-count":{"get":{"operationId":"NotificationFeedController_getUnreadCount","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"templateCategory","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Get unread count for the requesting user","tags":["Notification Feed"]}},"/notification-feed":{"get":{"operationId":"NotificationFeedController_findFeed","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"readState","required":false,"in":"query","description":"Filter by read state","schema":{"type":"string","enum":["read","unread","all"]}},{"name":"dateFrom","required":false,"in":"query","description":"Start date filter (unix timestamp in seconds)","schema":{"type":"number"}},{"name":"dateTo","required":false,"in":"query","description":"End date filter (unix timestamp in seconds)","schema":{"type":"number"}},{"name":"tags","required":false,"in":"query","description":"Comma-separated tags to filter by","schema":{"type":"string"}},{"name":"tagMatch","required":false,"in":"query","description":"Tag matching strategy: 'any' (OR) or 'all' (AND)","schema":{"type":"string","enum":["any","all"]}},{"name":"templateCategory","required":false,"in":"query","description":"Filter by templateCategory (per-app TemplateCategory.name)","schema":{"type":"string"}},{"name":"channelId","required":false,"in":"query","description":"Filter by in-app channel instance ID","schema":{"type":"string"}},{"name":"includeDeleted","required":false,"in":"query","description":"Include soft-deleted feed items","schema":{"default":false,"type":"boolean"}},{"name":"includeExpired","required":false,"in":"query","description":"Include expired feed items","schema":{"default":false,"type":"boolean"}},{"name":"sentAt","required":false,"in":"query","description":"Cursor: sentAt unix timestamp of last item from previous page","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item from previous page","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Number of items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Field to sort by","schema":{"type":"string","enum":["sentAt","createdAt"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"receiver","required":false,"in":"query","description":"Admin-only: scope to a specific user. Ignored when onbehalfof is present.","schema":{"type":"string"}},{"name":"templateOnly","required":false,"in":"query","description":"Pass \"true\" to opt out of server-side rendering. Default: false (server renders and returns content per item).","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Query feed with filters and cursor pagination","tags":["Notification Feed"]}},"/notification-feed/{id}":{"get":{"operationId":"NotificationFeedController_findById","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"templateOnly","required":false,"in":"query","description":"Pass \"true\" to opt out of server-side rendering. Default: false (server renders and returns content).","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Get a feed item by ID","tags":["Notification Feed"]},"delete":{"operationId":"NotificationFeedController_delete","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"summary":"Soft-delete a feed item (admin only)","tags":["Notification Feed"]}},"/notification-feed/{id}/read":{"post":{"operationId":"NotificationFeedController_markAsRead","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Mark a feed item as read (idempotent)","tags":["Notification Feed"]}},"/notification-feed/{id}/delivered":{"post":{"operationId":"NotificationFeedController_markAsDelivered","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Mark a feed item as delivered (idempotent)","tags":["Notification Feed"]}},"/notification-feed/{id}/engagement":{"post":{"operationId":"NotificationFeedController_reportEngagement","parameters":[{"name":"onbehalfof","in":"header","description":"UID of user making client request","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngagementEventDto"}}}},"responses":{"200":{"description":""}},"summary":"Report an interacted engagement event with optional topic discriminator","tags":["Notification Feed"]}},"/settings":{"get":{"operationId":"SettingsController_get","parameters":[{"name":"x-internal-api-key","in":"header","description":"Internal API key for platform-level settings access","required":true,"schema":{"type":"string"}},{"name":"appId","required":true,"in":"query","description":"Tenant application ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Settings retrieved"},"404":{"description":"Not found or unauthorized"}},"summary":"Get tenant settings (internal only)","tags":["Settings"]},"put":{"operationId":"SettingsController_update","parameters":[{"name":"x-internal-api-key","in":"header","description":"Internal API key for platform-level settings access","required":true,"schema":{"type":"string"}},{"name":"appId","required":true,"in":"query","description":"Tenant application ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSettingsDto"}}}},"responses":{"200":{"description":"Settings updated"},"400":{"description":"Invalid input"}},"summary":"Update tenant settings (internal only)","tags":["Settings"]}},"/templates/categories":{"post":{"operationId":"CategoriesController_create","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCategoryDto"}}}},"responses":{"201":{"description":"Category created"},"400":{"description":"Invalid input"},"409":{"description":"Duplicate category name"}},"summary":"Create a new template category (admin only)","tags":["Template Categories"]},"get":{"operationId":"CategoriesController_findAll","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["name","createdAt","updatedAt"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Categories list"}},"summary":"List all template categories","tags":["Template Categories"]}},"/templates/categories/{id}":{"get":{"operationId":"CategoriesController_findById","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Category found"},"404":{"description":"Category not found"}},"summary":"Get a template category by ID","tags":["Template Categories"]},"put":{"operationId":"CategoriesController_update","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCategoryDto"}}}},"responses":{"200":{"description":"Category updated"},"404":{"description":"Category not found"},"409":{"description":"Duplicate category name"}},"summary":"Update a template category (admin only)","tags":["Template Categories"]},"delete":{"operationId":"CategoriesController_delete","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Category deleted"},"404":{"description":"Category not found"}},"summary":"Delete a template category (admin only)","tags":["Template Categories"]}},"/templates":{"post":{"operationId":"TemplatesController_create","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTemplateDto"}}}},"responses":{"201":{"description":"Template created"},"400":{"description":"Invalid input or variable schema"},"409":{"description":"Duplicate channel type"}},"summary":"Create a new template (admin only)","tags":["Templates"]},"get":{"operationId":"TemplatesController_findAll","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["updatedAt","createdAt","name"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"status","required":false,"in":"query","description":"Filter by template status","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","description":"Search by name or templateId","schema":{"type":"string"}},{"name":"tags","required":false,"in":"query","description":"Comma-separated tags to filter by","schema":{"type":"string"}},{"name":"tagMatch","required":false,"in":"query","description":"Tag matching strategy: 'any' (OR) or 'all' (AND)","schema":{"type":"string","enum":["any","all"]}},{"name":"templateCategory","required":false,"in":"query","description":"Filter by templateCategory name","schema":{"type":"string"}}],"responses":{"200":{"description":"Templates list"}},"summary":"List all templates","tags":["Templates"]}},"/templates/{id}":{"get":{"operationId":"TemplatesController_findById","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Template found"},"404":{"description":"Template not found"}},"summary":"Get a template by ID","tags":["Templates"]},"put":{"operationId":"TemplatesController_update","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTemplateDto"}}}},"responses":{"200":{"description":"Template updated"},"400":{"description":"Invalid variable schema"},"404":{"description":"Template not found"}},"summary":"Update template metadata (admin only)","tags":["Templates"]},"delete":{"operationId":"TemplatesController_archive","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Template archived"},"404":{"description":"Template not found"}},"summary":"Archive a template (admin only)","tags":["Templates"]}},"/templates/{id}/channels/{channelType}":{"put":{"operationId":"TemplatesController_updateChannelContent","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"channelType","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateChannelContentDto"}}}},"responses":{"200":{"description":"Channel content updated"},"404":{"description":"Template not found"}},"summary":"Update channel content for a template (admin only)","tags":["Templates"]}},"/templates/{id}/versions":{"post":{"operationId":"TemplatesController_createVersion","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"Version created"},"404":{"description":"Template not found"}},"summary":"Create a new template version (admin only)","tags":["Templates"]}},"/channels":{"post":{"operationId":"ChannelsController_create","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateChannelDto"}}}},"responses":{"201":{"description":"Channel created"},"400":{"description":"Called with onbehalfof"},"403":{"description":"Channel type restricted"},"409":{"description":"Duplicate channelId or limit reached"}},"summary":"Create a new channel (admin only)","tags":["Channels"]},"get":{"operationId":"ChannelsController_list","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["channelType","createdAt","updatedAt"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"search","required":false,"in":"query","description":"Search by name or channelId","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated channel list"}},"summary":"List channels for the app","tags":["Channels"]}},"/channels/availability":{"get":{"operationId":"ChannelsController_getAvailability","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Channel type availability list"},"400":{"description":"Called with onbehalfof"}},"summary":"Get channel type availability (admin only)","tags":["Channels"]}},"/channels/{id}":{"get":{"operationId":"ChannelsController_getById","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Channel found"},"404":{"description":"Channel not found"}},"summary":"Get a channel by ID","tags":["Channels"]},"put":{"operationId":"ChannelsController_update","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateChannelDto"}}}},"responses":{"200":{"description":"Channel updated"},"400":{"description":"Called with onbehalfof"},"404":{"description":"Channel not found"}},"summary":"Update a channel (admin only)","tags":["Channels"]}},"/push-notifications":{"get":{"operationId":"PushNotificationsController_findByReceiver","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Push notifications list"}},"summary":"List push notifications for a user","tags":["Push Notifications"]}},"/push-notifications/{id}":{"get":{"operationId":"PushNotificationsController_findById","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Push notification details"}},"summary":"Get a push notification by ID","tags":["Push Notifications"]}},"/push-notifications/{id}/delivered":{"put":{"operationId":"PushNotificationsController_markDelivered","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Marked as delivered"}},"summary":"Mark push notification as delivered","tags":["Push Notifications"]}},"/push-notifications/{id}/clicked":{"put":{"operationId":"PushNotificationsController_markClicked","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Marked as clicked"}},"summary":"Mark push notification as clicked","tags":["Push Notifications"]}},"/push-notifications/{id}/engagement":{"post":{"operationId":"PushNotificationsController_reportEngagement","parameters":[{"name":"onbehalfof","in":"header","description":"UID of the user","required":false,"schema":{"type":"string"}},{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushEngagementEventDto"}}}},"responses":{"200":{"description":"Engagement recorded"}},"summary":"Report push notification engagement event (interacted with optional topic)","tags":["Push Notifications"]}},"/campaigns":{"post":{"operationId":"CampaignsController_create","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCampaignDto"}}}},"responses":{"201":{"description":"Campaign created"}},"summary":"Create a new campaign","tags":["Campaigns"]},"get":{"operationId":"CampaignsController_findAll","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["updatedAt","createdAt","name"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"status","required":false,"in":"query","description":"Filter by campaign status","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","description":"Search by name","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign list"}},"summary":"List campaigns (enriched with template + recipient summary)","tags":["Campaigns"]}},"/campaigns/{id}":{"get":{"operationId":"CampaignsController_findById","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign found"},"404":{"description":"Campaign not found"}},"summary":"Get a campaign by ID (enriched)","tags":["Campaigns"]},"put":{"operationId":"CampaignsController_update","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCampaignDto"}}}},"responses":{"200":{"description":"Campaign updated"},"400":{"description":"Campaign not in draft status"},"404":{"description":"Campaign not found"}},"summary":"Update a campaign (draft only)","tags":["Campaigns"]},"delete":{"operationId":"CampaignsController_delete","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign deleted"},"400":{"description":"Campaign not in draft status"},"404":{"description":"Campaign not found"}},"summary":"Delete a campaign (draft only)","tags":["Campaigns"]}},"/campaigns/{id}/recipients":{"post":{"operationId":"CampaignsController_addRecipients","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRecipientsDto"}}}},"responses":{"201":{"description":"Recipients added"},"400":{"description":"Campaign not in draft status"}},"summary":"Add recipients manually (user IDs)","tags":["Campaigns"]},"get":{"operationId":"CampaignsController_getRecipients","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"updatedAt","required":false,"in":"query","description":"Cursor: updatedAt unix timestamp of last item","schema":{"type":"number"}},{"name":"id","required":false,"in":"query","description":"Cursor: id of last item","schema":{"type":"string"}},{"name":"affix","required":false,"in":"query","description":"Cursor direction","schema":{"type":"string","enum":["append","prepend"]}},{"name":"limit","required":false,"in":"query","description":"Items per page","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"sort","required":false,"in":"query","description":"Sort field","schema":{"type":"string","enum":["updatedAt","createdAt"]}},{"name":"order","required":false,"in":"query","description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"status","required":false,"in":"query","description":"Filter by status","schema":{"type":"string","enum":["pending","processing","completed","failed"]}}],"responses":{"200":{"description":"Recipient list"}},"summary":"List campaign recipients","tags":["Campaigns"]}},"/campaigns/{id}/recipients/summary":{"get":{"operationId":"CampaignsController_getRecipientSummary","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Recipient summary"}},"summary":"Get recipient status summary","tags":["Campaigns"]}},"/campaigns/{id}/recipients/upload-url":{"post":{"operationId":"CampaignsController_getUploadUrl","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"Presigned URL generated"},"400":{"description":"Campaign not in draft status"}},"summary":"Get S3 presigned URL for CSV upload","tags":["Campaigns"]}},"/campaigns/{id}/import-csv":{"post":{"operationId":"CampaignsController_importCsv","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CsvUploadDto"}}}},"responses":{"202":{"description":"Import job created"},"400":{"description":"Campaign not in draft status"},"409":{"description":"Import already in progress"}},"summary":"Start async CSV import for campaign recipients","tags":["Campaigns"]}},"/campaigns/{id}/import-status":{"get":{"operationId":"CampaignsController_getImportStatus","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Import status returned"},"404":{"description":"No active import found"}},"summary":"Get current import job status for a campaign","tags":["Campaigns"]}},"/campaigns/{id}/send":{"post":{"operationId":"CampaignsController_send","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign send enqueued"},"400":{"description":"Campaign not in sendable status or no recipients"}},"summary":"Trigger campaign send","tags":["Campaigns"]}},"/campaigns/{id}/schedule":{"post":{"operationId":"CampaignsController_schedule","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleCampaignDto"}}}},"responses":{"200":{"description":"Campaign scheduled"},"400":{"description":"Campaign not in draft status or invalid time"}},"summary":"Schedule campaign send","tags":["Campaigns"]}},"/campaigns/{id}/sequence-metrics":{"get":{"operationId":"CampaignsController_getSequenceMetrics","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Sequence metrics returned"}},"summary":"Get per-step sequence delivery metrics for a campaign","tags":["Campaigns"]}},"/campaigns/{id}/cancel":{"post":{"operationId":"CampaignsController_cancel","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign cancelled"},"400":{"description":"Campaign not in cancellable status"}},"summary":"Cancel a campaign","tags":["Campaigns"]}},"/notifications/messages":{"post":{"description":"Sendbird-equivalent POST /notifications/messages. 1–10 receivers = realtime (synchronous, returns notificationId). 11–10,000 receivers = inline batch (asynchronous, returns batchId). Larger sends should use the Campaigns CSV flow.","operationId":"NotificationSendController_send","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendNotificationDto"}}}},"responses":{"200":{"description":"Realtime: { notificationId, channels[], mode: \"realtime\" }. Batch: { batchId, total, channels[], mode: \"batch\" }."},"400":{"description":"Template not found, template not approved, or recipient count exceeds limits"},"403":{"description":"Admin-only — onbehalfof header must not be present"}},"summary":"Send a notification using a template","tags":["Notifications"]}},"/analytics/events":{"post":{"operationId":"AnalyticsController_recordEvent","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordEventDto"}}}},"responses":{"201":{"description":"Event recorded"},"400":{"description":"Invalid input"}},"summary":"Record an analytics event","tags":["Analytics"]}},"/analytics/overview":{"get":{"operationId":"AnalyticsController_getOverview","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Analytics overview"}},"summary":"Get aggregated analytics overview","tags":["Analytics"]}},"/analytics/campaigns/{campaignId}":{"get":{"operationId":"AnalyticsController_getCampaignAnalytics","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"campaignId","required":true,"in":"path","schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Campaign analytics"}},"summary":"Get campaign analytics drill-down","tags":["Analytics"]}},"/analytics/templates/{templateId}":{"get":{"operationId":"AnalyticsController_getTemplateAnalytics","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"templateId","required":true,"in":"path","schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Template analytics"}},"summary":"Get template analytics drill-down","tags":["Analytics"]}},"/analytics/channels":{"get":{"operationId":"AnalyticsController_getChannelBreakdown","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Channel breakdown"}},"summary":"Get channel breakdown analytics","tags":["Analytics"]}},"/analytics/users/{userId}":{"get":{"operationId":"AnalyticsController_getUserInsights","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}},{"name":"dimension","required":false,"in":"query","description":"Rollup dimension to query","schema":{"type":"string","enum":["campaign","template","channel"]}},{"name":"dimensionId","required":false,"in":"query","description":"Dimension ID (campaign ID, template ID, or channel type)","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Rollup period","schema":{"type":"string","enum":["hourly","daily"]}},{"name":"startDate","required":false,"in":"query","description":"Start date filter (ISO 8601)","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"End date filter (ISO 8601)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of results","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":"Aggregated counts: viewed, clicked, interacted, lastEngagement (unix seconds or null)"}},"summary":"Get user-level engagement insights","tags":["Analytics"]}},"/analytics/rollup":{"post":{"operationId":"AnalyticsController_triggerRollup","parameters":[{"name":"appid","in":"header","description":"Tenant application ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Rollup triggered"}},"summary":"Trigger rollup computation","tags":["Analytics"]}},"/health":{"get":{"operationId":"HealthController_check","parameters":[],"responses":{"200":{"description":"All dependencies healthy."},"503":{"description":"At least one dependency is down."}},"tags":["Health"]}}},"info":{"title":"Campaigns Service API","description":"Campaigns Service REST API","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"appid":{"type":"apiKey","in":"header","name":"appid"},"basic-auth":{"type":"http","scheme":"basic","description":"Service-to-service basic auth"}},"schemas":{"EngagementEventDto":{"type":"object","properties":{"topic":{"type":"string","description":"Freeform topic discriminator for the interacted event. Well-known value: \"clicked\". Custom values: any alphanumeric + underscore/hyphen string ≤64 chars.","example":"clicked","maxLength":64}}},"UpdateSettingsDto":{"type":"object","properties":{"retentionDays":{"type":"number","description":"Data retention period in days","example":90,"minimum":1},"defaultChannels":{"description":"Default channel types for new campaigns","example":["push","in_app"],"type":"array","items":{"type":"string"}},"realtimeFanout":{"type":"array","description":"Realtime fanout policy for in_app FeedItem deliveries. Empty array = feed_only (default). Allowed values: 'websocket', 'push'. Consumers of after_feed_item_sent (WebSocket fanout svc, push wake-up svc) self-filter on this array.","example":["websocket"],"items":{"type":"string","enum":["websocket","push"]}},"config":{"type":"object","description":"Additional service-level configuration. Supports: deliveryMechanisms: { websocketEnabled: boolean, pushEnabled: boolean } — controls default delivery mechanisms for announcements. Defaults: { websocketEnabled: true, pushEnabled: false }. Feed is always enabled.","example":{"timezone":"UTC","deliveryMechanisms":{"websocketEnabled":true,"pushEnabled":false}}}}},"CreateCategoryDto":{"type":"object","properties":{"name":{"type":"string","description":"Category name"},"description":{"type":"string","description":"Category description"}},"required":["name"]},"UpdateCategoryDto":{"type":"object","properties":{"name":{"type":"string","description":"Category name"},"description":{"type":"string","description":"Category description"}}},"ChannelContentDto":{"type":"object","properties":{"channelId":{"type":"string","description":"Channel instance ID (links to Channel entity)"},"channelType":{"type":"string","description":"Channel type identifier"},"content":{"type":"object","description":"Channel content (defaults applied if omitted)"},"dataType":{"type":"string","description":"Data type","enum":["ui_template","data_template"]},"categoryFilterEnabled":{"type":"boolean","description":"Enable category filter (in-app only)"},"templateLabelEnabled":{"type":"boolean","description":"Enable template label (in-app only)"},"messageRetentionHours":{"type":"number","description":"Message retention in hours"},"sequenceOrder":{"type":"number","description":"Position in sequence (1, 2, 3...)"},"stopCondition":{"type":"string","description":"Stop condition for sequence step. Allowed values depend on channelType: feed channels (in_app) accept `delivered` | `read` | `engaged`; push channels accept `delivered` | `clicked`. Validated per-channel at template save (ENG-35239)."},"waitMinutes":{"type":"number","description":"Wait time in minutes before advancing to next step","enum":[5,10,30,60,240,1440]}},"required":["channelType"]},"CreateTemplateDto":{"type":"object","properties":{"name":{"type":"string","description":"Template name"},"templateId":{"type":"string","description":"Human-readable slug (auto-generated from name if omitted)"},"templateCategory":{"type":"string","description":"Template category","enum":["Onboarding","Transactional","Marketing","Product_Showcase","Alerts","Polls","Custom"]},"label":{"type":"string","description":"Display label shown on notification (e.g. Promo, Alert)"},"alternativeText":{"type":"string","description":"Plain-text fallback when push is suppressed or rich content cannot render. Sendbird-parity field."},"tags":{"description":"First-class tags for filtering/segmentation","type":"array","items":{"type":"string"}},"status":{"type":"string","description":"Template status","enum":["draft","approved","archived"]},"channels":{"description":"Channel configurations","type":"array","items":{"$ref":"#/components/schemas/ChannelContentDto"}},"variableSchema":{"description":"Variable schema definitions","type":"array","items":{"type":"string"}},"config":{"type":"object","description":"Additional configuration"}},"required":["name","channels"]},"UpdateTemplateDto":{"type":"object","properties":{"name":{"type":"string"},"templateCategory":{"type":"string"},"label":{"type":"string","description":"Display label shown on notification"},"alternativeText":{"type":"string","description":"Plain-text fallback when push is suppressed or rich content cannot render. Sendbird-parity field."},"tags":{"description":"First-class tags for filtering/segmentation","type":"array","items":{"type":"string"}},"status":{"type":"string"},"variableSchema":{"type":"array","items":{"type":"string"}},"config":{"type":"object"}}},"UpdateChannelContentDto":{"type":"object","properties":{"content":{"type":"object","description":"Channel content payload"}},"required":["content"]},"CreateChannelDto":{"type":"object","properties":{"name":{"type":"string","description":"Channel display name","example":"My Push Channel"},"type":{"type":"string","description":"Channel type","enum":["in_app","push","sms","email","whatsapp","custom"],"example":"push"},"channelId":{"type":"string","description":"Channel slug (auto-generated from name if omitted)","example":"cc-notification-channel-my-push"},"enabled":{"type":"boolean","description":"Whether the channel is enabled","default":false},"metadata":{"type":"object","description":"Channel-specific metadata","example":{"apiKey":"xxx","senderId":"yyy"}}},"required":["name","type"]},"UpdateChannelDto":{"type":"object","properties":{"name":{"type":"string","description":"Channel display name"},"enabled":{"type":"boolean","description":"Whether the channel is enabled"},"metadata":{"type":"object","description":"Channel-specific metadata","example":{"apiKey":"xxx","senderId":"yyy"}}}},"PushEngagementEventDto":{"type":"object","properties":{"topic":{"type":"string","description":"Freeform topic discriminator for the interacted event. Custom values: any alphanumeric + underscore/hyphen string ≤64 chars.","example":"dismissed","maxLength":64}}},"CreateCampaignDto":{"type":"object","properties":{"name":{"type":"string","description":"Campaign name"},"templateId":{"type":"string","description":"Template ID (CUID or templateId slug)"},"templateVersion":{"type":"number","description":"Template version number to pin","minimum":1},"variables":{"type":"object","description":"Campaign-level default variables — applied to every recipient as a fallback layer below per-user CSV values and above template variableSchema defaults. Example: `{ \"promoCode\": \"SUMMER25\", \"supportEmail\": \"help@acme.io\" }`.","example":{"promoCode":"SUMMER25"}},"config":{"type":"object","description":"Additional campaign configuration (free-form)"}},"required":["name","templateId","templateVersion"]},"UpdateCampaignDto":{"type":"object","properties":{"name":{"type":"string","description":"Campaign name"},"config":{"type":"object","description":"Additional campaign configuration"}}},"AddRecipientsDto":{"type":"object","properties":{"userIds":{"description":"Array of user IDs to add as recipients","minItems":1,"maxItems":10000,"type":"array","items":{"type":"string"}},"userVariables":{"type":"object","description":"Per-user variables, keyed by userId. Persisted on each `CampaignRecipient.variables` row at insert time. Renderer substitutes these into template content per recipient. Example: `{ \"user_42\": { \"name\": \"Ajay\" }, \"user_43\": { \"name\": \"Sam\" } }`.","example":{"user_42":{"name":"Ajay"}}}},"required":["userIds"]},"CsvUploadDto":{"type":"object","properties":{"s3Key":{"type":"string","description":"S3 object key of the uploaded CSV file"}},"required":["s3Key"]},"ScheduleCampaignDto":{"type":"object","properties":{"scheduledAt":{"type":"number","description":"Scheduled send time as Unix timestamp (seconds)","example":1714000000}},"required":["scheduledAt"]},"SendNotificationDto":{"type":"object","properties":{"templateId":{"type":"string","description":"Template CUID or templateId slug. Template must be in approved status.","example":"order_update"},"receivers":{"description":"Array of target user IDs. 1–10 = realtime (synchronous, returns notificationId immediately). 11–10,000 = batch (asynchronous, returns batchId; processing happens via queue).","minItems":1,"maxItems":10000,"type":"array","items":{"type":"string"}},"variables":{"type":"object","description":"Per-user variables. Keyed by userId; values are { variableName: value } objects. Variables are applied to the template content at delivery time.","example":{"user_42":{"user_name":"John","order_id":"12345"},"user_43":{"user_name":"Sarah","order_id":"12346"}}},"tag":{"type":"string","description":"Optional analytics tag attached to the send (passes through to delivery records)."}},"required":["templateId","receivers"]},"RecordEventDto":{"type":"object","properties":{"notificationId":{"type":"string","description":"Notification (batch) ID this engagement relates to"},"feedItemId":{"type":"string","description":"FeedItem ID (for in-app engagement)"},"pushNotificationId":{"type":"string","description":"PushNotification ID (for push engagement)"},"campaignId":{"type":"string","description":"Campaign ID (optional)"},"templateId":{"type":"string","description":"Template ID (optional)"},"userId":{"type":"string","description":"User ID who triggered the event"},"channelType":{"type":"string","description":"Channel type (in_app, push, ...)"},"eventType":{"type":"string","description":"Engagement event type","enum":["sent","delivered","clicked","interacted","failed"]},"topic":{"type":"string","description":"Topic discriminator for interacted events (≤64 chars)"}},"required":["notificationId","userId","channelType","eventType"]}}},"security":[{"basic-auth":[]}]} \ No newline at end of file From a717a3391d14c0b6d2a8fb35dbea9bb55f40a361 Mon Sep 17 00:00:00 2001 From: Aaryan Rawat Date: Wed, 27 May 2026 21:30:50 +0530 Subject: [PATCH 30/45] docs(campaigns-apis): Add default pagination values and error response codes - Add default value of 20 to pagination limit parameters across all list endpoints - Document 404 error responses for item retrieval endpoints (feed items, push notifications) - Document 400 error responses for endpoints requiring onbehalfof header (mark as read/delivered, engagement events) - Remove unused UpdateSettingsDto and RecordEventDto schema definitions - Improve API specification completeness with consistent error documentation --- campaigns-apis.json | 161 +++++++++------------------ rest-api/campaigns-apis/overview.mdx | 10 -- 2 files changed, 54 insertions(+), 117 deletions(-) diff --git a/campaigns-apis.json b/campaigns-apis.json index 813fd4a8a..1f97ace57 100644 --- a/campaigns-apis.json +++ b/campaigns-apis.json @@ -196,7 +196,8 @@ "schema": { "minimum": 1, "maximum": 100, - "type": "number" + "type": "number", + "default": 20 } }, { @@ -298,6 +299,9 @@ "responses": { "200": { "description": "" + }, + "404": { + "description": "Item not found." } }, "summary": "Get a feed item by ID", @@ -380,6 +384,12 @@ "responses": { "200": { "description": "" + }, + "400": { + "description": "Missing required onbehalfof header or invalid input." + }, + "404": { + "description": "Item not found." } }, "summary": "Mark a feed item as read (idempotent)", @@ -422,6 +432,12 @@ "responses": { "200": { "description": "" + }, + "400": { + "description": "Missing required onbehalfof header or invalid input." + }, + "404": { + "description": "Item not found." } }, "summary": "Mark a feed item as delivered (idempotent)", @@ -474,6 +490,12 @@ "responses": { "200": { "description": "" + }, + "400": { + "description": "Missing required onbehalfof header or invalid input." + }, + "404": { + "description": "Item not found." } }, "summary": "Report an interacted engagement event with optional topic discriminator", @@ -573,7 +595,8 @@ "schema": { "minimum": 1, "maximum": 100, - "type": "number" + "type": "number", + "default": 20 } }, { @@ -823,7 +846,8 @@ "schema": { "minimum": 1, "maximum": 100, - "type": "number" + "type": "number", + "default": 20 } }, { @@ -1215,7 +1239,8 @@ "schema": { "minimum": 1, "maximum": 100, - "type": "number" + "type": "number", + "default": 20 } }, { @@ -1453,6 +1478,9 @@ "responses": { "200": { "description": "Push notification details" + }, + "404": { + "description": "Item not found." } }, "summary": "Get a push notification by ID", @@ -1495,6 +1523,12 @@ "responses": { "200": { "description": "Marked as delivered" + }, + "400": { + "description": "Missing required onbehalfof header or invalid input." + }, + "404": { + "description": "Item not found." } }, "summary": "Mark push notification as delivered", @@ -1537,6 +1571,12 @@ "responses": { "200": { "description": "Marked as clicked" + }, + "400": { + "description": "Missing required onbehalfof header or invalid input." + }, + "404": { + "description": "Item not found." } }, "summary": "Mark push notification as clicked", @@ -1589,6 +1629,12 @@ "responses": { "200": { "description": "Engagement recorded" + }, + "400": { + "description": "Missing required onbehalfof header or invalid input." + }, + "404": { + "description": "Item not found." } }, "summary": "Report push notification engagement event (interacted with optional topic)", @@ -1682,7 +1728,8 @@ "schema": { "minimum": 1, "maximum": 100, - "type": "number" + "type": "number", + "default": 20 } }, { @@ -1966,7 +2013,8 @@ "schema": { "minimum": 1, "maximum": 100, - "type": "number" + "type": "number", + "default": 20 } }, { @@ -2924,53 +2972,6 @@ } } }, - "UpdateSettingsDto": { - "type": "object", - "properties": { - "retentionDays": { - "type": "number", - "description": "Data retention period in days", - "example": 90, - "minimum": 1 - }, - "defaultChannels": { - "description": "Default channel types for new campaigns", - "example": [ - "push", - "in_app" - ], - "type": "array", - "items": { - "type": "string" - } - }, - "realtimeFanout": { - "type": "array", - "description": "Realtime fanout policy for in_app FeedItem deliveries. Empty array = feed_only (default). Allowed values: 'websocket', 'push'. Consumers of after_feed_item_sent (WebSocket fanout svc, push wake-up svc) self-filter on this array.", - "example": [ - "websocket" - ], - "items": { - "type": "string", - "enum": [ - "websocket", - "push" - ] - } - }, - "config": { - "type": "object", - "description": "Additional service-level configuration. Supports: deliveryMechanisms: { websocketEnabled: boolean, pushEnabled: boolean } \u2014 controls default delivery mechanisms for announcements. Defaults: { websocketEnabled: true, pushEnabled: false }. Feed is always enabled.", - "example": { - "timezone": "UTC", - "deliveryMechanisms": { - "websocketEnabled": true, - "pushEnabled": false - } - } - } - } - }, "CreateCategoryDto": { "type": "object", "properties": { @@ -3397,60 +3398,6 @@ "templateId", "receivers" ] - }, - "RecordEventDto": { - "type": "object", - "properties": { - "notificationId": { - "type": "string", - "description": "Notification (batch) ID this engagement relates to" - }, - "feedItemId": { - "type": "string", - "description": "FeedItem ID (for in-app engagement)" - }, - "pushNotificationId": { - "type": "string", - "description": "PushNotification ID (for push engagement)" - }, - "campaignId": { - "type": "string", - "description": "Campaign ID (optional)" - }, - "templateId": { - "type": "string", - "description": "Template ID (optional)" - }, - "userId": { - "type": "string", - "description": "User ID who triggered the event" - }, - "channelType": { - "type": "string", - "description": "Channel type (in_app, push, ...)" - }, - "eventType": { - "type": "string", - "description": "Engagement event type", - "enum": [ - "sent", - "delivered", - "clicked", - "interacted", - "failed" - ] - }, - "topic": { - "type": "string", - "description": "Topic discriminator for interacted events (\u226464 chars)" - } - }, - "required": [ - "notificationId", - "userId", - "channelType", - "eventType" - ] } } }, diff --git a/rest-api/campaigns-apis/overview.mdx b/rest-api/campaigns-apis/overview.mdx index d513f9898..fb2b225d9 100644 --- a/rest-api/campaigns-apis/overview.mdx +++ b/rest-api/campaigns-apis/overview.mdx @@ -83,16 +83,6 @@ Non-exhaustive. These are the codes a typical caller will encounter. | Domain | `ERR_TEMPLATE_NOT_FOUND`, `ERR_CHANNEL_NOT_FOUND`, `ERR_FEED_ITEM_NOT_FOUND` | | Framework | `ERR_VALIDATION` (400), `ERR_UNAUTHORIZED` (401), `ERR_NOT_FOUND` (404), `ERR_CONFLICT` (409), `ERR_TOO_MANY_REQUESTS` (429), `ERR_BAD_REQUEST` (4xx fallback), `ERR_INTERNAL` (500) | -### Available operations - -| Operation | Method | Path | -| ----------------------------------------------------------------------------------------- | -------- | --------------------------------------------- | -| [List channels](/rest-api/campaigns-apis/channels/list-channels) | `GET` | `/channels` | -| [Check channel availability](/rest-api/campaigns-apis/channels/check-availability) | `GET` | `/channels/availability` | -| [List templates](/rest-api/campaigns-apis/templates/list-templates) | `GET` | `/templates` | -| [Get template](/rest-api/campaigns-apis/templates/get-template) | `GET` | `/templates/{templateId}` | -| [Delete feed item](/rest-api/campaigns-apis/notification-feed/delete-feed-item) | `DELETE` | `/notification-feed/{feedItemId}` | - ### Variable resolution Within template content, variables are referenced with `{{variableKey}}` syntax (for example `Hi {{user_name}}, your order #{{order_id}} shipped`) and resolved at send-time. From 9f988a610357f7079d89bceee83e25eb924d009c Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 28 May 2026 10:49:10 +0530 Subject: [PATCH 31/45] docs: add campaign image and improve onAction examples --- ui-kit/react-native/campaigns.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui-kit/react-native/campaigns.mdx b/ui-kit/react-native/campaigns.mdx index 498ebfc5e..bff724618 100644 --- a/ui-kit/react-native/campaigns.mdx +++ b/ui-kit/react-native/campaigns.mdx @@ -5,6 +5,10 @@ description: "Deliver targeted, rich notifications to users via an in-app notifi CometChat Campaigns enables you to send rich, interactive notifications to users through an in-app notification feed. Each notification is rendered as a native card using the **CometChat Cards** library — supporting images, text, buttons, layouts, and interactive actions. + + + + --- ## Overview From e0dfaca209ab3d2aa2623c2b9b69447919a86016 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Thu, 28 May 2026 10:50:00 +0530 Subject: [PATCH 32/45] docs: add Campaigns.png image asset --- images/Campaigns.png | Bin 0 -> 401902 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/Campaigns.png diff --git a/images/Campaigns.png b/images/Campaigns.png new file mode 100644 index 0000000000000000000000000000000000000000..6877cfce395547ff31af23b761e4c6e754f52fbd GIT binary patch literal 401902 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�(LE{L zgg^iIuT`VS9a{u_ZiAvF=3!{70vuFVwmqGt{kUjroy z0^@qXfQx~h(Gl5ujtuKKg7K$Rfm*F7`v|U|Ti1KN-Q!O~6|eysu^WBO-w!w9+M{EO zzsDm-X&1^AQ;HuNG(>!7xG79Wx~+d|!FzehG+hYUzqItxixm7(Um{X!hPdcBh*~2~ zmv7qzJ*z+=5?|d#e9;Pe11BK-{jEdTnT9d2WDQM?ZcpIx^}$hP0U;~a?rz=^ahF_e zVC&z2y8@WHdptSR6dS++Jp9OIRb`bty##Ci{4wY+`L3imd+ZRzVxDhE1SB~nr8I3g z*2DMV!;?|=en0$^k?9w05WLhtf(0f@EwtpIro};=w{|% zLql(li$>P)XJ|qHThgm&gF4rjZ03O04w^e=EHfu6!{uw+l?wQlQ z_`uD&a^czb3i%&2#WhGso)-yWa`FCHyl{7JYP}+`tS~o~X(%tccNnc&iBd2u)pqME zx|Xgu15;0UM@4B4l7!nrgdo`7#k4(%Q3&M+*^B@}^`G>6e44o9xwzX~A3IqdSDyx-+78w1c4)cLQAs|R$RZTQ@m zmn?z=@Zd+sIg?%&Az?w7w8Y3tTJ%pPDB*QeL2j=4ch8zBIR_&cjFV0O_84)A#J5$& zx89nl`!Yd+U-^T{F8R!QwM9wp1YKJUqG-;B&mb0GLn2lA}>J9`w}>JZC! zDffwdGiIKwr1s0SESUUE>459S+^Q%es}i8y%vplFa10n zrg)gi=I#EJHuC?WyfC!F^L(#le$m|g9fz~4-0iEW2Li6$J8H~jLl5{1gxx&cE^HV4o?z4D=`1|*x&>SSTQ|Mm@H zQdeBD+2S+AXVz(Md_!8JT1+ZNb?9vuC#eay=Ib>0zmx5=c32-Vr~=X}tJU3mtyqU9 z06}c9Bzs@MSbWKTddv?9f|A9NjdeF7)-^d5k(P2X`^6184pFBx(WB4CcDwwfHOw~! z-rG4!u~zTwN%o=|$DQdsVIR&oUi22|GG03GtG3|j<6+X~6B)VYhD(Ziy)qkl=BAzT z*Zhfn*j`rdahx8?}5ErL=#_6lPvn znYLBDv<877Pb*iiu2OHw|09;}E82q;b$yt@Q4F3nZ5XHwA979!mNzK1LZ7XS`{HW+ z3}3bWbC^h9L7@Ws=6H9l(d@>?F2_BA^)kTOk8;BGDFD&=TRw7d(=YdNr+NZZmzNik zvfnhdjoA-fOpB^@=6c1ivf@9R9amq6t*F*17UhM2Vv2wqC>x9LA9}mEt z-0~VfQW_L8^TJ2!Ix^4yORk_r-z3y5y=Jz%fwcQ(@@wlb3(L3VrpVm9z*%)4qik{U zf)5qa;pCw@R;HDX!(PA(#p%ruPGH7vejc}@6OE2k-urG>()P~c1nX|&xcQY#3Y%si z&xGf3r)hxY)7HV#%+j?s~T24<#=%-%|fYzYiXu-?C`^Xk=IMx{xcHne$`>D|sK zcA!JXUckORxnks+`z_D51ft`1cQ(9rm;O<6V9S{R8o8b6I^sRsgRGY+2C};hABnJ~ zxQxb@>}+xR;QBiDtf?#JAvL@_G7JbEDNW3tvLH^yMU@>I?`gyMhLC>w{QTrscLeB%I;PRWBo2Sp)I8OS8 z+mJrtq#_iUX3Sv+Yj0uX*{j!0VWp+k>SZYM(yzDMW$-a@5ay{q@_J9uEYU|a4k>MlnVKz4;!Yb?QPRQeSAzNbM6y!F zCAb||iQ8%P0n`k!*L-jVek|=Znb?)Gg1(ey;^^b1u3&=g{Qk2CnVI3DI$b{si%v?>j=U%>5giL+qd=i5Q#Uf&D`uf z2p>E4Z1Fl*kslb!ix$zh!+3Gv(+j@t^xD2rl3~@`xrDezUc%bZo@A%3%ePA*cY5}{ z3IL9HkQ^i+G#+n7vtm`faV>pzrg_G5cxC$6O#dwpkNOpRFxNVZj8o49iSv&_*(UtU z?lvN4@}?m$Fi<%l?r|%MHOqUv^m#DhSAon|23T<*IGv==xAo5u%SW^u*m8#nk~pp- zZ83Lw`E5TT#WbTaknY-=cdNl=xzJP7F&x3}N~-XRTh3yPt4bUWNL38DkHC)HXc?p;gBYXO6pi zj4cgT7Spr!6(BW%8ZuX~4<_DEkT0g?$ss|JYCq1U@bfMBvtpq$>+L2m1SSxGByu}N z%bmDaQB5&&bpcm+tx-r2#@p*qbhOo;{g|0+q!dE9&YUm(*gfzS!O=ubyl~kbEp+^?oorC?D}!VXd8+M_Q-U^#FlF!YaxWBtBKUZbLa1 zSBTTo<;-?3xLTyei+!IdhA{URL~6+&wnnYTj(C=x>^TlGcr5I`<{UK$Ze0 zZumtJ_^g_}ML;C1U^SsFMElnvR=Ib6r#sWORa3!@XK?*ReZLKH@_wHA{2( zO|rgeQg%^f5rSf#x%}d7!^%!}{yG#m%GCiEP0~dy|0}aEb&5Xqxz_VpJ5XI&hD45u zN`DVLC3HalfEbqA)B=D;UF+zGKZV;5ab>KKBROZA@ng8#d!neWLLo_2*=5F!-z?T< z8_X`cm6HzMXXmDJ*`c3XQ)G*Hf@)m3c_Nl#}d|+ z?UqMikPkwEJ+BDYc=!2`1+qN7S@ZSuYVHzFp_JV>n4Abz7T}V%amuN#Rm=GZ#8qiX z!u8tgTLR_9v@YzZC@2_!vEy&3?tQDN*&RJ+jog6~Dgv)&TS!xiNyzrRI=~6KF3;OJ zu-KngYq~XLgjG@%g@xbngUi?VfkVQiyTPJPRn(z?j@A^)!UxHu44pM{_wwR(c6A55 zu<9@=ysH$Q{}nz){6mfXwn&N}>qnH!WQVViuGuvJ{aee`_la{YDCLrMW)(%hWZy_1Ay(FhIdZ z{p2XJLYI^Fy>ns*vQ!$WDClzQv{Jm9MpBzo<}7;>(*E#}y;P3^%@-jZx- z3p`+McnPsofG5A)(S70xx%!-7g7nMCFXwiae1#i9<}rco>#TG?AK282xK*rJmARWZ zEo%|A#F1S8@Mc{x1;HwlY#~h=sdnL+a5t(u^79=K+Vy$f>C}Wcf{4AZeJ~ryzAUn*wePT;EIjyU z-x>WG!`B|ho*zXFMH@>#!LaXi^p9v7EOjJ0?6j_cxGVvO-y~6E$id&3d%}~ zXLb5eB5WKRQcI;fu)G##%YY7>yG(=Yl21rXRZ|$N%`>yZb&&Rt+{#xC zclmC3FXt0-ZXZ86y>~06)YG8h_7NR@E_ZaX^!ZOp`0w1CIG@nD1zjaOXbT6v=Q*J{ zkm?Z5{KXQ;m1KPvmZssTH20}rdFrXfA71lOiH=(L52&4)^9G;2zF)~i19d8-=)`cc48v92}Wh?cs>4{@^?>RdNvh)a|1dE;?EPil1N zc}RBZj;WDX`p92TnAYjV^uEQWi#AnBdz^lf00H}z3JWbvo!#Dz_auQQBeqwMo$!fW zRI*YN@mny56oS_@QO?ts#>#|0;2{TPXlG5{&YsDv z9a>CNclDG-!d9`!mLGEhUl*NQ=dRw=<#M|y9N%D#**Sj6**G0r@M~;kmK4%^;q55V zUr3}hd{I=6H%-siZ!rBF-AgNmxb}XWK(2Y}tBh5@E@K2>_Kgh=CLx~H7kL;Ix&Wo< zg-Ncuk7mWFzfc(VD>cG8=vmJhlgJ;7q0Odb@P}4Rb}Oph+iQ$IEL&`!UPwQyJv%m) z)Y7k{%$adTpF#s!)+@K3zC6>7R%_+vDE#TP5c>h;$q3^|3WV_A_t*}=*DsK3M&|v6 zbQ)HA`|gHlaGQh7u3 zA~3r}(iz7?V4Jf$@MBU@7%o8K-4u?+C5n6to+Cm0p+V8U>LObL!aQxjTUeh+9;K1K zqW67j7^on6cgfcP+N?C;GbUS)k`ijn8!qJ;ve}WPrcSWwIN6G;u?HfT3*Wj8(|rn$ zf7{r4&n63zmgg*IeT!c-iSqr+N6RVjYv>vU6ZKQN zr#29hernFF@?e$S)Ol7fP{k8?BiQmDhFS!>LiKJ|ytbX3#W}N!_kDqcP0%JIf1T~& z>1hn`7OngJpe6D%lZLLyAQuhTT1`*JJc&S3^@mQs87~{9g@NqwGPRODeI(WM{!qG_ z`Da%rn+`c`9y%BMrwa=Uz5RxlkX=`Cb$gf4F9&|0_dX4=V+;x`&$Iam^N4tIgD*P$ zQng4E6<0i0?`u%Y#4SteRE6{_{MHEdb=fwN#?`#D3wjOPQkikh79%g7J#(h-cMh+6 zCIlXjv_SY?(V%5j0bfI4m3;RNDtu-zxuT%4@}G~=GQ4b@VrFkS;2_6>z$;^}yrsc& zhj`yVn0?GQBptrLikiX0t!GQ>8W6)BdZLtM_mMZ|rX_A!VvNOnBc?RUPlihCRJtns zPH_CPi}{Q_p5MY!v18RWyiG}r^8}h(`G>VrJv@Aj=-1UWgUcC~=*UR7lCPDO`OupZ z+DWRrHQ&rtcsk%8g{)7OJ~+Vz@F^GRHT){7Y?0M{pYX|2JI{==(6pWb2H^aI3qHGS zfp87T3iu`Hqv*}AH8^ils_ooWWb%ktB_eNxm6Fld*IMh5ugtz}<1r)NBPe4dBHt>b zBlyq$+;p+6#~-^t!?KnrvEJcOXmg^WYfxB+|F#dTBGWs!ZhC5)WvMj%)eCBjeQAk^{hdmzqBEc6T?daJI4_yfB>sSl>9{ z%SZr@CpJtvqxjIT<_lw&XAC}D3g9TKS5dWb&ZMj5+VyY_D;jpr_5 zg!$RWy$n9nUvWZeoPc1v|5|R42vc7ppMn%ZS8oXJrYr;vY9sGyZpUs;Lev$n`ix~7~O5OKf zFDn2a<zn!eOVve_#0s1wm@$FblrShIs98$Hi7xsYd4Uo8(>=F}W z)}LkTBNp|ZHI4CDBl>3*m=nSfu^?X1ohOv^3o6x7(+tVGwhE{UtzLWPxN4rYXkCXq zgl$lWk{{~;Pqp1P@`F%O)L?!U24{z9U2BNys_XGo51qdkJk}U`CCfOeFHXDamI9cn%|A4w|>B3fF5)YxsIXz zLFN?p!N@jkwA;_R{`{c)^~~vV&#jCQiPk8Um0DpF4Gh3$<9`eQwQA{fd1>OF0xFI6 zMz31b5XW<(Ls<_dE%l8H6_BCa#CE3SVW-roiD|K*C?Nbq+_n@aDc!t2)TPem>1EX~ zJn1qomHv@pIEz=61N*m;m)mVO+tmeXdQOu>vo4oM`+^<294CX&SeiV$CUg7H%kGUX3Q+XKIgKt1OsM@JmJ#YE2;}T& zd`)b;Q|8?_-l{S;u^Nt|g*)1SFd}()xp}!Nr6i>JRUAPuf;RtgEba%TtS1}dP^&29 z8WQ|9Uvz!C$C){@MmIO3yv&BTbTq6pAci!=8VaD5k}DP=p-V9MVEmiM)dJq`9A#z!c&qpOF3s$UWy z%zGl4uEH-DXShurb|3N}4Rv258V8-;#`FrEg-x;>PS3=wEX~5gqcp;eb!^)K(+TNC z4K2U*`KT`mE1v{w0zvX{m9s{N4a`)fOtS%Bwhzzs47Wx@?*#Mc-Iy~Jc=cM3i?4qO zZ{(582Bi(!`(m@}x&c@-*@*?rZ*3;0BdN9UuMxnv9VWwsJJF;$50h~b@` zWs(j7txNfC`sazjw;t@=waX|br?>k;1#D~G()C}q5|@))hchW}!0d2Iu$WqsG;)xe zOPc%jo^0qCAIJ)vu&AQ1(O+Q^*GVWe6m6-`f^IOAW|1q;TdV8_QL(S7r~s#*v#|+g z>SKRy3V*>K*Bn;=b0R?EJ`LBjqWf=l_{Xng{j)`yd8!T?G7=3Bd%~{`Q!GQmK=$50 z>pDJHNZA&jS%MLsQDt`FU`b_cxwIC%Q`N#tQ#z{ThWk=|@cJ+8_cPRFdz#K~Te{2byr zaXcvMLJ#t%8EWYxKqnM@p!!}~?bv73jSiVfzpbTBa#uIt0JZ;DV6vneIPj2_B5#x7 zV!gda#QqC-nBN?GN-5AX z5KuvCq*Yovhfry-krqZux}|1BN+kv97(@Y;?q(1akQzd|r5RwTVPNvNIp^p(?{&Q& z{-6KXyFajp+WU$1taY!o?q@v*`cC(&f>KV;BM!2>2J(DC7h^k)Y7va~MiVoE>G}0! zL7~6!jppeUjPL+Wre+6VL9HRKtL{?Unrp&Bk~ z%$$MF?iH-qb$DL;g&HkV=8vob1^kN8BnIA?ybNw2)QD=6z0k514!w1|L-)s%8k%v@ z@{@MV&Kjnsj<`h3iY@b}g?+utN;fVy)foe2wSYul>rmXWwfJY-R3lIVNy>-^N{T>C zXUh2?Fad&jmd$gA56&@y(NG`-48-q8iv66^05b?qr{|DUa!_oYz9^JrD7Pd3?dEcz ziO0<^FwY@)EI}BdG*SbofuRR_EA{UQq?@aXsrVZsKv=!f4jDPV_WD3m*p!R8V|S@+ z|3S5{!lcs0ve*LspT)8ud}o9m-zkPkAEo45{es0x+#)KZ+6U)|l$7HN=>US>>F~}w zx}L{Pwq7aT@G>YZoLob)Qz9`h``){`pp%PVGo<__ag-BPmgVDu1H%yiCr zO$5Ikn~_^A^J{veJtlb(+!4AQc+#tp%F*VouI{??YPUKP$QJ(W7YC;|o$+1q2lDYh ztrdCwvkVT+x}C4B%WzyLy09zUps3H?XFK*uUxeBEke8@|PfxPmyuc>UR>`?L@jzpM z3?)!1ig(Dap#dw9LI)ytMpRsz1hRUY9}w7isrwes=eHjWJl~Wa+18+DR1!1slH1-u zp$%1a0JJ;qt_RI30sR%sezEg+2=v)vkLNSlug~Atxee_L2FD~CW^V%Y6#d}v)M}6K zG;2W69=`t2$<%2<@73s7Fz*n&A?YWn?Mh^fUhO6t)S0K+rhcg;`2G8k@wI9orvkM2 z#3i18vht1d`v?2D!QjuYgAUT}o7^v|s;RMwI3uYaY+G$sxijs3S*=6ndv))-AiRYR zP^0WG8bqIJ+&EAojgyBUbviA~<;fFqPGE2%M{nN0fB$c+t@St2T!S54Dl8!_S%*v# zQ&uvrzj{CWFUALerfedHbgAF04Kg&A6!WZ(C5`2!-Vm2dsmC_I%d}`NJQ$kt+;olr zU$r_bb12F>5p5zV#_JNKV>G^21L~18lit z0kLo_uksLJ|io@QtoNJV6ESt(Qq zwl2JyPy`~SPLY#QlUZwr@_Ar%fCJ12$n;BsDZsH=cszax;848`<7-_B4*t4P`S;F^ z(*D2qusqwS%CF3e2BjRGFI}|;9TOC7O#qZy>EvMKg9Gv*`5_VX{3#+V&Vj!5JK&(} zMT$gyhw8$Bz4Y?qH)`*gOf&<9)Yg`AM<45U_ZWU~20VsM=c4hoL)A8*#Kz08D_JNp zgvS-d=|}!_QZl#|&f~sal9`P?=GuuLiZsYdTthBVMT8H=*umbOep>J->yCf3 zu)a9Uan5$Xr3Rz}zRGE6bWFW#SYGs6qn-g5+|T2M&g`lCUV;?+ZE9ECAl8{R#$XWy z9hr+YO#t1hs#+kW1Ssco-__C*DD|Wj84K>)x68|IX!sv{M0-Jh6}xFI7(Jm7NS+kS zhaFB%zcwN+Ule@GT)v`yDxm=mRtmy?92U$g=8t6VNlejcURnbs)05+?odbYEK2|%y zl*$*AW5?<$ns_tIfxMr`_g$~8al72A+#w(}?jW-Xhz;(0UmMxd6kranucxJt4z#qW zI>0fu`4Et>iBtpV%ZqVwaq$!)g8N(L-lH<`(BD4)^0|Kqo+Vk7tEER>}v_YV%>h+H&<-jxyjW`48J%C7(NJ1#ef^0i67>v#21?w9wz%qV$ z^lR>G$T+pHI-&TPtE28e8@it~Z!0?-&zEE&SI(^e9Wgxj%ic4xZOH1)&b;xH%#ouy zK;fnLMF3X{baY-LG)F>&<(NMf2|reu*?)6^0Pfl+3&6zPsN*=USP3vfjsL0z^5oGX#Wq0O8&bbl0(e1g%>Z9- z0+?MOKX%8Y%-ZkSGbzav9@Gq+T`E`lr<=qSfqhw?#hY4J*mgXD!lytz$G)6ZfDCc! z79(=xyN05urr5wEl$Y?bHJ{H1)>J%JfkD6VMROT0WZ9dwwk^{B{XY4!{-M=#X(I*z^l z?Cm$Ksw*HHnhYO#0*GsyYw8j=Y--#}NJ zmVvK$jwn`ej8*ktrv1fH>pv!IfvWysK#I6&I&#OqX{1pmn37n5*KbF$@-kK8%EIXbz`^h*jL)_X>(B}Z;lQdmM zWw@WuDJ18b2tY_j#NBK;0O;%cT|&y^4_0@x>D~ym&H#nd7Q!y!=ouL77!OQz)vr@UhpU!o50 zFu z=Hs=OdJ+1O?IFZ=iDS=eB_MnnPoBk3)iNd;Lmp`xV~F=^eziB*joHCHdAD?eu(~`v z_3KS{DEkAnCHHY2!J1p3RiYYVFm6AQ{x(R7i7l$6sVrCnC|oBi?LNsV(qyRkQQKFF zE24yy=L~;nkLSZZx+~iw!%p|q_>FBvuunlQ)*(Vlqr0fTB#|n|+-aBJ|I0c9Q|Kjk zX41nGWVWXXazR2?C(>U1eOX*ROlH6N_vJMlPxT#KLy(vxSldKu3zt z@uo9TLb;el=BYBAd`dcgAy!qeesPpv`NO1g%a5#`N zmR&F>uXaeEGEX=$#L&DDP_=ZI^f}-Nw+?isWG8lLh?i~x=#qf7=a^1QdDQPb7f37Fk0?iLlCpLQvUI3DW5SC%W% z$qr6QS1>uVS;9i|d8V#)Yw_J)4Wtni3cLI8jUhA7#p+%r=pM7E|C|C7=jeVmRT(SIPfE^-c=SeTw%?}YTmrLU$k)#!6Jsi z*O54VKhH)EAzWSe5cy(Q#UL~kg=IGM&dlG<%2vs0ngtJIWelZ~beoW$Iy^sP);CoK z@X1YkWWCvtKazRB<3AnS4HFBxg=j{@4%YA#!8WsZ^)}X>a@2M=}YhLJKlH*nxm|+XC7TZJe{9QdAkC8+d2eAZF;47_6=| zE(!xa#Q00F3V$5HHIetH*$AWsBwChb|pkKj5Pf$`>8@GKon2n z;V@EA?MfEj3xQEU?4U_3P2@-f!D?!0MKfu9GFt@y)?0;`3J#i5fvpS9zLuRi@>d#$ zyz`y`W?vcDzVYf+bK*|~p4<&094waMl+}rNfGc%`$0*;{ha#F#pr-riZZ!Bv`Efw` zEUbLf@~FxZHWiTQpQ(EqYBLKuKxq|Vq>LZUC{Fu#N5?7xW7=ItXw71XF<-}GnWJrR zLE(+AX%Uj>6;N7WRP0u4rRVtYfn)^k#0);m*YWE5Jx^X#=Tuilw+!D;(cqoB zLpfkc4{JeXxfK0xf=^w5B7%%HT6Pu7M>p5d4LILCCBg1aG{Ok6elZrG{gy<+A`EX4 zp*aAf*bm!;1&i_tqiNP>k-N1~5FD`=N!Y+k$C)7_`~wKeLAy*C+0J%wOjYey|_@K_jh-oN+3LOH?W-OBn>9+-n~7B-ix@)3P*k=W&Re{Tiy2}kGK zY%v?5moQtli#I*MG#{D~E%NA9E!Uf(18YCigS(saW+yuz^F0s=wmo9u7R7K2Twa;M ztS=-nIQ+oJVhuKHpoUj0m!6AaN*;ImgV$#@iDR*ZOmoQR8a$wqfl|)4YJUbh=Jao1 zrtCO9MIPA%UR-u5I&70xB)*)bBX3Cf6KB-(8(+!{kxzFpdYgE zJ$CBbTIvHeTMqA%FpF?c#D&Y2oKZ>6Xyr@eBMeOou4}~DZ)KbOUq9qCLyNJW7yOY8 zTs270^A7C#!BY}cmI^4&xnx7UUKAfdcJMzUQG&^0+w5dAtb1D$fQN~Tlqy5PX55Gf z6a(l7y1fZOhHm3Eo;-K(!{X^2cAaQl8goO0Q5}8@k3q4|Zr67d z?R|hcA9tNOH+XBwe1X#Y|XgeOaO9AAtv#B3yI-D)Gf>MMl ze2c?EvzO$J4b{hg57YjJ#2?V>by{g!b(fijEKFEvt8QW~hYIkq3R8UQKK+LO9k11= zHMgp%5)Fl9Q^S`U0UGz!Pu^3HS-OXe1ikutjYivieawQmuDI=`1=(kge%P@WK8hMm zC@VANQ1pL*5be0#(Zu)U`(K?eT5K~;#+e67kqRy*Yqw>f8hf2=Gug%gC% z{Jh{gt8L$V&*^i||UmSNyUL9ZwHaU%k5eoR0)0e-k9{*(A&RT^E)l z&Zv?uM#hq7^7nl)vE*;Cd<10HZ3RU{Pq7ox6Gn66^{6a7q1tfa-E1?VIC*x@Xt&M5 zaG1Ht?h71m8x{FaCs)H>{RfU^)2o6f??e`ogDpvM;m`B6&lstzGi~|RbFV6l30s1R zgFbh9lSg2=-@Gec`tnFcvp_eHYgJ&;c{i3mq}Iqoa)Qip7H+ zL#Pfs!EHa5*vSAc3fBXa080)|xpOE8%nkC#g~5m_1OPk6Kw`TjZEJ}C3=b5XdA@?$@glCq5;&W*4~-{;0Aicif+$Qy`ja=B zc(_eXrkey{_v$TuC_@)MZuZcZ+?^n5ihzImfj@uL@^)&G7*A*>{z`5|PaNY{T=N1o zF(4;jN2PLK2yuY&x15n|+Mz4yD3YC(I%m&J~C8sd{M$yAM@*{KS(?mf%xa#!SV_k zg15>bni_{%t@_ElbgwXbHParvSt2yix{r-dP9pP)jH~Zc_b-jjVhl+EMBI^B(B)&` z2`I&VYkp9d|Nc{kNRn?+Bib)-k9f`t8^yKoM)qdF}|! z&+CRo^FP?T`8BQk?d@AM*RDLL=$~*un=;!=eJ{XccW%zPSwm zxdbRM&dna<60msypO-MbsAsZ5E8O8z`_e5fuEYFMO5GqI??&`J1r=HBjoJlAi> z-2dP|a{~Z-o}#Zzb4RK0AOzKmgZLA7rmqUmPXO{bo2KNuv3i&2zq`2B5f;vWejhM5 zEjCzX7RqnQCPqK!fh0!bgMOozJ!Dh~5w~!#gCwmrNM{gU8$!>54s#jwPY&wpcC$Uj zF+c;7&x&SyDHF0ZdA&J)an*#wGyUNmhw!Ku^=$ zB9?&-JvI=#j$~3iO3D9vk5Ld5m=TLYXbZWoc$Ot_YW5(0_A}6bJLrMs7H*oHP9nZ+ zq^^5#2FJTo2I=4;QY&dpH)RkrzLmnk7-1cL5h`_aQ<@e+D*`%OoNdVY=Is@vA$bqGFkI58 z0W!5P`MLu5p@`|rMAAJ9I|pzfJmE#v#(lPnvs~hXo&9CN{h1CEeMZ zmUz5h3=do>-t3Z@1u)+{CFwDrR;8>S-`91Zgb+IZ>qj_F6a6#0%n62j{?rmfdP)?! zd@8=BdPBGU8on8`oDLi;C6}s^3X^xCu^Z*)B@&oHj1i;`ak?u}>oJ9#a6s;B-ct=mg zi611K;Qf@0#)D=HkU!ANVu!OsOzkRbHTPwOM$wXs_y)8e)HEA~HQ819n3ZhU0ZUb% z+4~Uy{w;0wH~u4Stx&+5_{?oeqh?4Q5E^O;UFDpO1O;}?)<5;!J@!<^@dc9HPK}k! zGD+u=0gzg(2j7D|z%${d3q3kc&?YN@0HA`%o zrW?oL3)k)dApY-5S&{0u$u_F{{{5DqB?FhzD_7~kL&098`pF-L(uccxqXnazKKKdN zpwDqtaZjqH#LhZIHcxpNhE?9sIid4XKiSa}D$}v2r;onRmKlo=QW;_E0 z*~O6Cy4~}ZdC$KfX7gu5l!m>tHk$OCdh>C3l1{mh{lk89l^;p(Bv zP=Xv8to~0Q>%Wvy{Eq*=%DNGr^;gLEpRTQ@_x_1G{qv*0Z>!h;FWUP5qtNl~v;Rl~ z4=&HM&njc(YB$rvrTqdW0omhU;Fe2A)h|3W z{=C3H`VL1ZpYmhPg;hsDcw_Nwr+oKNkG8E?ANvLf_#)ec}yZ#uq2W*DjyKhSX z(M<6}*M4^Tp1yNYOYe1I=Uq&VME&;F+bdi*tAiLfNjC=P&zAaA40rMD_UcFZ^U_lB z@0uMr3~Za~3Q?n6iwm!4Xh~jie9CIXPgRb5#2)=YQ^L2~+1|qQN_zV zS7mD6`qsNmbG4&T{)koIw^;wHdyINK&YwM;<_VLHHw)HkP>9}+`Ld5zQc91Lwq?Ow z=07?M4>1XDn}?9Ff_;fp26DmxIZBV=)bt^y5Yx@OVUVOB|5I#bWdH=nOjh22 z-v}TD7>n;v3qvHzZ`Da*j@%3~l;&jlAd=;DG|KBp31gWd_&|AHlwl2b>}dYaP{*d` zy3$)8J(Ng>{IlIizfc+D&fd@`HlHWoOr8rl3;X4>AHWP1Jk;1lfCwoB&eY6Sa?SqA z@DVY4{ADj&sl%6*$^jSdN!fFg_J_=yaIEKw4niMRTxfP7*D}ESvLy})|GEn?tzCZc zeW#5MRxdg2x>jR;1mQZsr+7ot{V#d-Lsgb6g|2#UEDVN0oMi?uh)KdyRF1ekM@9$0 zN!_%!^yN%K_=}N#-M9R9Q0L5{^3<)@TXIY`na3N_lMJa`N+F1AI)pjH^p|fK$!Op^ zy;B;NZR|xj!W@t%>0lg*Uw`?;NQPYKu)t5(9z68T(Za5!*x=5>po{hWsga1$MazeA zd)N1f)`zKHpJ&Gj#)O_wSTyC1x8)iVbaccs`Tzm$w_zhmkGM!*4*i`I^3VF$@7`Bp zqt$G9iDdfZ_%W3V(>y-^QTKFS*40C^hN;@i)&4tA#8#}{&i;7xt)=9Ycfy6& zp|agmFv}|m44PV3Y8zhfOvu+J4zY@6-5xQ%?N~}*QTNJvQ>h2X^reWt%YsI>Fzz9h zv9+|SqbJNlCn8VZszOhrAPBK3LSq54P+dq{us8QTDJf{s($}Y!(9uNy1;ZVl=?z0= zZ%-EU5gN2Yj=yZ;2#rNYNI<3;y*B9e+ zyt(I`%+3@GKs};_Y;>&8$cLsbIj*ndUDe5yrBQP%eMmWN&{o&?AmNJpLvo?C_X`EJ zyJ4yyKH5aXjV|X$R+N0Tq|;@w>f_6ZX0=a>SOP0hfgh!RlL(!2)~8HRJtG(K(dV&% zN=fWPo5v$)UhNIOC)Z@_E|^WN@SWfE@tM2sSN(HygwaWm`zct^jd#&+Yo&1;B1G^y zbj%-D9dv1{B={E@BPI&THc}_Tq0GjV5hcp6qKmlt_RsFuP|G#nv2K6b*?w`Ec=&W6 zu(+iq{Jfu%h`*kHO#0=n#&2U}sAIRZ;=yPNtZ_A3Vs{b%Jz+;5M(4hHpJOd_UD$}% zsA2n+gi0TqEhVLCzcs3ZDa$=2k<0Ay(E?%MtR2>n+&xzMvmDKr?K$OWB`2xaL^!fA zs0+Np(-`@lo@%#6n-a|ur}y!Z{b6;9epf(6u~hgDh*Ft6S8erH<1Dr|YPtUmNv1B2rA z$|ZSss@~cr9&D_9B%l5r{%bgBvNFx&7WhS7^3==4Jd}9AuQR2tR8Xtz20B@eoK-LJ ziHY{-p3KwH`n1F#-minLu|-$kPMp=YmEdAwb1{gLI;k5V-v8{Vt&3lnUA=KIWaGab7IbOa9drfu~IDB#*}1q{IU*q%2Ur;Oc9B)tn7EpL#=%1?=>Q2Xqaa4 zcecy-E9KypCqn3H+qS2Xd=^bfN^-cp*jxr6REunV#lG3)BNFG)B&%uq8ej|!m&M88!&;ch#QM+30L-;>x`?_ z{sb!+{xz@uG0`3JxR9?OgUZM4?H2?Yo1SZ*@-%xlu`5hhHTcvi{`^Q$7gO{z+WRpC z$VDglmQ}(vrwr&R2jA0snHBb4qAkw85>t=cRA2rW?3~)~e6{P0HT>q~#kfchlZyS~k2&9KuPCZo1gTJnVk&?8jt4 zzmVXJdQMPTYP-&~Ep1}oZ0Mdx4kE5yhN8mRvu;iupw&y;Sb#=fQPu*ca989Riy9C~gc z7h6?00>5wCXURob?KvJ!tbo3@v79sW&2F6!|Jq+NYd};f*ICK`@=L1ob9|Dmydc!l zpEF-uE=*wmUW0pZZ|*IQ$EM$>-b3UL29uvGJNiQIIB{dU!5&|}mCAB$qKfRvA7ISxrzWS=h^D$Qq{}23k%s)P&ELiw*Q+h~! zVVgVS`I>Ir^mFA>w-&Hb&e%+-DeA+zmz4tNgGu%=3v)F)&tV3K&>mM;IUhEyUiDzf zvov!P=MO~)U!~G6ReOE9eQ}wORtV)qxFHe=1Q(kwI8cm&Le$@{_&bDv{P=dqXQ|6x+103 z4RVl48d;S}ASTv-*(f>7xKPWXzCDV}420Wi%QY)J-fWdShxkaC8l_s%_S@frli5N? zqCD*bMu=_wATv_zdE+c(cPR;(6lmP&?y z92vdLX{($kH?bd`pw?>!3?lJ-0jo7!m33GtS<~C+S|Ks)z&3wP43wXZ$wQZvDNk1yd=uV+)HlpdY?De=KZ)OLr16Ws1sJ4h6?{_UWII;R3O)ZyF zRGq2IZPbM069%zE8`>{3mv;MP%xV_USP2u;TQEP);U_PiN&S3MRV|)Bp}}Da*Ek%h z>kqu{+(tT4dC~-*Dn!E=(GTq1O3po?ia;-wLR7~dyyw51_jB0})mqJ%p53jTUIxk^ zcTgM2v!|gjbb(4~vr!_r*$TN=y7a4v;U}sj-X|KjR^_=|PZC&TeO4@#S|np)8l*Rw zA|1yu0zdTya@ExSiemM-bU9n)HUTHGn+5MFHsZo&9IEKkB^%mV4;BQF`t7#s*`7g# zKb}tGv=VCd>_6N-bFV`_F>jePLZy@Zf%{=!MTJ(H2n@=u(04%MttHaA-W>WtxKIlm z(_!BuLnIu^yKGVts+I`oswqpc%%z83EXnHYrye#3Ja> zYNKIGp~n)&OW*kG;`5{B^9!WpfoY4cwxgDuLb-@XOW3Dp*D!q_zco9~ct`T!{f3P4l*9o*gwGZt7}$V)8EzL1M5s zk%|3OnQKN4AW%E@1?|7)O7^fO=(qMk2D@iT67=?YBseRkkJ|M`tPX@-D|71x4d%xV z9q&D*c@oLB+iSCrR60`wrH}68Nt@HiR_RMeq2;G@|Fi>5bR=e~zs7D{QdQ zYao`?B-*b!XS)+l9n5+OEf+UX9IW0=v8A*f3z?ibRi9ouO-GxAv?{F&yIT@r;d?@m z;(>#1AK}m=Bu98arHQkAPGXh5xOxs}U!GN*(RaQKCFOH$pGM4F zE>_>IxrqV?x?Uv?R1jfuI+`Ls;^R(8$MAPqfwtogk~=kg$$6~>AZ{-dt0p{5_0UEt43aU zoY5GiSlLdQq>(Z-=ciKa9q!u@URoE?XUh@kd1@}1YTO%S#Le5ZR|uOzE{(xs@8(nA zKN0;FUSJH<^A!2qd(dIiZ{Ih!JlcHrdnuZsr1ewhz_Z>Q`<~XPbi|+}?ICAGY*-cD zVnM-bhH~VvWd6YBfrQwKdb>&Jo^(Rqs*6`C0aNl}+Wx)!rgQCqbjj`bYKaCkSBAVF z7YlKZGP6x09IBLOBcZrJs1w7Gzp~=XCVDIvn7)C$#LX;EbtO~u%CwXGZ^=a5PMd2D z@m<(Mkm1`&#E3HTgek#$vBaF*Am{kh1lNT1e)Xf!0_Ux>uQ!Z7T0igg%TkgK#>w<| zu#s^f@jH1X`ggeLY{C^Yz(vE;f|;929bVPB5E2C-o0_-JY>f0G*hZ?#-pYmE<#!Wu zuCaN`TS4!zK8h-OUlVwCAyG4UNwa@Ltj$G?7l08SevC_93hk4;Q96-1Is;!`eB7?N zPPkQ4c?o+q0vg}4IU7q^GxL(@+jz-`d{S@ZB|A2F)A_+H0eDBg^ z!O4}P!q45cYOi^)qitiBiX)cFo)zNyu&57=z_x!m3Dw`%anU^X=xLv59s3Qv2g#h0-FXbNUfMBwX2^BrE=!v$QS2@ndlK zZ1Fo?UMvDKN&SS4jvUm1F1dMQrk#bfIAm|>DLI4F0KZx8;mDB6!H?=#-+L#)g<#e$ z*}vEbQDB03OG<1Q&MFOYuach3mCSCd(fu}A^&W@h=6^(u(cCK2#V|zEA?*gBnobJp z_vN0tBW>>}C7e-vzK|9fwxqr+8)}((O0N6t$g-AxuoUP$TcK4T$qE*5!ghgg`L^!W zkv>Ouuc`sH=3lU4rwhgUCDhaAFXEN%;014l4J56-B}t_LGI3?I(wL#15-jZ zJ$JV7{M0?`JcE#~*VVUlyS;O*?k`yL8NF>8xNOU za|K>%Ou9Q6j*NX#Lf2S43Zwst+qn=n)c(xjTGEbD;%hYr8ZbNEEm;E6mgai$t$E=5 z*vF9Ci7@-JDvWzrlI=HZyG)_+hEqM>Svqu5%S>OA_P9W!kJhD9r5@>~)Om3qs?<@L zok>Pze9!nYkUB`o5bR+}oriLu?;YLEKg)c6Sz5*^B7UR)Jm-fAcewYH3Gh7=uW}g? z%u|gmbd)E83T5mW>DGBhFg1@t;DmWW_f7qp`6}1I1Tt8apoX_uYNK!?HR7dP<%>d)Om$)l^4i<#*0k(J zi19_`?CLH&Tzc99w1;0-{cZV-=*b|tf21!*$X<>kEbjq?6)ND*SSqn0+?3_-$?aou zxxl$~$&0eYOU1~_k7kCAH+Y$`D>C*8fV-bgu87Z?s+aAvwmYue^^v)wt>sq``vo-d zlK;ZwL3*bL;)B%J+VPfZ-8;QK*1*B!rDXn^_kPpTstpug)xo)uZnt7~x_GLQ%2Zym z;u{QaV?CWuCP@f>9zIcXtxZDP>4t>2q&w};N8Xa<>0T=)%JHmeK34Lk5u(JieHSM} zo%GEMII_}1;nBk{T#RKUbSEq6uT;wFK9DZsK2@Y<4V#_#DQ5XS&#quHG5n#Q$`06* z9cpl$MpUCp3GS?-+Jao+PECJ?h6iY(~MmRPi{Qr`yLS$2z$65#`*m5sk`bH(t*UE?R!rw*jqR%K{MDyYhtN9S6)(JdrYvF6}2 z+~&a@k)uu8`cIF>=aM&zWo-l&mG-lT5a~V~0I3s&I48u)#9tHby z*mO?K)LexsU?GUPt&hn}rmW15sXucfR^M9AsNFjEa>HQW1JsI;SUa1H3l5%ZZ0jvr zaAU)it3;z|#F&mILYXbdAT7(B^JVc|Vuc#n~#TfHF!**>$kf{NuGZ zPE?M!Ugly3UG3Oo_mj&^GQXu3aaYSKil>`TK|e+#j=GrT23=YX@Dm$T2V}AKb`$D{ zBkf<)T|2%`x@e%b8OhdTH|5uJ!0S&Oz_d?M!V@LZIU%W#^Q@mzM&?d&(Ua1; zC~M-$aFin}9VDbIT)A!4S*!Kg6_eKT&x>}w2Inj3``630?cv25ZEQV7>F*fTYXrhS zFtWq>?VYex#9vqeJ-9&V5Z|!R!k4JYo$jbycX=v5{#TCbbRM)P*qwt%PLxxXF|xt= z=e`)`OPn!(zs;VwpclWL67@Olth>>UK=g1q>;tzQ`^(m{@5arx4l>4?>T{yu@AQ?M z<-)(Xm>nRs3cQTlvpq9L72-HsyK<}DG-6A#xY*lW)FZ17!gc*eLxZ~Abq%YzbQ`I> z`_5CM8csGRX!$Bdk`#7Fb3GqsXihURrGjYSSk2A~3ca*vXE>SUuy_|P`IpI!q$>Z- zjDSO)@adBko+N*5qL1cj8Kh`V>QpqUSU!BeMTMhr$=^vOknNT)Wd(HNcAxdTr>in1 zwc-Y4z}c8KHldo-yoGrAiS3e@+m2k(w!w*;TwMNhjQh6O`8yS1;{!5ptQV2m!9~}< zR?jBM2c8jhX{KVO7vFM*cm{ByCQsM%!ajcPY2eUhu*46txZ#B?!xRxKo1{ioT20be zxgO>2tG!(1Wzi{5m+PMly-xpdd_2RhbH?&zzf0~SPqG#jwE@el zu|8@ua^5SYTe2v0i%~)i`IbS4QDVrWX zYp!qd+SgX7(mpn){Z~4iV_%Tai4Oa6^7iP zq7$6#Li)1Bd@C{1IWr-^_G@I$=c4-1{0Sjv+L5?Y4X4(b%$L&c9HLtqNqgx-ai!H7 z`SXu+-%tDu*5h9bvYGB5<&mhj+@ZIuSP34LUpwDiymB84RCGwFb~ci=9918? zY`Z>{(r;1}{7ht$-pV!ex^baYoP)#RU9I!btG&IXA0w?4U1Za|cKVt{M zgGMhu-kU_@(9}g5otNmXiwABd;vHwuV9E`U=(*ojc(Ff|5FIwXkRo0y?4-=0IHX4V*-YUk{gf5tHe8 zZ~SvX#%dienl4i9$EDwCkQ|}^s-G)jXNlM8V6;`|n;wUb`r8w$KAG`wHjpk&`k9|L zy=2dKG41;aTL|+~m)|}TezQD~YI9iA3Xke!VJT}We`S#$k|)Ju8ab6{kMBk1QPt^} zTJU$S9DMNZm6ju|8@>1bB_3y-JsXU2hBP(>dq5~dB3TB^&0eBn-=>@&nO>)ez8q4x z7QvSaakdSof-J2(`5DYd?=@fT`Dt?y2;7Y<^21ydyDGT+PCe@izH=G?sKd#jY zL-5d-U$!AXbf-%a+ab)Y-$+uSmmV1nyFZPlbqpy_yiz)h7Ane-Nr0%P;WqISx!SDN zp~VYG|9tMHQ#MY;Bl0_wZ(FLX718zg#A$?4SHQVZhKS>da;%;>h8M;!i@eR1x=&~ zuP~pZE=7!g(yj4eGnBGvhQjZBM4 z{QUdYIW@$vCpEowM(>B=j5agR5zCYc|K^4xfiF&=Y9hvL%k_PyXGUG=%MY4%ZrKi{ zOC^&ZQ|IcF%|a+;;V)oi**)i+=-cILB)&bHMf#?;#_00xz7MeiIY_pBuZW!ollxqM zwFC#r`7b!LvOt0|aYf%9Rg1m-C?0s@GFRo_gB^>8FZovg*;HtL`Cn3jImO_rAd`bO ze+~;BjZ(5VGf-{({o|L7Oo7!`36?D}{e9Qp5g+aw!YG%H?u%zWl&WW$AK_9jD-Ye# zVd%AbU8!|vi9bB#?4Gke&xiYuuJQ>6EQ-@l@96YQlg^HfUnXnhpDPs3S@o9KD*RC6 z&zKp>LL_oI%~uY~V#sA9vmOZY+u7SUE`BRgcQfvO2QhfG6|udszY~^z>V<|z*}1a2 z=41M4;m!n$wr}37VAkfK`TE}iAH#VO5$jxC^cU|U+E<@#ToZA{w``HcU@H+vVCtoK@>>N7Ndh$}GFO>c8 z_|Yd3-e;RH(pj#5Eqz41C^P{QRw47U12Xz(wSGdLBPgRyj%5euz9Ua(kS=E_TCHZd zjM$2A@2lQ;KwUX0@8=KYR~4C`d1goJ9a-J}vmkTNmEb+-WJ~g=3ZHI{$(ILH3q^Oo zPCR!OU))45=7+gd-HwH9nz2Zxw&}ga?_eZWS!scYW%KeCv$UN3l_CW<)|G&kYcLVG zYu^>H53duX-4cFnqI$j~-Tp+NPjz&TLs?nfn~sqhi3agVDHOy;Ze)(s@5&%avmDcd z!<>6iVD99rAlEd~25~$+LR9e}PiUJJQ%iE~OE;PyO%i>B=-@|f;1=>GnhYj*^R0+l zG<1cdkotKEtMOBd8mAjT%!Zf{*Hi>o_dq9^R_Nay3S9qg2SLV%qPxm}o`|Z9WEzOn zq;bIOOBtr7UikdoLm|A6_I&E2Vqf2a7Be;M?S?9)#OR` zCnHZ>nxbgvTKmYt8A@AxavQOquL^c+RU5%9y2>TI3i%r+WTLe@Yu-2yzAmEdeQ%_w zeGdkv`pCa%2Dtl2<3e}8ZCA(*Y-&4Ja@b#eKo3EhGagt$>?NwXB9<21J6R*GnVd%3 z9n#mUgIULXBxpsVi=ouUY3pU3{v)w>)hBa>-hOm26D*bBbJlw$&|UhG@e$=HN!}q< z6m!^hekO(ngY)g#5Bxbf1nD1f$cH2l4O>U2xdUHwNZK5lD*y4(N;7p z-8skm3BC5XCZgf>9+Z|L!{Te*@Z05v`D6y z6e%5j0O_gP9Dhiv&1G!-lvj*Cdoy5IZDsBJ{(b~q))$;Y0)xBR^*ev|5E?T4N2czH zI)79>lgBs^85ycsLHqjc!7>Ms$O!k=8a$czZhH^1s^q~ixOHoT-KnL}yk)iZT9nKq ziLr@qpJdhJjkI*LX(ApZe4?R>mg%CzyUh*zW(T37#U(vI$tb#Ia=)2T@Yl5~^>(3I zh!!>be{{WfI2-=^HQtsgB`B>uTdVent!AlRUG|DtLF}!lBB`3Cs7+hciWPgr4z)M2 z_lUi>C*R-od%r)Q?{z(ozw?*edEMte_c`YdB;)~oAacWzKYdx>g(N~!j^0nzPEN2= z(b(&IO1W>PPCf4^nu^Z;rjLp09{3U=#YG=Hs2$wtB0Xlir`HK?U7#s1?Q+^1hMxGpB_h}kSP1mJEW7b`$esSa`>DOP=y06O?uOQ3r?`+$Q|0kTpY zyRRks5`D5su2~66treg|uX!012d52LyzA%j>@O7(TSZxUQoK0VcH0eL5L$#HSuv~; z{0WMe_W2B#Q(UixV#wX!759!cD^)LK3+#{Y-qG+fYr-@wbSi$<&TU)Wlv<4zuq6+) za5%rNT4%_Gz%nwOt!t+hKtDG3*$-+0{ z8&MH^mkF~fakD_a)hH(AMjQ_6^^kE7cofa=Su$I!a{+c(FTKQ;I=+c{{yS3{Q`kQ2ZDoVfQ)C6GVjdNgo_JdY^uRkii_d=tdsJ_Kh@kGz z(l>cIx(y!T0lrMh)(!sgt4wj|@248tEx(n5Z?nFXv8}{hJgDgtj_EO}3*5)Y4PFNN z0j{%y<`7Ed!m6pa1+(12TI@h0$Ylw>RJNMojXk?O!{2mHKqp;2A3KshSPWbGs&}h?mp=x-%+f^CP965oCx8F=n`XoQR7R z=#FYnjE6XvhIPH~g+BwbX#A+V38ooKD5ZhuBvjg_oDIi?n3ofSETFz)%%cly6@XE+ zRH}MHod21}rl3#>Wh#F-c?fB6#8OmZ5Ks>5YOtE|kagObpXt_5p>=@bPt@W2{#~}d zLW5RMES>)YuC&E;_=J&}|e z{e;&Yk?m{S%S(u;N^8`0&Q)nPg}gTMTm8XOr{Seg&uYogTvKB!ZC?UG>nKCu;gj*j z&SUYP3{UiprdnFV3%uvIg!_umg_flp9=l8@=N792uX?|OzX!`=Ae3uNzBH)g+e@?k z)7$-9;$9V_sW$XYYHlQN`uIcMllK#;67*FKIdox@}2RxLzG3mIEe)73|m%M&=Xhf^OR` zTYKr z6slsReNvCZlxPpT4Zjy|w|uY-@nn_ZrnQdK2RzX$Et&|e7vB82x)F?M9il7LQqnhg zTZs9-#r%5saWAjZBl-i*xDQoY&nn+sx`e=?X`y3xDl2(j-_OL_nVw4;bvQdcv5Oat zVHEE596}n4YugV<ZG(e>e=Gcfp ziV}pnW`I%e?`;p|J8YV}F|tI%e_jr{y=Q)~IIWM0PD5MLj-ZSzufAyYR+1{bj;BJC zSHpauf6go8(%O~te_*uPYaEdVQVH^xR(zX3kRd;L_S_i2lcPTH9Ykb>I zY28n8Y*{Y`IQOpy@D@3+cJZ#3Qw`8QrCsIvdk^mxEvk-82_O{-)LLhN`!MN+qBd8q zIGwX^1KvLs2_RToD{Uk;Kt!%g!7=6j2- zgQO;Bf(pOh*e;#h0#%-7dEOdAY@k~iNoeZZv*GIN?5d@{s(JwA*O0C~QNuhWqLfBf3=6N z<0zx;l@CAcBHp*;hmJZISZ+MzxjW8~2c)K#iz++l{{Cm#1v061HO%^a@cP0;3j^=|IkLLKrUhaj!HyGe=V;5ZJOvEb#)?QU z$WbS!RK!h$`r8k9<*Q*vJ?j1Je6S_uZT==COx^uv0L^aS(f3^5`Pl|j`H_)#SIA?; zTxcPRm14(cUM*p@GcjLCvlgpAGWKZ~M%Mi~Y78$LIY?zd7Nn$ZYwP$X!;qqxCR!cz zIU^w@ViujoRiLE{eu8o#N%6SWf~ns;(Kt2CmsHQjJbg2o8;{xCwody zI~Cg+M1Ww^`)8`+UJG9F^h6uo&dc{s&?5OdtMR4+qm7bB(zFTEl2Hs=dk6N>nWL&D z?l(`FjCSUOtcc{D%>Cml^RP=IkqlC2|a!3iS!va$5beob7{UkIC#hH5Pj4PQpb*g0AUdKyDQ%ofk< zB>Fhz^7!J${_p}@vKu8mP`|?A&(OecTpYZ(ct2uHue8DY5%~7n_I=ZL(nGb@-F|Zi zjqi}q={e)Y7y~&{fP{2c?$fXpR5rdrg`|FR4rBh?!Hwo8lzKUR{0RE(*Z7jH`4^F%87D< zcWhRh7|e1!r$}KDaQ!ZP7!L?q$t$pN8mT0GT}ShAOw1O*3LkTT7DTaPyPnv#5)mE( zpY*||W+WKbR91zAl2yjB3U~v_;nBV0#|82oxKvD5E|ac`&Ef_N3DV|MYug~PZ7S<1 zhPI-)lZU*B!uds|@PJ!d0;8!3<88Z4Uw%HpIIpX=Bb~?P0ooljZ2MQ65AI&Qn|Ty- ziSBrE(SFCuTJSHO{-i1TDyT(nu;Xvd;kt*4M2+s^?4glF9aC~1eY|xkYX%#Qz2FOi z)?TyD(_(k6vCXLKd_>WwmdDQ{mr8pG^@U@NL zO67=(`*c=OFBMa;YlCu^{-{u@=6q_YIzN8)i(@H(c+!< zBu*~&;fuY#tRNZ(D4zK^F9?w&{ts0|m>4azlSron&7l%IouYYmwT(EQQUl(GmATrU zXIZI{0O4C@1nCve4N_hekbB0f%(GeATD>t**-0AEO&w8Kkx&}ilVLpj?xFeQGa9nF zR#>R>I6FX~dk6nBn9%TKTe0iV@|PIC1Gc&*5;QfiCFAe8xF{5ZwGgaJ{+g|gNo6nO zi~Q@jW&of{sV1XW8YGk}FWM0p+^?DV$h%<1dAO-ah~tWq2fiD_7gmI7WPv(6r|b(` z+#?Wsni(3RBw3jav0al|y!_>k=emNhqK*QhE-khbhrHe!i zc-|LI{N+leEF8`a0ZSl0l{M7^ucP!9`evdR@XUfIP1}PkC`ZSXk6z1t$qGpnhuNM< z@}j9A&)Tbz`T`WdDCX90sGBs^Ydy=GVu*A=LoeX*3^YPa++{WB^3BnOs(Nm7k98~y zA47hrl4D1wA>o1vQIM`sA+N#`&B%B8qu-Bf@{a^QS8PnmjXAV+n3>GZm6aGj=Ydp< zymWn(5|yijW_<~GuN(vYpU-k;s}FF80*a zwwQdm2N2k_lT!JtFK;ZrwukGHsQD6>MeH&+5(aA){lR;G zTTd@}jwK~edNSD3L?{ja*Rf-RQ9(hMLHxq z8$cSel91B#`iRzCjS(KFHIql!X(*Nuh&Mq10ij1#e5Qe1S< zh~?=sH;0ZFkAtINaSv`7qOQ*TX8xjf=xDOts$))FT>QWIiCuJRe!i)08|bQ7jBaIK z8;EvF4I%f@z1Z$*Od(h*#h)NX(hYr*Js5w77&m@Ab8y7OIf`rv5?PNU>lXmPlB zpEq4L`W&V}HQYR8Zg0GJD=bd%QFPr1BB+;6#)llZTFFk13!>?bm?Ac5U1)?knh*wY zdUD4Y3((tr15qAUZ*M=AMC%j*1?&sTM(9Rb; zpE59R&Bodqso*Wc^voV8TFt9~CBFdl4#oFH4nNKp6@wv_xJKJmY>Bk(vE{gf4^R3$ zv)BzW(Qh{MDy>glx;i%ZHP)p*k~gs;9xE=oL`rcvn#{Zh$naAbRSBbS>1b1;>XLaa zz=!2MaVnE(gE-Z|#qS!rFIr=|w<8M27sfzMee}fHNB@_OYf?gvx#ixjd_TOyiFe)I zF6C+zhR@v0*)MlvPCMxS_8BI<3<9M*$H2)p+tYXLmzw^-2a{1bM#R-6N#KU5S3;ZJ zu@fh!4{qucZu}nK?B7tO1~NXrshRE)9oV~67ZMkNTcn!%b%R_AcKf{$;~zKC1D!1e z9)+Ntf%)0aD|?kyb^)bUpU$u7@t4(6xUAqFztRXL#Ut(1(SdSL z))Y;p*4S#T0t$eLiqxig^%+ZtMzpy#GHQQ}qJzGGhR;;KVls65L#GOXTkqpE(@yz8>n{_ITr8r?R^ap7XUYNOW~eYRbM;9IgprW>wzd-ET8{wyORxz|JE{6KPh4YzB562fO< zs@-4;&xGTA30`Wvky_M5CvVo$N13r4FoXw@@?)h22mp+fHCFOt&%Z)P%S>j{Q^lo; z{^$GnsLN=N?nIP_pV80$8-^PjOM2{f`l?Xj;YVE1$a7bYdkm6s1J z7vw4R_(nL|&~sF}psR0wH!$v}PLj`ixj2AK7sb4UuR)01v5%icBDha&ShT*Wo7drH zbTrp;OCBADWryy0j{{{VZ<#+ILz2CPqwzdu6;yaNis!wIfD3x2I2Ymn3P9Xld0b;w zD$fx)d~?iFgw;ySlwU+qsUA|~$x$m2UFi)W_^f7zwj2a$;%zxx0cRfXATyO17E|?t z3S_E%dBtdsO-;f>YTv;$ts~!9GKL;rhK?k+-2GRmTbHW_O0=NYkwoPKMh1)*p-~zX z@T(t*!PFP^PJ2j9hw7=k4JAi5djA8lzpVTG2QWQ))X+)$o}-dVn#F}(kQdFut=AnZDgoYu`lh( z%)apNS7g7wOJG+CV&g?oeZmO>sY-! zb6oN*$Fe$yDj_O@(kzYejD2TMG-GYM^I#z1os@R*=WFU~r%bJ<2$D?iIL0 zKyIBSDXCcxb*0Quw@*-i%qT}PUNXVp@GRw<=g!NN{OkKzcnWORof?tHHWC4EdZB10 z#`B+m!gRC$EWmJFp&_GrVagY;ozPFefJz#Dm7t*CG)+(w`HC~aWO|!<-*6hI)K{gx zk2Z9vd*9qOx>I8YGH2`{CYHF0O%XbU7AyKXGil#T<-vAdm+2ceuhtMx7@Llu{-lY5 z7?g`&VhAJ44bOP*YSUBZ$0b`-@vJ#SDD`lgH5N_o0AFHt7YQ6l&c^QALbSx<%#nUW zh7f=xT%pZwft+Ftjis_`TimEEl8~*eI?=8Pkh?B|d<-rJT8ar}M2`%S!0V1i%BN(9B2PR!+^Z+H z@V_m6b*#OQ(S-MKlEEYks$NKLI||EzG#4x}H1(q%TkrkcUjX8)dip$1fN$V?hm>rY z*Y83Bm&0U)H0!DAls-a#-{4T#RA&s3j?NKwYnPUBIT@^K#CPkwFirsQtLFWS3=NStaBd7MN+hbIa zmQTG43-7zA>r!i9)Ce?GGA6wL?{PMB`WfNaGZKXv$wgls`3r`Vv6%Q1t_(f@9$8a%*8^SRMey^2F0bw%3Gn}-0}S$NuyvfL43^}?mAKJ<{aP@A;Q?uPrK+#%3D?Iq z6mG`O#Tdd$LDw1&3`__=KgAEgG92S%TBjy(2Q5Ns_$ZJ-mZ*0JFwk+QxROWjNsd(1 zaXP*QPvA#7V1o6EcSM}2G(npP7T9SFcWXV1kmYcD-t5@0ku({$+Kq2$ z0^xQpx(vG%@#LOo$_RHeiub!W<-vt9k!mi8oC&CTle{jRZ&k)~O=f7egTOLT}Lkq-uR#&e zA0!`O^*GX%Z8!k4L%^bO`FiXBbe;3|e*+3}UB|Y1Lj9^54R)s~<&g++XLn3K^Ux&h zjCw#*l&V~MMq`*l{ptAdPhSR}?kn2}ZdJ1FcTl&f{9fb>X1L?+9WOW-W>=K6J z&+()I-F3+^X|P0(xd2m@UqW;SN-4hV=DbtdP!dvbu9jhEWgJ^rv?M^L+x3IKAbwaH z$Cp;0gza@GXQ*+zIdHM!trlZJA94~>k7z|_F-khj#jjXO=Mgn;N;&N;o)1#$$mv3s zrSP+lr$>YK(!jB>g#k_IVoknZH@F+=7ROQ{x=%ySR7H_fxj>g>OzyEhIYLe3DO^7s z-OlMi5qwz6*l-Y>aK}9sIAAA!RWT>O(|_{UI%*|Sl(*Ok>@V09Oik_LQuknLHrmer zOLi~o#S%qms@Y_?oSv9{yQgy|*VTC+oJ%uQQ|^mSmivJ^(zi6h%GS59fc#_PVZI1w z25HIjSYCw$ATq6nAzJ6jGdYyi1Wb-kB&pq*t$2h2K!mWj4vcV5U1|xAo=08v1-59I z9mqjc4NXtunavQa?gj&!_RY5X_22us&N4^|a6XKG#Pj}c{|!}zbk0YZQFOR9&4n)f z4G-Y6AwW%CAE@AI%921A(LVCvOC|qrPK&B z$MTGcNHr!TqoL@bPrP4$4wOzzM93VWGdMsm3>stHV%X^3R_2hh?vlP6?;Wlsla#Xx z6aGeEEeA%7I3szV@eMAVDIVxmE%mTjrdb0}#Xtw8Tq`o(X`qAB!Y+Nan+l)tgd!`kc$zBVmcA$XY06#qEDq z5V&Bwdh)A^x<;?kjVgE6mplp|x;%3TK^lOyi(BBb+>{Ybx*U-M|5HYXpA)9aUcaI{ zngrfm{LkX9V9keYAvTs2~H_3uko9gct5(vwd5%U}XWX0c579_rPT6nkmp1v*uFUk7i3>+}o(SfNw5|2_agtEWGTD&W> zwgsDe=1%(LdieU*9m}XYSCaBQg9Uob)QTFO>VIv|lj25Hl-hYM^v>Cd$#d|=bonTcMzCGk>HkAdnCFO4wvLn~GsJsbJz4e~2{Ya6L zw_>}ZpYIJ&!#i58m!J-Abzxepu-p`b5UuOp7#?uek+NX*uNuGpY+GvkSq`jj&s;)Y4O9tWEv~ zT(?8$XpJ%QJH>}Z%`|2Wr=XAfI}b{Pc}vC^S*xBS=}#+w-PI zNru`J!|HuPqnqQZ(rJa&4qp-jI>qJo>NfMb=o1InWMRmjy(2Irq^ME?<2ygd5M*{n?zIR-NMW-H^euj2wt!kjy z)|(*}XZl%RfN9Ke_IaXW2)_G8Pbj@^MCu6wV@Sao4#=9xF^0H&BZ2r1ZxtHVDD_x)noFUFn&tte+C9 zWwL4nX)*lVK&PGHIa@ZOvRn(TGkxFz7TO$e&SssG0`f+)t2L0>R=;m;4e(QbBIV;@ z7O)pTNd(f!)Gr$yN;iIxko`g`3>*vSN`;h25{(A~Nyov6f@034uWnD3Vop|k(-rk+ zSxi;pJX&SG$89K(gy_))FuiU}+_!a@Y8$yc^5E{DV#DbSiAE2p`u!wUxk)`ZC71{( zKKIZ?C4@96kaWUTu+F9>_8-e)2oN6XirrNw%JkI1rvv29y60%NBc31-1XL-a&-nf9 zAhGc)s(P1RzgvShl7Y#{p!L9$xn@oqY=w8|=B8?eu&9+Qm2^nnI4;=k{XinBv1ee> z@9IGDGx1Yxh8x6qjMSDI!MYuikl+AdAFBIgz!b$`%cAxjZWsvly;dHFN z>r)JmBYW|A=Yr#J7D~3rjK91y`NsF4xW9*a?vGs4?#-&xA~VB_um14pvlW#lP5NGB z(2&_}4QwvE*SOD~HEDaV_P!}Y-EkBZV6dkKhwxBe2PUjXGWZlNOY4wQyTdq;! zG0Ni(L+OSWHz9p`6zhsFL~~56^u>hci_*9(Kx79(8xQS+So0E&lEQy6ns4l+DqX-{ z!oWOqr&!CquFR=RaeS(W1!S4KFyE7bexLlk0xE@%V9%K8c?&H^(EnLs(6AH@iGBxo z0ROYsH>hnrveolJ<_9@b`a`-DnwJUwBmE!>!HkD&-QL$UO3vbri1bA719y~*{|Mj2 zaezgpWRFR<8!N~KDiXc)XU%hHnl}g}M@?~nC>&g2w=Cov8XIfJ9QO&0GB$KC@Bau9 zuzCpn`ujyNXv6vT*V_Jh-EMo=v6j2>*WRJ|tavNq^$5W_XL58j0{}Wfk=_?eJ}~ZP zym{@CjyvGZw!=H(j;=!-S+3jx9JS7WoyltTOf~ z^>;Xr{rU;KdZ&$uLE4qD_!5gQt*`#_iuvONs_%iSkyhbZF%%VfehrXor0|*vWR~6U zie7pZu*jSxG~A-vG7``<<|JbOyE9x!biU3 z1f7M0t0vp|y=4O!o**qq$c~dmy(`sH#jt$f6J;mjv_2Yy&mFj#FMU+~Nqi>;3guF9 z`f|0X?2zV9l6L%oL>f<@sE~uzo4W?{m^Je`U#0e8k+xC^$4O_7N0z|my=C$r@!KPS zHt9Dj)u}|RZ%2Z_)Ri@EU;OSR^COs19lVfEo!#f<@g8w(K%gA|8trM;Hd>i5E150q zb-THvan~3)V(n?foA3Zmt!_{=xqG}$1yeD;m#8H-&JiAs(^B$h*|a&=IS2|Il)L~~ z)6ZzHr~@cDlV*s_(O7xIl!~JX8bG-nR}E8!F30ZCrJcR|n=!lrD^1lU*yRM#$MBc; z2IA&@WSoSIp%K8>=sl+w(H=%s1O?FPS2G9Oir_NiUtJ@TQHS;xPpZSZY$;zChAnue z^_N)GkFND?)Q1*7nMQnd{3V^Z43F3z{&kWB^Rxe@h*3}*O4MQ0cuo4M!G$FpwHNXh*sz@d#s((Z1os( z&$QpHUS)HjK0-;%9Nan8eoI$X$Lc3#UR*j;n`RKD6x^xOIvsZyEcJ!q1hjaZ$GTJY z@DRbe%e|VYMc7|Aa|skuD_lx6uDs&RTWYO)SIb{1T}P9e-WHzQvMvm3iT0Nn?(om; zUqoCoH;MNNfO^e%uUC01ug0fy1P|fP&a<}FW*z3?(`MKk{kX@^B=xKi3J33%rk@C( z%YQmjuy?B*Wf8InE{`jzRgjTri}5+rMVJ|&dWcbx_~nr#1_?iB?7#s5OyxyXtI2%8 z6kH;P>}6|KrDit-OX??qs{g#bQ>pz6Y3bzTlr^>S^w^Du`E$wC3#28E;r(&AHh8f; z*Rqc-ZjBV5qcc;os`k7V9o@U^JuIz6b0*yC5&x%XA_IAFS04WVKY@o7$(rSwt+6<| zl$g42$=!dQ`ac)%vi!Cx ziraha&6o`{#Ki??^_2plwD!5oN`W4X*6%aFsqwgW4(J+E@Pv)v`&fb0;noy(SxVAQ zgtU4TdlB&{4gz_sU@0sHtXe4y8V>{T{gsBcN%yv~PRlUH&O$JvLd(f*zY8LYkHBS~ z+?s_8?{fjYA50A{5hB|}`mk6bX|2?h(>LE;HZp)bGnQD((N!fP$ymdK)X-=^$q704I`sO|+GV}z zp0&00z@C7Z&kRmTPXmBW%wiNxsc6pKYA3fXOg{Iu%GNgUAIM}Nwa0tcg-z4wh(^dd zY*p`IZY3{`P{D2*8fVnE!ot6BR^5C0#{SaA@v)%Q2J`|) z*El&-B?m#?44Ytmm6b%uS0$ZF?LPHP5v0I)AT2{x0icazHA=~DT(c!rJ{dz^?ZFzu z*W0aZ5VEhzC8h=>=f&FD>bF*Ta(2_>wS|V`%5H31>bJ@0f;poGp%Q$|mR7q=M6a~V zpgWQC?jpWQOE<+)q=Gesk}28^AYEnHOD>Q2{XsemFh&X)8xVATk}IV4?zVccgx7rz z^l^y5L|j2bLP|?yCYK?Fp~^uo`Hz)9z|6i=ioxs=eF}Z=>M{duiq>kCV=U9wxS~gT};hPcJ|EndUnk8B5K!5xv`dg@~f_xxo~gs8^tjxbfHC zd>xP7KSF=xQ4cSp!w5=?YgX|a52aC*a_3~Xtc};j4Si;0VK4+clJAgv9PKSby7tWU z))g>%8kTc>fp=Z&Y%bSGJ9fRx=kou|@WnBXEShjg_|s@;3p=KW%C~{3@nW=ipAD*KI0*W+%NWaqh!<7C@zcXoxn7ANY3Y4uK8F=D_7f@DpxE~{uxMbzho`dLGx zDVzE1-Wrl7`pEEw^E?ab)?Bn_`3m!Ep(T{Myss5RGGu_Bwi1r#;NWC`=40!FNXqcDfD_*!2Y`r?9lVer-Kg-g`!?dQNfvYWe643b-9&y35sLlA_}pyJf-mt02PI(NzHBzpvQ|ifUPOyM547tYhtT;3(>)+Z!FQ$yV`v>ZsK@_& ztxcy!wY2QWeH20dScNcmH}%xchBi2l#WMnmwbYdkvTGoi8CizM98{CiY3xbDezQ*$ zMNWlfNTVWMJqhf6f&^x7?=*VPT~Bab%Tx%f>ApTGZa#du%3+LJs4e6G*(Y@mnYr`l zDPlcd2)NF)jqOO$q;48_By3KI@h74xl5l*VVAWK)1hQn_>4E#SmYmlC9W&qp*Eq+B z61I=U{PZysZJ1Sp+L;|o=DL>t;h?py@b&%e{w8fdx1UQi3p;hYEqU)aU2d;WmpE}V z-=Z|?P}+UURSO1IQ*xdX5KzGVV;c$nX_a5r|1Qqe{{zq-4HAD=iOTk<41ootL~Or9+xPG#AH7|TZ#_O({EXj5|Az!~^euW+}96SL8p4O%b7A~7Yy{Fsn zMqDtegT43!~L&QOaVy6MqHRmV%mvRG$P&--zr zOZnX#VqywCk~mM-1XN7w{#tB1sReAM^{j{(ozK}^lX)mZO@&=FLzvmrf$8#b#XZyJS5 z2Aa_>uWJ5{l};abjy?KlOH(sM?&trRv&QTeis(Pbc0&jZok9@{S2+J#3ZKInd*j7A z;!83Jg_YYlEX3E+%B})&ednKsijsQ_Wf9$-qDx zsp>)J5S8SGv(hDrk9(P>@DB!=xS{T-zsSj9y`sA0_KvQuuJh`&fx|JJCgK&Hy!g9j z?1sObiAQVs$8C41vyoic?8{5^?HThDpOa5FVq0UqmehOKPKxs%X!Vol>wmvG3Ik#* z4{fcrNC=*#zBnhmLEWCrE#gA#*Q#$|*iu0h*{F9!p{uQ7c=MRp7=Jq2ye`;NHiVHZ z#)_)Cx_c zS+S3ikazC{mXKfcr<~*1ki7Q#B3f6qtg7@a#rqQ{_|J23YE1DV2I!(mAd0JiCO_2C ztPDByw*WBc9H&z@EugNYa%p8uc=nXQ`O#P&(4KxOTx((t`oma-$m=lwW z;Mi3g%9KQC?9*>2HY0A!(SyaD&_RmN37*&s8aAYfE+_yggpn)ed>uT>I>I*vJ+sgM z!q@s`sY1v(RWsBtd*7&~nQkYZypW)svH`-vz^cj9{kPOMQ$Hc{Pz2I3_PluU44f%5 z>mGF_sl9moA$Ky8$n7^^$b;S6A7}3VE>M)=b*uGLw0D7MgNykh^20Vk<&|&7RZHTY z9$Bj5T%}G=PYclc>P!UsMcdtRn@ux{hAI)umQ+iind;T>m9ux$dh^1=cV~uet6`Xd zKe}7&;7wNX3cXW3)2nEP!7(QQx(YLa3sS8NoZ%2_-?}2eR0z1|C?VT z{```jGDLo7Xi@p&k@)Sin*#wCxf(e{mG z9|==C6j4;$d0*r<1B@`)Mypx8QMDCWcIT8_lTs}VMS0p~ltQb%vc5LRiq$EPEE*9S zC}5q`dGmtEL8H4w#T;Ac;GjX;Jbk85f`@*>;nom`N_Uju7e0hSCzO#VcN$Fq!sY*(ilj$|21Q0F{eok2<*|weZA20i_F3v@}_m* zQY7pAp=Ppi+0@LE0CjRLQ4m=DDvMq4+L!<#iEb;g62aLiLBl2qG3BQI(|l?=NE8zi#| z5HBy4k|=`Pt=`~xTS9P=rUMVDo3WF41%#$JJnx8xCDCK0F~sN{Bsc2AjE6=APy_zG zJPAmwCxqWnurM*l*2Xh6=#xM74NESns8j1G&A(LN_2X^eN8i3HuqXE&GjY}KKq6U!=! zDqgw#o1LmLqPS$&l45H-XX$VMhpT6gye1PhmTN31@DWbOrJC?ete7+F!lJ*MTlE44 zTT)U&j$pifUohH&!6eTbZqwopH^+}!rM~c^N-;_s$TzJ~i)ozgUrv=#xIB!zACEoR z@SPqm8X}Xs5)9Z`3+RjHY?YqfCj}^UA4IBv$7N|@|3c&1vj5tvTIDE$hoi$dA4W9{ zjG}K8i4j^E?*OLSq9p+Zar^4k?l@k8;wM8i+P2EN#Czx7&1v-YZuRMopYz!Oh~c;j z`OVz1G7=BRVeFT_JL{M+dxfKwF0baIIIKwH^lX9Z0FyuUn6&Wgf2Ept9$52VUj5d zN@*0m;(cI@+r^vp+Xf?H;^6MmLe8mJXOEfGElW*hThoL)PBf_DPu?^d4tLV+gb`H+ zRm%2XCg+^Ql4;JNPkep|GSY97ob~my0hq8d`bBYLo}iFSCxd8qKyUAmKCc_cf>n}S zO@O5e)3JIm_6uY58z#(8~pC|bZu(qA-?$4 z$=5eK8HtkinZxufMiQUhZafb4PGuwF2MLwIq%~|&T9h6AW}itS6Xt9l0z$iuVPXeo zZZG>nE9JnkSL26<2GEdnN5bObP!ol-vQ+%q6)_r;y|0NVhQo5VT1LrN6ePRuIJ&o? z=qI$iM=R7kpArJ>-#yk z3)d+L)NF*8pG7mDb2Xl|3(OB>DM`35lUuJ^52QmO!}}MD%F~CV#hK;xUnbjFi&6u6 zuNKH^n}W_C&)uGkFEH+Uix{SK%iAzoyWuqq<6Q5nB(MU;%s)UvLyS%>59@qwzP?f^ z;oa%vqkZiMv7E%US3S~@d2 zi3Y7*$f!B%ujWo|FGfl4G~M7m82CQ#Y-)RW*s;nnHZJZiX;6$cyX@xS(Vv@WlIZ8W zoJs*TzGa?GnvH_3<#yVCXz3x7J!6sG3nH7}TWIw@Oqr88&xFa?fICehRGwX%()~Oo z$op5pxSsc4zKM0^`L_y91wuzA54hN`rX8y|BMXP|dN%_$A@HOt^Z| zdoe|_YDk;FpV~eyd%LDDb4+Nh6)MJ1R|c zak!zg8^fJ?;(~^=isj#7^D$gZ8hh^z;n`eOduF0&eYl_!rF-XZ^`Dh3JCj~QZM+); zgoyLOZU|*dmlhj=fJ!>$K5kljMKX9Y=dLr~=Ec@LT5lp96xh*+4` zkQgjJC{1@`;-Ju`Bbqaw1$k?FZwdfm9*f;jk5-F%y(6BiH8X3Q+WK3!br>^Z5+CQL z6Ga?xh2`iq@Gw-+0-se}6$&bur)^o45uKSX$Bx9iypsk(reRnv%NYI*uSR07W_{+SAQ|`}) zn|p2i0{yI`mLp_pbkIe`dCKy6fw0Y-Zn;C#%!bJ20jvPzB!9yL=#tY{fS4a;INDSqT(Bya&yi6i{?0GJfYWlDWEP12EmUE{xY( z6TojDlC&nXSz|GzVo6^_b~=8+_S>W;(GQ7@0M#wcPOn#thI3GpjNKa84^LBVQE-3` zh89fh3r5ydIo+?R(EAtqOREeE->MDkDT(ON^TsE^c?=2}^tGK@(gQ;Gd@g`^Lq*j5q}^n1ARdZv}_Z`VGEgY1s# z3`V1b>>@Xze>i{QaTd-S3;61n%FJr&WqBCVukG&^KJg& z6f@9%)Y@Xrl-`oZ59r9Oki6!hGg|4+5sj%9J$j-KC7Y0^jtXjLTidpbu(TYv6=o?L zu6Cf!rq@p;XB~VM(fn}3#gs#@->_-$<)rpg!|x*-jO1s0?qami;o&u8g!Xu+Gfo=| z#og@n-ohLEw3kZhzG<`CT9x(n5rIKGMTWu`b$igmOvz{VfXSiJ*EdL9Iq(Jj{VMj! zpmsa)+1c3w3MZ=ySrK&>qaw>>PuCi?*%@Sz!9c+nTYZ`Ocy`W45vdG z-zf;zu-0DnZTBL5o36KSg4kDI(qWFENk6oh$2q@tcu;%zEm(xa@rS2L zp3j!rsXdXevz!_Qk`!0!R5hTD*0M0OPtQ@38C;H`eAaqyEa%=tII1YqNC@~O(lch? z*wm~u6DD&p(ponI(6aj>DDg(PiCblM0f2C-p4I?&=GP+~xQ89z=(5kgB@S)Mua$z>EzAs$ zyFBjCED>=V7Tvl>)XjVEAl`2`&IK zt&i*49l*@DGO9z^GbW<<2!}hd~CUO@uZ>Dq9MU9-*h4!Yf~ar?(D5g6B*2b(K|h zQCUGt^;VV-?R*aEQbCUr_4V~~c1cd}9 z1EHwWn102JjzxETp?}smN&f#01bl^-#e;G$i&Jb^ym_-H&Lu1gT66w}j!kId<3ri; z0PBdwrt_A80k@waAc#d8LN1OEwq(ziy`&HC7G{wyK=MNs$<1g75nX@r5!p&7Gr}Es zPC<7;p%25P;K^rTKz*g_mT5-NH6X2*;-=G-3-t}|FRk2hEsf-&Fo}Fj(FmSc81B=I zskwGU#PXa6O}ew!*Upbi{v4uh!BQ@VC0)FHG((V;%lE7 zOy`Rt%*CgpOG9=zaMz3(hbIQigNV(Ojy(J~ygEFmP%C_^gy7hLHv4vjK>ILYs;Ym<_RgS&+{H%fRT_ueWO> z>lW0C?5f&sytp6y3}jKJlf65Ai(2y5G}BYJ6#A1k1tM>>RuU)m)#-ZnQ-udkxgdW0 z$Nt7}4!gVA4^#`;FkGrwK$#Y}BD>FyEvyEAs_RA(ebw}p*GhMCrk$XG_)%C6({*c8 zQxr9leb%t#X(u92>lLlqP}q=~N=YD1{vK3gHEPRv&PvZXjbtDbA9215YMqFr-VyCC{mDn50XrwWI?*g8fl2w)OAYgo_A@gn?OV@lH(z@{&NcUmbM`nwxj70|F7?mE8HC%)vP0#E;3Slq(zx9*@62XGVV3CyoVSM` zttr0jOo3vXndv@R$0z-8wZ`}a^zg{n{!TzJMm0r1lC7h!FyX)qCDSmo$)GXrxX+P*^~~Kr}B`$KiJpj|U)$ z8hKZIZQD5aaZCc2(&?#fd>`D`cQnU}XfR9&r>o!qPAbo`oi0{z$1bRJ9AL-=WlDIj zy*l(k2;9b!^&OCn+2<{#$?@Bnm2+P4Ll5nKi^l(6JIebY2Urf{X_~%v`#zQKyhI%d z!LA)vU8Ovcm|3&3@lC4Vkc%YTk0iew9^GG`_`|v8hAXC&;_tX3_4$2xZ!f-x-y;nc zIM>K(4IN#movOGf=V={+YVf$dcs^Qu`?zdT7=#RyFEug(AMp}#8Pf+|oBHp_(RUv- ztbk_QFYBnhlYmKBJ3|hBM^gV_jXS3t|Geh{P8i+3x|EfC+zno;!G%qq@#FTXhGQGr z3zXA8YoPcr{bTnWn1%(43lGj`hu}Pi5q*{a^*dokJDtHn;3V1jMl5jKOFAmR?Mwt2 z%&}!@#VgJqnQDoTTo)UU4qKcb7l9uPK+qQ{Lo|alA|jK0&mPKV*Y2KXRyw1?0n>f& zNy$fZM1oMMSw{0Z=D@iHEBpPONgi@8n^%U{;~RnI@2x2a+0385eC=OL;n2`Ye)YC7 z_Y|VZ#G^|0%vdvM=zchsV6eMl`%3)6Rs*@?@etS|keM&x@p)=$ zcN}{|AhrAxiKVOsfqCOe$&T4!aC^H&A&2Yrev!dh>y!-qLrRn z7hlViWKJ>rCw_3J*n3MQiJh1&trBDBQpM>tnV8#ce`8i#UAIwWeJ61a%YLW zds!62fg-qqE26E0FM|YkWZ|^{JnXKh+DU@H0%X_8L)8$L5%#6Szw?%tbv;&3%qo- z5|bb6A$>zOtTowjc<-9JlGO1c@G!%r%7TUp{;bO{Y_=?Bcy2~DuuoZ7TIpCn?WghM zDa>U(RycRs)(%!9F?-}uwKRVoC-}fAmlfx{R~5hG*}!`_D|21>7Lz|H5DO@1OKOP*li6%)|1~+8+pg2T}Q$2^hbLPg;p|w z+Q8HmZtlyKmVdB^PngvINFqP*?%c!0AK9n*e4Fa622lH;I~Fiirbfzk)o04G(QJdJ z(gM$6p9fB=TvX| zJQkHt?%Fm~_x^Y#P_lj7&*)hPYjk4GjzMeiN~vQvM}7e+Vp&d}U(7(3AYNp+C(DvhrZ*tDZcOR5fX(?WP1?JBY) z?ji)LaTSCuGhMbe>0Fy4nNbWE-sMbn}e|COgX811&LGW_|yro&BZazYRgd)ofV6-Lj+-@p^oH z*mfjU>g1HOZyXvKN7K9VG~N+6?-^|-FwtZ$kWN7^DnI>U^upb*K|X#z$^FSdJ%I|V zyh0pTM3{34pF()WAOG?wd)8y45jzwW$vPRg72=VWE}Wem(2O&4aQM=|2ur9~ zyVzh3-?OmanXzNWJ?m)jX@0z61pZA=+hp!?zge)Y6X=n^t%bPE?jJs=jh$ml?OAT` zTA1qxoht-|!5mu%jsmY*SRfUgvo6ZXXQg`Q|KK1-82^UY{E8d$;O8(QL|i{WXco{jH1AUp}N}MsA=A1L;1?_+56erHqj#ZlC}J3S zng&2iCKZt7P@MR%`giePe3#pGlt^*pJ!v46YxKcUQF|O7==d4l*5rOa zxLQr*n1tfOWU3Q3Rw^V=sI^^~fziMeEHTXi8XM<~`>5~E1j#p?Oe4?EkZT%&zV5MQ z-p26I7e&0jE`5Goge<2OUWxKEMwZS9OLJgh_i=M3-iT8J;i9b_?`3P!h7R-kx8cdh zR>iGG*4L~?5#2`u^F9XytpX$tyK)z1{HY6)mX8&>ADG@c zze%M$7dN~g$bMk+&2%t7NTS%8Hc?VoxTm+QCG*PV>;s(UlPrQk9%CCf%u1g74G~*7$jFVrQ9vVMg7(?_|ZngaH z8iMZDyXXEnUeBTbE&$Xvz)>Khi0zAawp#jA{C4zpY8dW&S{)8UoaGVdVdyY5)hWX9 zLg3zawX?PUK5*-kR7~8{jMFO9EPd#{lgZ;ME=|Jaf);K~TS(qKQP9H+h4K4(yGkB@ zbp03Bou=(eo#{_oS0>E#Cbdlrg3YWMz3q%FV~(`4g02fA8H)Wgln!T!CS_qFOgXJ2 z!Lg>sHuoxc*7;Qg4h23qUy?W5kx$ z6Xz*yu3CcPF5u3`uJ+uAk--YrRZ)jO9-@&l7>4JVYNV@;R{aaj%Lhk#KjqgP3T>Z> z`Hk8W&99jWH4X+tA64{p-h^0Zk>+ppJdqa{WA*{1@(m6OkB(fNUE$Kpi%0E85HZ3# zq=2D~H~Nt1i_|$dLEZ?r|88^00G5Rv%Fvg`5dAZ(@L}#umUu+0Tx(&h;8?kwsmZfg zqk^6?{`>S7tp=8lE`>~EWCR`d^t?uZUk>{3;k-bh8_ zY+O*=fCnRt#{DiOGc)`GBCrP%xHqySGk)rm6KQG;8;8=^FHD>Vf{ppl9xrC*5vxks zPyF_Gvwdsfm?|9o1VXS@NsgFLAH*S5}6|t*N%bWyE zyyJhoU>zDbWl_!OR5HFL?uga0Z%R5o!Ld@!?z_KljkvXF(=1fAvDT%^+dn>uFQWdv zA6x@{-?5_50L#Q^xH^DEHZ_TsmX=z}o&C>+m*3N+(^EaD$uYeAN;`Y>w(8buhNLKT z$`v4e#xEWE3CYk|;Nyh-0Qwa}ni{{vHcvoVoDKmlm|1LR7C3kBh6K z${A6$aH$GEM!QHwcoHZzYIje##vkRMcr9PAbN;w?u)6SOTFtoWI86L;>`g;=218z- z_LY8_1a^e;(Z%)t(U|L*F{AWjWs#TEa0pHE_ezBb*^=mI@rou5+#}j~a!lb<(p;S2 z&9P69I&zTEGGrT^scNr}Jvfl67eab|Heuy3b=onZNGIoRj;<(|gwJbH^WRpV)QImsZ1-6sP{W z7Vl=5{gmmnX~5(ft6G;Z8xa_Hf&tgVgm>G76E{nO z(Y~ODv&rBv{E*=c1uRX*>XkVzjh{mgW&)ZEhZCpZpw_UkA6%GeAu0Y=vi)A$I2(}OF*c#k4o)dEJ?iv`s<=+>U4Pho2PK8 zrQeNihGeJjU97fvE%v4Lx7jn}Z{iKJePYkrKvU-5+x)J6$IZNAPj(x3yh}+*AszEs zFOKEQJ~?s85OI;s8cOJzSW(dy6XL&yx|n0{JU#~6FSqKVYzN$3u7zOJb*`?yyhO}1 zs+}Xj@spo%{ustLgWAVX{?QLX{zr-pI~_4+{Oo=a+mAgYpH{tsuDnj{ki+(n<7BE_ zonpR5(dRdb|GeDZDWO{b@5lf941GS;SXy8LwXo8%K3s^4klKIq~bBb5<$SrP0UvuU-T(@6Z zC18K6kB+l{SKXA)%dc@QdP9qU|K=q}00tX|U8;~Qm9#ho|M}oZ148lpwX1USQfMp@ zlenzHvV4BtH`k&tGp`YO| z;~pxckgb_4XL zW%-)tg~6W0c61S&l^=zAzXncz9{=J#wxlj=#Spoy_&nd=2&uTbt!;Elvx{*9iC{Lz zODv#KYi7ZxFw>~qlR`HBJoEes!bl@zvY`~AE~M~oHLFfuie+pqJ=R9r!ivebl{@f< z-10Y=pg!7sHKk#$=Sf{9iLKjG z*k7v-{fzW4`r7LoEC=4^QMky=tK)Cd0q^wUPPGyVcRZ4vyL;wVl<2RnHtl}=EXKaW zp6onKAZ#}J1^Y*hVe2(KM#S|TM>SK7bL?`)LBGmlNGl{l`|D^b#p$rRgO*L~sgGxj zB~hxK`*51zT&oX>fp7x?Z^co&h}p8r^1@bExYlkJn#%7A%H09{eRM=|pOIbx0|yri z=hf2~_MVREAJL`GQb%zRP{&aTDKhc5qe%S!@$LxnuVw%sATWi1Xot!tSw3YM(8&o2 z>#_%7V{Zd;w%Q-z+>f%W`&aE(ZQNduTK`WiBa1i4i4CRf|E+M1qV|&S9pGsZ)yyBg zcC#N)#tB*bUet6jl`aSk~(HShwuMcqM`JI2_6CEm1LCp|b^?gl$=yd6u8m2&i!9sP%Dv z6A6K#h$Oq)sJki=N$Mf_F|@z!&>M=O)sh#wmEWP8v)7=0^UC7Nz9}yBnF5!aWf2E* zHUV}=a4LRfh7#*eSeFn6PkAKgcOQXBwr4n2KlvFGY#uFUWxYfryR!JOn%Q?)6WL7M zKeeO==aG{057JlxU)T5AtgGLq1G}UN6ms067`6qP0bh-=3bn;73UA_m9SxAqqo!pM zvK0zM%!e~&H1U8)3c1b9%(70QGTcj+`p$Ur@XF!U21>@?{`?)w=Vly&Kq#kgzsen} z4py^bJ<7ObzQnL_9LS7NmFt;^DH=#q^~My$kIbNX05LLpSgV@&V`)>0YzG30Coz^) zQ9M1h$ymPxU(_OXEKMlMnXQ_CZ=!{f!CJvPuO~9R7*7^0wXK^5$achw)ZJ3^KyZ=G ztoSN^Ynpzy`V2Q~8ndg6U+_iQ&Q7W65+D=sDwIcG=o@8uVe2O?D`#JOnyh%ReH~~W zOg?sOL*Q~3h-T`f(}9ZKCYh3&B~zZFa8%524glnisrP9jqb&W7niNdTv0^CerQCOj z$hOFxhXrSGzPA%&jdU8dCog65zf7e~fz_4^J1&MP=4;tMcJ}w)~gW2 zc$1NqM%?Kf7>F7ZgRheVw(~=?O;$@ezHT$;(JYv)!vAsU(cTN27M;h zlNMwc_T^2d@b8{?b8Kqjpzk^&p99HGSzR|@k?o&{TOmIW)jo!L?s;-DF;0Y;87m_3)vNT=i~6e>}_oqxH9o6va}16D2S) z_lk-OWYB$*XT>Wf%1t(EIihGA$=Fks&)cip+WpgW65wYtp{%!T{`Q8}=^OnR0 z6l?H3Nn0k@8(N=pcT#n>Mpn((N^ZVUQO8@fOnpSH3Q$n=cF_t?+->|_mBPi_*@M!0 zO<}w8fvH`e`4i!A5p#}NY-`N0ybgc1>t&Jh{iQstAzNZLANdmc0G?M|6~L&2Ck$Lt(#*4nB@Q9@~unAZs$b6%VeMemJs z_+1SfZ*9^#^x&kent4}PT*Ya=`u&Kp*OJHeWkW}-+xAFQ3%@j&M`&fp4bCh1a6b7j zcF6bga;`<}`hDHdAi^Z4FsSqBs7TA!2dG?%qF)*FC5w`lq=!7Lx zlR{8GJQX7BL8Iy)PBJ$`rXv*bW#)5j}K>x^q3FtJUbQNO3{G#djYW#+_-Fwz?O9u2}& z{;bxcB7bG;dZ9eCGmErP4FyP2)eD-9@~e37!htL@#7@}S#Vh;IytCzImiBa3T*D?g z4b(05PQ4M+a?XEgJL*%Q{q?{4sTgEC*tlRr7{ciN@>CG>TitvPaTHMH7}4U<40N!n_tDGMLVuB@7@E*p zfykHVJ0>~HxxC^7_7s7IK0NmJf39%DMn+mYPxWN>W`=;<8R4dACF%8T2yf_Uv~;5n zrM2oqUJ-$K_;T!~h~o4qK5A7{E8Q?)Yx}ZV_NN*I*ML(-Dvp=z%5L&cTJEj&Wc0C( z5u;m-h@@!jo6HU#-tb;6YDvR3Yd@*WgZc3C5mo5u@LCZI2-(R#&uq6pU^`!2%;vol z6!broFw7>u+67Dv+e)JUl0)s_76)?6ZLr%)XO9ArtX;oL#M_{tj-HpgXAzyF2*A?x zG^Zc*zFQ(Ik(vc91{v1jt29irveVP@O|7pKWo$qshX;xXC1|y+0{+UwhTwI5H=%TA{rJx z@T_5cCX6NZ*g8V|Znc-l@HfTzlHnxB?Q%VQOPnASo~!v`@*5nZI#zSr$T1#uMugUM zP?KsG=Z9=_Dy+_5epfKQohQML=bkE`#}eyn0bzNOo##wJ26^xl5u*ivxJk`*51T*? zX@>dBYBi4EKe%5iC^Rj`f)t-Mv(vJ+01hYS=G*3LO&2UuXfYGSuzLc0JT37eAtfS0 zo;WQ+$|fuIbvl!BO(wItgM~nST>J0#i{q!)BqKA*sMeZ`zVnLWAd-eP5zNt8_YoRx zFyMJ=2nt+&P9D*~D+g@!cEMOr-coajINrHT?L`S`v}LKPE275AIY{WJ*YGlqXrb({ z>v>4&GQ)bmixoK>gYI%fNMTNBHnsP>_=2o}IgJ9LCmZBN*G8I0m<-=q;Nd8KS-kS$ z<$;bfF#gtSJ~cVBfvKS&gP*_fOu!DezZHRInINpE-m{B3;Q+e>AcR(M`LB_Qq%158ulAZSKZ4LkFD znQbebEjQ4j6Qx`@joW+$J&zW`7bae1dEcwkNc&(AGKCmBQ)FiJtftk?Rn;W#mEwnn zmQ?acvmbMf#u{6jI43h0d1{PY5=QuA-m9*8KaL#p(ECfbP}7Sz2j5mr`n+9)Lm({X z#%um(b>rjD0ol}KFGXPKMjt6ZLmynk{kb90cm20OFI%ka`K6ommHPq_iH`FLC7l{e zg5}HlhL0VeBrg&qAeHC;!WR*+hG$4Lh=|1!t6=^nH4acevUO!kIGm~_yE^Yhx4yVY z^LShFMAhPT)aSQyXB5RwKX0Hdrd;wLPe8Hue?I_;iYSs;hZL{OU;YT`wfMp7@x~HI z?2k0SLn}FnhgT?Md_0DNPAc-|#s?LHa!Y{HcVv522SH}nRVZXPT`Uui*cs%iEYnf~ z0bWBh^!XJG@lW{9*;fDBFrn1`tfi;Ky`UU$vxN7vU%>{jiQggj$YG2;GPP7`2hWUx zbo=IRx_&sb9foTuMvivh)~U<<_?OkqsiN8b`yx#qacc3aI_2SlcMe#DHJTM7;*)bg ze#{)rYl(C8FqdD?M$cYr;k&uvNYMWbN|KV?aPe-`?wTK zyk61w>SUyMX|jOe*T-y&?k3#zP6m-(ij*NMUsoJoZkGnT@Wn+zTUWje^F z@eCKYrhQH&w}HKztF0Z|IHB)1&N~Ode_cG$}4E z)0s7QdSoXd;k!OiEZ(#x8l>(1XR($l%V{cmSUtK=G^EW-GWd)Ue5w%&E=LHA`ZKV& zSxq$8eD1mnx)1Q)%_p~MiyNd+%>NbK1$cheNcnlo$BJ9^gFn>VfhPy*hc|m1K|oz; zJN>3;-<*~*B`qzfEo3QWo*~fG^f_|n1W&*xyhJ1_?uR|9CLF}}<3^8=@}328*9{z_ ztLE9Yp9*x(Q*MwHSUyjzymXSd3euLu+qBPh>)v;udwwKL+U@v=-x_C1G`A|r2n2m7} zeS1-azi%OIEoCqyPs6)m!(iey7enrHy!tjn+)LF!iPGM@|7T{_SyO9g76Q>)Ss8p% z_3_4BChf?A^=B7&q?`+|;p&WBXryXF@2qgfgoa()ib1^ctduoVq3~i2imLx?2)l8B*%B^Moor4YGnHzr!1wCYU zlEnqF^E`n)Q>P`6q~37_q-d4=qlZ_n;wSEqr`n-@XnFaoEy&1f`ZK8z^cK%m1;<7# z3+$c#WLCnEjk#}N40z&AD$Zo8<*JZIx`-;2jEWu(!$_Y1Ar~@g~l&R5#$TMZpBqP}x0pCcf zWjX;T&4hVRuBz4tA_LUmZooA@B1`mnA(98q%r{5N{>Em$H6!-Fi7;~OQ4Ov86WBV9QXN3a(9{1L}< z&#T{|Gw_}M_us!8g{9|F6_Jiv&y3mM%<+xIeF-fYFBN`{`P|bU7V`Kg<>M1#?A{-p z-oam)e(=In>`Iv?6u?|DZ3{2Y2pI*4N0JUjMd2bJR>Mlz6%;BkLXiDQi$OBIu8A0D zB=`go9)idZyFkYB>_9tD>t>DE*Vi8+?<_l0Az(lC`t1{+M}v7Ox#jKAY{=NykJ_&C zvB}9vZZ{XBKwLPU40T0igC%$JaeG=XTWDQ&(4&vb*T>kEYl4I!i+M&-LO{Zq(O;t4 zf3V2^i`!i6X`uYq;>#&sSWVFlWc?Aup8(B9B>CN6^R^ztl_;eAw0M-2P}E2yCEMJy zW8`b1Vvv)NzdsEL$@*U{>K#@ulci>lxoV3Tl)xr`BNXY~n66J)xbsZpxD?HZt-<%~ZtVUghan~n_iXW!y-`7X_-Ub;!-cy93 z?N+Z>ScvW%?gUQ`IV0X+v}3QFso4qh@(YI6fVZ9D=ajA98?2XyOA3&{5NI-$ihw?W zi_&2~`M6*sDs4WVYNm8-@4VBx!ip!g*wU+etrwzI8bTU#LQx@6JuY)lOY%b8HPOnF zvd)%>`zTE#1IyKL>uLRztP(TZ_K>I>oEQ(=u6m+WuDyfcthnA-UFGUs-SjjMHY36o zF>1gf3*qwDk1;21uZe0o4XJS+D5MPW{Uek@g8OLZnGIke2d>n-X{31twk&E^%(FIxEEwNYotqoOS>o&5|nBBGR&a^ZGO` z`nUJ{@lt26q-Npd5O2?)BPR379}RxTf5QL3nAz1aK4g$jnGyGAizE6i9o^;*G+Zgg z!Q0s{l4t`D%|=|H^Ba71LWjksER77@$v{Syyh=t}YBU>{?i5HXCf+jUh=J^&;MAX! ztVuEM<(e}`-SFR?VE9~CGg*au&6#q_V3K(&2d8A^?_LzN6I0%uNG*q@Njc-vV% zTAe+v3c8@qXKJjZo6E>K4rx-11q3Gd)VW{mO*ifqq*qxE>m@`p^{9W>YKaw+`-KP_ zM2c)2V@O{sOOx=53`%tuG~!abU}^FBD_YU=qed`tH)Z0gGkElBin@H0ho_|*sP-=g zKjbiCv2nij+l*iOrh>1!(QU!X>*gbmCRao84)lA9WL`1*xiu1j7FK-t#V_%Kk~_0W z(h4*nF&bu>s0r{%Pd($;z*$Io3gyAWP_CmDPL(6o0PO47VpUSS^opr;d*S{iIP1%P zRlv&hahniGqns}2o>$^>4m{TwC>e+dI0APz2Xb(7{9d{=P0Q?0be(HzpdXcXr4PJo zcp`d&bv#haxVtkrPXA(q?B0v~;uE3E%P#2qs#eeUcM7hzr8pX96FtY?H2$tc4?JZ? zP$-|4k`ZJ20_h$W+gp!1>Z~Cp`Nvt8hpkTwO2iLtL3~xd(zK_wol=$?jAXho=@(X9 zECh>)vPEKMFF^=2NBGU{#HNxqyYfS(cm9qF@e0lN6c5q}mF{&%JO+xC$Ifl5E5MIA zeCmj2T_WyPV83qbZfIJJ(&gc9D!^y+A*mz$6vJFgLZNL5v#j5HFqilaEK($t0;oQx zl0VCy)qtO8OudWVH#VY21b^Sa#Izacr-3p}uoYzWOynJj>d~u~e_N~is0b>SYe|${PpeeBmE>+Rx4 z5I1g#NUgaaVn@4&z3I!>%!q|L{4z_;#YBGXbyh|f9J z)QGlAu%*i3y@+u6%AhbLV^)jUwVRNR)bqspO@;d_4J+j6ql>03yQGZ)C^fZ7wh&SA zb8L9(Qy9@(q*Q}P^K`x+$J%kek;d-N$W&B}Q%3iKKXctD5LX=A2aeyEjkWgpJ}hw( zu}rJX_6nzSmy#(V3_hlf=MmK9V9`4zAUW8#V(y3|ofP`dv{-?yWPXz@>k7n&KH*RVeS0T#eh;*U<*i zqFd>)Q?zN#gX)how+EB7^)l7xl0=uu`XQU@uW92K@Yc3C8%fDE4eANPm_zHa(8yD) zle!j48+`X zoRw9d_fai%AAu+>l5YOQ)imlv%siYehK1a8A4p?+4#fCbg`q=_<7gv%ILg9d@)+4$ zB>XszVckJ6%h$(Z`FI{;AW)oHR^M}SMG=1g=XsO-J9~(?R{5F>mR84y%XIg9758rz z7^a{1*6VOZ-CbYLVH~z^b0LS1m}xw0Y;S#((;0;uPfNd*2&vwV}Nkk=aXB9bUtmwU>H~ds=_$ z-0nEWY(1Wcne#KDH}ZZI<=`9vP8YxZ9{XLX%q0`^?JaRw<9E&Sb+yZ6(5p({{xIN# z7_VESfE2e!BFfH>#opcDm8EKcG476bl-4w3{!8gZ?NS?5lPK~5AtF&c?O3c4D`S_M&dF= zVL06=WG%aW|H<@y4DpSnrmUi_mq1-kw%wliZm#2cuV0%?p{=L3MMy zx_z0VhK0v9sj?Gn-XkO3?Abz^-^4>{85w`wGY~Qr-~@zJP~s&W4~DUB1%3m82o2nZ zr{@}780q{Fyb^bdF)7)`-74C~ky#^-9OIJ6@E&oY@s=x;g`}vC>(fl{*?q^($Jmm} zf)*6mAcGK^4S#NY5q@#{oYOcFT8d-VZJ~sMKoIN&tDd~d$+HEITI#dU(s?O*(Y!FM zze;m_Mp*GhO}^Bz^(;IMspqd0V&H?$Jlg1?BckjB$#Mz{EnSZt} zu_*-`(BFsCM#M=)KKpC^wX+1ci#O96#%n83o_`}m2F3+=aGE)I>B?Bgthq|7o|z1W zmZvHebACYCoil&AKzux(C*mxbz-lQJtJOzF>8y%Vb84d5F!*nKl9);%YJ( z;DUES{j&py|EGaD`QDT z!0}~&XLlz&QFwfODg5z^9IKZ$j}R}RG>%D&kdVV72AE$h4|xi=d|PKxeQ69;T1u^F zTWzMlwiZf9J1@s@zu`yEp*$Uaxd@nBsGvCW zpnpA4;f48JRb#36=cl_0y-=7@2Z{EPVWe3gj#0pKtC6_%cI>R)7;cb888f#_ZD`L; zI;A7~(pMwe_kBYdr4<4fTamj(nB%Hx#)Kil(7$}n}}u#X6(o6b+=2M3^B|z znu;)NejOfCk^4mTzN4+i!C49;<+<&lF@_+EVBJ;q$$QXa^%0yav%3}mvQ)EDP2AD|%iPl5Hr`+4aob|AD~tud}%IB}Avtjt^>_|j1;x@3tq za(npA=_XWZdS{f8>mLNKkcyNn@pV4Pify!fA}poaQ)vH1F1(Y{wZtj|1r2ibrnwH5 z!wPy;r#RmokOg2>eixGU3@Ya~^>w7;7ks7?aS)5)45HZigES>XlZd908ygm2ssFi= z5TxsZCVyr%!mIu#@XQk~JM42_qykpeq@kX8tk!o;X=FXS24FRmC1^JQD74V>!~vhg zt7`cT?Iyh?r^s!s2?TlPU~#VHuhsl%m=8on6;7d5NqS~^j)M>*FRB2LyAUT?W1s-DjjUJVSji7$7A3WFR z5(cEKvj;g$CKbbkL!=auV)lJP3sTOW<}~=jDEMU*+eo4T*`WP<6}zRb5yO_A-J2>1 zJ^8A)tLv|=ZTJ?oQ+HXzQnZKNtmy{)TkA&WMM~bu$>M%&bLYTA0BN?X?uy&D$95R*KGtgOv_? zZ9|V2?T;4~>)U~AjB~X%{OuR9PKuJCS;)@_6&as+!*1HnJl@oVXPzZbsd25C(r1%S z;D|1?<{ht=Oa%~~w-N%W>K6&-4m!ZSU!#`}4$oIJaijaV3aO^X4JIgc&UiX(cBOA} zH3~Xq3HpkZ&8-F;`DF)hQ7VtfxWhC4i@w3P+mZ60JP+v|;z(1ujO+kzvbi6qvXDA; z*3Bv&={N{|5E_z$*ad}t5B4n4u6QnTvooG25&%yKP7(J+bT-%7_1=hn$oxnU;a60a zvEN<5cpMl-kVB zobq+W49Zsr2!wL**&*G{@X1=0D}s+GAw)M2v=mX}Qm^M=bJa)f^sY#!M_cL%b1`5= z(tA7Zfnw1$%EsKc?|sV`aMr`D$?ZKWC{sE{3rM!DN)PV4C~-az#IAIF5Yst_Jd=2b zWAZZW`E`OHHrC{y&(uPjqL-}PZ%HH!J!cGcN`by?GUBR>sq=sq+P+Fw1yHv&uYKHvxgFLmIpk`)?;^7;r*o%9Kq+-;W3c7beRuxbt>pRvBy~JrB}a~%O!j@nk-cJE+Vzc8zsmzlQf8r&R% z&4-r|B6`yVOI$Yk)g?~*8UJdPXlPFT6lvk+O&#_#OkR`RrKV?Qa9x#{9>0+u!51X8ZMJ*E`%5-PhNz+RvxStw#=ex+LpPLCMAw z*V)O*?Q_Q`NZ-R*;~>dRo8i;IpCMZ`njYT&*;9!Bn;!BI9AheqpJJ*lkIneUb z^Gw>D1KGDIHdvVu=02o^B65CSH-vM3I>8je&aef2D}?Ur17Lp(NxW(&c23D0qDj!s z8fv$4-eT`%i5VJnORi|UZ%XXo_r4jtt7x?>MPVYDwL)IoYh`T%H@$CGCEV5TTUuJG zkifjVr)RDX$2ag{qK|4YLMPY@;)2Pu(&EG|Ca&0ccj&a1zH&|F7`Vi#Q{!3I*;udt z+n?;XxuNaFRX|?Uc%ML;5kVdhDB^cYT5rGbt)#qzuw;htRd(ScYmod184;zCOoT^fX5w+YyNyo#h^{?YIR`>e|7c2KuIdFyHh?1#N6RChS0%+&jT8&;~$88-eB; z!Fc>8w99BxZq*O|*P$k_CI(|;yOF7V^xND=aRJl!xW|sCDLbu9o{P+9%Tu4;t$%+` zv8c+CBMM=yRu=esq{T1m=878=OQv4 zF5(I(*{WDmLIm^-;F7njMD?5$qLSo2a4y4od*8n8w^8i1L~vqOIV4sV14@~A;XL8#=s_8&%l34tb04&yWyTJ)w&w;d>g)-LT&^h z#BW9{f14rV zdd%di(*%g}a`sw>ggJZ-7<1)qlxj=)+|p^ znRYd#WiJ=2>5xKGoU=V9^hk+Ls97@XJ$2rUo2VOvG|c#sYej%aSHw(V=Ntn>AB%qK zPEA~8(P5=vfo5X7l|nNBYn+zJxIH1Awf3sE9;1F}2-}>0x0Wx&#{FhHBC67Y`Ke_% znlFr=1T8IoT3sTGlQFP|iMUEvqdP(+GK9_Cs&I|NwIvD9NUKinV6=$Mh}MwI<*N-m zST12@k5nSdMI~ca^m_l*&|X|7`wQ`vv6Nw*u+e_EY*PJg3f&k7me|Zgua|LVwoX zP=Np0^z8xJf!wWhZoth<+;3kUB9fv5ZPRSi&$@Z4zNRis#An=@3a`&Owi_Dk{Y^ZO z(%ntNM@BWe_6L8C`>jT2tOsYLJ*#^Wn_y98Iz@T^yV>DQRrOmI$|6orkDZ8ak=sEH zHtHcROz z$@BvN#qI<(DYj)3u4DycW9gC!CXk7R$gjdestWIgJ+xmNU>^h-nO^U^)Xgu1f8O6& zYkIq!xu7V=LLmKT`=``aaW(PL3@)yIOqiQ?EjqG8>~FS?f)h_luG;=Q%v>ZMlu;uT zF}HP4@{ONC+fMHli##_j664kq4-qp*i3q;=zJBT|e&MfeVJq!pv5)HJ9Vm3scR%c~S0PZ7etc!*_2}7hSTNP?dS+d9_mj+YU}9?J zIwCK{anQ^@AJ-@^#%lpSzjFq}wTXtasCp;v+)d6x1-F;Q!sW>EsI>OYz^D%BAE`WF z|3kF77m75(xht-A1$KAKtu_c%TEc~J61olZE^~5r{s22eLMjtQgr{azJPrsm>FBXg z1EXPX^CtD?^ykFgUrzjm$c0qftJwsnXyst1hdnuVXe#unf9mp4O$APdg zwMjYz8HJ*Ia}^$jxy-6Yv!`#iD?X}cr^wsWF^Jpqe;C+{+LY-=j``mE0TeCqnZ1DS zk8{<^h|J8m$d60%G_W)N(5NiQHA(5WmbZkx1r3sz$iDS79!bx#R!lIU_GB0|%oD*o znh|3aj$sPo+AhW4$Uh<%~6YoW&p7Vx@0>Uy_r` zHGM8SYqX!M!~KHrT)BYZZ`}Wb9<)J2^P)sUe?A)l6%ESIe~0`dmDO+K6C}BeofcJG zv;FDe2=4!M^hEQQ{v5Eg^Lb|PWq+wX^LjHKGhqwQEx!6L$BN2PK3%Dw%;3o7nSXGtoV#v*7v zHXr1lg6dj#`MIT_T5uSYo|4kRB7O706M17&-_dYU>+Ghb3-LeqGp|3r{_oCaDus{! zGBJB`P*S9Mc!{FK{Mf}5^c>SzEc`DVgubb~+@Xm@zDygYr6$QVvVXYX-BVSo&f0ZF zJY_jhS404=6@;Zu{IaW=<%~90J*j8aoIKlR)}+Xy^YiTLJh^QPFa-Z0&W3}eNJ#BT)1LSJ zd}P)8+6@IfqA2%@>+4_M0$NpV=YT8MNtX9rLD0_Rdjlv%93;3d24<1kY30`yr_0PE zP*27C&OSeLiF%Lz8(CE!f3uuR#@P{}5-n+IsiRvY;7e3v_r1Gj@Z;5l-bCx1~@G1^-5i!(VlzegDx9?I&?t*zhhMjD1w zNNqL^?(*upvrkEP_eI=QSMM9xX+o|ou#<;D*9~oejtl~=o1t7f&ljnH_l76o{cf3M zYz_&g%VBhWx<-=?8%g?K=AEhd$IqFFRz>AyKf(v1Pn(9QKnI+{j0zVEdeH7IX&1n~ z4l5i@aqP~vvQEX?_g(3te%waT{Sf(d;`n$WuAXhs^0;@o4>y89>GsgjE$4YBEi0@YpqAW}B|F zXD@HNwQ1VS9zrTP;*>_1IH541PS~h)NPz8VC>q0g!h4vKXrgXt52N56iu9{!ke2zR zs(bHz_R~cAv&0);!45~JqI;W@CL$R>CSB2;MGuPb`&NT2=BCpZ2#8<=bX@&GI6^7d~7vh=UY2bRQcloiiwG_s46 z<|QWLq70-Id?%jskoOPr$ENQhwo*pV?$aB`=+S@R7NDmenX=Zu_}%V7CDgN*no*E9 zZDMSRoDjrAT4>$5K%7ZwhfiHYF&S$zniXf(!!>MBCdwaJ7(~cdpVw`kvR7_4kmk01 zSQJoJgeonwEofms4T%YN5PFHZA^V(mXteJ1J7)T9ejN^paleja} z*KhGkrFZLhUK+b}G79c=I17?IicHxCvUbe^Cu9!kUdb!z z;M61ngb--EMnbL?Bcl4JEKYtWO&GxzcM3LH30UBhodFL1O(trnWy4j?pf-aZ2C ziu3^!&$_8{pUeJalYhO>O6V~DJL$J#BzE?u@;hP338$+BX7SgngN=MwLdcG<*VM8n z-b3XEHi(#+nL*nl42@SYjy{YSn3$$>5E*xrPni2%yLq~=`;LltK2VE$QIl~Qb&QXb z+>eh6>iq7k#A^^G)1edTz+=Ftli&!jw((2(R4meY%VL!o`#+FpXaC zwLjYaG;TM7X4%$KiiwF;)_jDGsYwTwmd|pJyb5E$lk}P-B|G|U*+F9M&Cp*SlkU(I ze<~IN?A$PfTXqz-DaD!6D;N0c_Sza><| z7*eYzXX5j21bCWRAB9R^lP8 z2%h+uSG0>KNOseprywY9GFbt1$|xsuD61^^#{A5)MG8?CZZO=_xW?kNy$tGC!?#ix z9-DO;nb0700v|NFZSNlT-mjuSsHH>u`mlFP-U>Pq-`?G!msnU?f{BlQx+*U4hNn-^ zwXeqoB2u+@k9cRI$mcPmrf82u@A0NsQ13FQl3GJzS5{1S*|}|Fjzs+*sV7E)2ic=# zL6erro{z)yt{BH>RnCUooSfJl4}YoY?N4{@u`HP$vb`|#FH@dM0iHs=_{oi5f`kf* zU~98`j&F62=#9KK-${5(2kWiRrU>g;P5WmzjWnW(|`UGCz-HS zrwC68gi*w$G@@3MNBizq!H@qaara5)J7dFSHTGR1&DhRx<+vg-9O`>&G>^(gbaJL`B&UEjOI?5vY5alHSWr_?bSZ9|C{S zgq}YlJDn*S?sP$Y{RJyMn7-dDR-Psz4E!dESeA5}E*EYGZuPKVl?P|FPr2wcNm(s> zVf=D2pg^Ifv<`N7#?|_wOZa>aCKj!U9KVtKApx{~|3ksHSRLgQy@$qPy@{Q*oGw^$8nd|+bEEvz?-$9*acsJa-8^9*LOhri5k!XB;YJW&qvDSgriySJJ2 zx_^Eu)iynwv@a6cZ8*xo1g)<3ZIGdpXFsK8-B6TZYwar7aAGAdgf|-drLL5emJ_`f zbMYBA=;P-}-K$A(hEI{pZXWGTRd~*Wv5qvo-d=9t?BowW z!hHW4YuaW$Ac%US6XZ-%h`xSG^RZAcuurACyEDP9<$Rw9e6$$zNzSn0a`z&|<2V}W z)+p}cBDPe-7ZluiE`3)m-QDE2=d|E9)pw&ar}kuZ`T1X-n%!FF|Ic6(r~NG~RgN_% zkm5QCxf=LsIBL6d0_va|3jt+HgK#Y;jFzbA=AyzMRl9=rA1eywp#Z~BYR&=qRwFpj z+yZ0ra_}R?jlxA)cc#OWYF~NgNJxM5Ctpd#`;w#1ZsyzHh~#OXL(hEqfd~p;xvPVP z;mw_*A@*yltcrE0PrmnQBG(vuh1FICTJBRh(s|-Rx!oFIf6(zh>~~>*bFfd{+`@M} z*tmXw+^CW+6rq2(zfTPuJy$C$VRWinw!b?)tL>GtCtbEe(d~{75C6GWc;1}{>1cy= zAFk|UIIK2NmG`2mNqk!wg9G(J>4ZI`HcD>it{3LkW2DsnmMHJQ=$lP>yp?jl5Iro- zH2N4g1%dc*dG9I+c;CdW7*n1r_IE7dH8Lfxi}m18o?hPGIh5{grS)JVlu32ahi-}w zb!6XZpLC#-I+<^jP)1bI3Qqs4Ck!ZK1*ytx+e!X*q$E%qJNr^{N{>j}QqCt37WzSR zXl&4T!>A}@EoCuFICaVsWc5rdTidh}Jsh%4i>gEtJiGof4SC4wH+=JOg4OlXY_yU` ziD1eud24%|N;L&p9?Qtz4KWkW_|FNR;(tyZehQjyv0$e(qPiOK zbK1Y=Eh&Dfn4kOeBqRhKIcaSgHcTVVd)D+BNR#89=Aur~H_KgJT$y%mF&RKPE zg*u_>&$ncLD`dwC0D!4sue`a3;s!bGZhM>cRpj?U!Vlim?J0A=WK~)5TTZ;biz$gH zJ<$3bih4ZD+U@JQ?VR}E@;2DG7BlO9yzuy(AAuvmDfq$uBU!q~Zn2ifoj2*|)aQ;` zouiI=S_wvn75Nkq$kS5Jmjii^%NeVaRD_TG=CHRXMrOtQmG)sN&YOREX8g#1ApOVG zNjavt&4Y>x1Y-y=o>9EMpR8{S0pcowCKSQc6cn9fYLYRXfeiD0clzk6c-?5+Zqzy* zYGcaOU4i$I?{M3C9SrVR_ zhYQLXGWiH}dS*5mPeIa*3pf*FEb+6&*|#(`O*i0vWY* za)s&E=)&jwPpl9#d=*Ny>IHd5toT@!>-}iuTCISP&3%-Tm3OWL<$=HwuR~wf-T}un z4?CSf|CPfaLr}~9F7yWua`*4&S;D4`cw3vMi2lm>G|?c9F9{jLOqRUA-n$6$@=yiy za-`2bV|6&7M~<9%HKR%sf?h5tQ;O~Yt$`u*;!|r9Wk=^eMW|g5;P^|izc5m*o$;>E}QyKqi(i&Reb??=Uc}3XjOL@FJMIpy}bjT zdQrnUMhy!Yy0xCfuWKf=d{@6h#r^RO4Hq=kh_2{APwn3i)=o}Lgt)ivWz-D2_dA9r zHYQ#B`Bi=MKjUxd<=gUH^e6A#kPHpnmCd1FKCiRa&j-)Mq#kW-c^r{bQ-5!3d zwH7rD)sPKju?=x13p?QhHn7;NY7ZGwzIsv=-yZ3n*8EM57F)0XdcoF~?$iB)yh^c| zyVq(m@x<1d@NA;i++IvHnTCq^pKuIA#X`ezxQ{kL&&qPLZW;+pj{;k_Y4$tA{5Q7< z|LB=cFJZ9>*I?303C8TiW7`=t<9dUiaPB?(&zZE>g@H5EpU;~h(Bow50eiA9ygy8gb6zGy~r$bUID^0H?dWXayb zDcn55ATr*$9DMfcj2$)M^H1$wuE1y=nZbcI9-aBC{^K+Di!8~r45S(cbHbtoc+zE} z#6S3Uo(^$7U+hm^%F~IrEFlxCt3)=nYeZIHkIfckRaJm{y%_L0@+ky&3)Y&>6u@3v zRa5h7OQ*spq2VkP;~}V$5sCHc1(iLyYOrnjLM$;0!qmnUZLa!>GP^E62AXhZ8f$V%%mxmCWQj9b^N zo~T(TiXwc*z*67Hm@pZe-t3fMk`;0V7Kjq&q%G91g+*{;~4362_LSl9tjxZp-eaY|cAVN;~h{oqQ*W4q+l19_W zJi#V=-DK*p>?60JfruYoSszo+xNy+0d7jnDb$xpTobCz3Z@*CLn|s^c(Dg1^o4ox@ zcyx=*rBPl;VU`bmZGN@K;mlgNuaR2a`J0JjNy%*EzJ!~J%swNM6-fz~z)dnOzo#qz zEHm+70^e=9;^w+qXo8)wVI#qRKD6Ke`Oq%XK5k~-1(mRGk#YxhpGIN7-DEW77r~L6 z*MaxKL5PK`4qTr=sD0GRR(FGdSY+v?VTcNq5N;gxyEd6*Q|gz=?v29K;(jDR1hi*+ zo5{nT0A}_fyBRJYBA=l%8@ z5@*sbu;JrLd`^Gxm5PjYST9x2^O$axdrcLW84YcpNG)}d$di%|eq$>S!N8OaeB`e) zI}>~hXi(Crd37Xpp?(XZ$w-xHyvIN8$R_p*yN&)7j6LW4(#NJFZ< zd?d69OvBX?jKXW<0&3SN1HIW&sFb)vOG0N6rS`)lGUMi0hP^~AJX{(4JHm*^~D#tTN_)?1a`A2B#B52}QX za80SNzBRZAM5*1=Y**>`Z6MLG-+=L^xtNcnpokgZRP~uS4h|zC#%lY>ixUcuJat{y zaV*Y1DYCwe?}57Xp|ctnf!*#LODb$w-pwDjU?8a1|z@ZmK8U-GVTyg z5L>_KPTbZ-O5{bM^zPv%JPydEFJ@z#8h@5H3@QV~)P0M~H2qClR38wh$b6Jy&CoGD zPh#YAicLM;@IvygywHW$LTYCa&wRbHU|LYLW;x}9eTW~u8yjH@6I#ol9RA_Lm+e&B zm#Hq$Nh=vd!%i#S!|A{?1vMH41$UOAnZntsm1?tJFOiwY!ui!Ej^Hd&h1w1Ag=m;u-5xX zMDoF&hg+HJ%&-6u7Lva2mhJ)a)Ruvr$ychLq8%Qo-H+^z2kl9Z!-Ks~ivDDe^uTrW zf6{RO4}x*q{2xx&gspZ3n#TjZO@23vzj>I^(iPfzjREeE7jc-8Sg-=EH(r@ME|5tr zn%MI=%-?z*^4y{6ks0trFq~4DxGlevGJpr{q@|>1_3rRnIUOHsaBykIyGLyoeH^h7 zLPxutQEZSY-#+VYR%W#`y)V`pVP(T1Mrgk(a~DQ+A9F#0^6ar z#rh1Bh8d;z4qWqfr{ab3eHkYFl^VTmfh5p;8d`-p!1l-2v*xQGdYVh-ZEVYHLXMp8 z?&`1CiwHu+IPK;ekQ2tnbZblKF0@gjfU&MjFK7jqQJ~yGSty%W26}9LWu`5ZgNs3dk1C>fvuUP=&ZrDC8G>?+4 zvoA4jKxc$!FT7FLEX$de-Z7MX`-`=BuTMti6*-HV&70^%ieLGN5%ZMV5U#m5Im_7t z5mnRpqmZ?l66;;k^iTS~^+S7j1X#DCOW^ylgAPrm&W=zK9;H?$s5rT~6-KJ($L`3L zkWi#$u8sV+51D@JZv@Ep8V&8kA!MIT-8dcJaqb22%RYe&-&`2mIub=L-dXc4<1i$?%CO zqnM0laK{-XKw5&S`%622R{}qWuukqzsv29{&Fj$1aH7I!%W$QmlMTafsM%4o0H9tt z1xIFTYEGI_2r%To>>NxWBiX&6+}yHaW7!Ws$UCS7osMHGBZldtr{Q8m)0AH`b45OH zlcKY@q;2|LGZA1sEddyV6`&Io_ps$mwQ<-sxYKrVkHgdR+bqvcoI!y%hVSQUKh6Vm zUlCy`)dOWMF0;@00+Kb`)_*XZ4;F2$GhU43rg3T*VlV_9kq9Jqn%$k^xRJ1 z3;pyaQs9d2iWc!3wdnGM87r}`SfO%nG}XPB z$nS0Zh1;~3SB`}I{-wpdPWZ1}c?Iy;`a9Z_Md{M90|rQ#o9XrU*Y|EbR&L1t;sCS7 z+QZu(rE1bS0agDaG|;i>g!!kQV6gT;&~r-bFiz0b!D0FGe7>o~mp$kyev9c<0v+@6qxc%NidibQ&gN_6H|qsWq71Y zCYMdN(fP3fPcC#d&92WKg3~Tbc=%NpWVo0){-I!NUqq_zRIlWHkz7sEANS@UK*`z*j#{VU|kU zh*|Stkz65{^NI>z)Q9h36m~O}=%V%E#eL{NK@n=zk$Ly*C-SVcp3n;_!ZaZc8&ncp z=~XG3z-bCvEA?bki#onKL;Yntj2L)t@5sagXL%)FsF;8exO~H8P;hMoZEdBz zpU--xFSDs_{R-T;Pc-zjOML^ZCoqKfPw0_8!hi#+(&2L@Rnc}3CUgKxoG;4_dWZj+qIko&f(X=UKKyXhFx|5Np6tdA~LXl2ei%V@N^cevjL1+zvJxr_m^$Kv*%A zpyZMvgva@u{#*Nv(!9(RW_f$W=5pttTlkm<`P+{y%B*3U1ECoK7xM@U>&dt#tCwQP z?!-#lXdS9Oq9$=^G!@*24FSYq1VP4RMH+2)UbjCCqjr}HYQJbOh<|K{$s%t!M?hKv z5J^28?-v;Z;=k3!2V63|5bu7v@55|1!A+w!mP%4cZ^GYV3nR1U5MJ=%cm6J?WiU<* zzunnk>+@7l2)RACrYr7iy(bFWcaeZy^WnGPl@`VcF0EoNXc2GJ$@LviikH zz#C2tsFluYo9~CX+0g>C)@MJMnqkPI=v*SF3xNxplyyIQ-f zU*huK*db;C!RHnOTR!!mF8QJ5@#pcqM&MQnG^0gbB0^@+`>2|yE-St(kH~ukgI8%+Sj1fhu4v9WJdqmLuj^M=KohP@-4tCAEK{IEYBz~ zjhtgeb-a*ARg=E>9uVL|&|T*@*G&vquddo}>gIslFTk`~5fcTWTFwr;+EGjRkjF~h z{4LV}RBL(3$L9&L-L)zolM8r7MuNQCF9elUl_Ta#-yv#RUQhmX zuC%EPFRhpfPIp|4ExkOD(NRkl*-pcw6_RAW3^2eqnuE`|V2)g(*xbDXjtpFn>m0J; z45X@s810T_N92Mb{HfYtp?4^<%t$qCCi$FGbK^hQWo+LcR1@2jeTHY!+yPc~41)&O z=a}VfX`46yG%#khIz2ZcUkg;AlWU#m%|F2g<ppa;1f$80g_p39nT!S(G2G#;mY@R5mj+UL!TTRwxz z2n~raw32cShiC&_t!muq8Vm7Z19BME(y=^LQaCz(RAVPuE2|I+3*^o&dgnRVC_3-| z^uVgYWl$2*nZvL@U8mygc{rpd+2 z8ge9R914wg5hbM^plMs_d`VN={5>Boi=1<&4OvafyM8B7Cxhf%hv`^@s>foF{Lf9B zS!aPybv55ZL1W(ldh{TUKM>RY$M4#N(GC}d5MIn)zwt-!hj&_xm{(-S4H>PRix*M7 z&$$lIb)C3+e%ph=n@2$@xKiNls$RHxIwp$dkdz4iTZk+S>^9+IKE? zcgUH(s~`h%sVJXvdi*tfU$KwuLa>xTk7Sx@Hh&@5f-^_4RhIJxNP0iTAuR%xzwDXN zf@Pe?Gd-Alu zTvGYsI*Q0g2O@Q;gQ&U34os;4Kuetm+!!d=)!u@!fqUX}biRlLLMGLdKChTnncMR9 zfsbmTE%blVO2<}lPloAyg&)rxAG4?j?O_15?*7p60l&%n_GIaC z?1}7Yr~QE?m?U8*)l=g1Ags!Jz$>tS&H4@Xk9HSg_`vT?5lLmAt;Y5Wk8%KfahfVW}BD(eMS zygz^3)l7x&vy6<4hRrlvG>8RJoT)`9MK`NFnW6XLP2tIO0zuwrNu>8d!H|CC*KTcU zF%P^#wanhsF)Pr-;)BTj>KRrG(v{Di5lN&FinOv!?{uD&oSm+5*MZ|)ES=|=pWR#= zWxgVXUtT1vkvBLn0=n|jq4ujCL$rK!-IQ$ON14*m4yvUFsy*#bJ#05Sg$T2%Pja1{ z)uLRpepGd~5#0`?u{}GYE?Y6^ha^sOId^8bbME^c00Fb9sd;2!E+nroc9c4+Xa1gJ zc8D^qmid&VK6S?hn0}OHUI>4;)pBHl9o86M+`vZM zN|Zbfp%s#V_$Hsouw^aNob0$k7o==%Wad+6#2AY;6kQ|)hbvmO+v+%JbyUQcQh442 zRlcy7mro4e)(bhY+z8t2gzW1pr)!`~814$lo(&$460-d?;#+)Z_Gv2CEWLj5D0$C9I7xHKPG8PZHU zF#2SZoYOCU2PsL@PzDWg;|R>6kfq}}xI7$te9Z>R+}PYy#Nb@;?_&mLqWE(HMDB|( zf?xX!18}C)c~z{ovBxfcR6@HHnUfCBVS*7MOq|TYWK+|0OTD&LkMcfj zr;|Pjyjod~2X<8;@x{k-@#7tCUwkXomSD5R@6*ZRVivJ2SB)P$XV!j6yZ@3dyRSP{ z{QgC_^^%}7ix^~w$druwyZ%@;SdhD$DixLg*S6=QbkVQu|1qgwP)@bqxj!8W-z2>0 z@W-juv%mR5rk%J;r$liN^)(K?$J3fOkEIojRG{-jo3Qx=l{OR(tMJ(vKxupYoxT{1 zfBVJiODMq_33;6zCN~XW7p3z;8UoAFH}OROJns_n^JrP$cr_A!#<_*eNvC2cpPA8$aGJ zxjPtORvW@g+PLiIeo+t;Cy4leC5rk)x2AO#{w!?Bdxs+77O{4}jnz>;nvY*2LB}ag zBI6ADioGZFvwQVkLQI@}m2dD60pX;bW_>Y0PLK4?%45Ld!CEFF zBH8UEDBtZ&(oT7Kd7}Ws?m1pLv9JTJ56K--VYpvd0cRZ757ApWR5MKLz}-BOX!+SJ z^%34_VX+^&(ihv^9@}$Es+l#^4IjzyQnpUtiMTq&yeJwrED3}T>@5|DHfb_K*@wm9 zy#ylVIr}eYCn!)arJAL^`<|;uQv?unZ(BH=xA-^Aa}AlBnj#(E9FcfH;N&c9WlX6*s8(jRw83^Gn);B3t`4ngd%-kMmK54O%}b zz^Sarp_9$Yb$|^;K$b{hJh_d-Lqq1wDRAN|sep*Fnk98;>6y}Wi-Y6=3(fVQl~fGy z_>P@4rP9($SpB(_Yp{5^S-%{J*&f4RsAj$7vKc_*0cI8F@SLQeyshWLwRItkFV2j)$B%eH1R~T zdI7&ZSb98oBKV&MJ)>?-%ztIT6L;^Fb((%!E^Q=0#tIS30P36!zVsM~{FhytYL)q% zOdb4wsN)^@vxU31pJ?`?-D;{;Rwq9~I*bRu%5+U0cvt?lSV-KRyEL54PMITyB|Y|6 zSe*8^HCmon7Q?6<+K_>XBiY0Q!(d8&K))x`m+H7$+8y(9-!MoqEJC&_xGE~oBYOntJO|S+8*G4JV4NPGJCWnD#*UE z6!-4xpj-UNLs&IKG-^!&V)isKWpYvSNyPj96&J)+go>P46*91pit0}(>Ngc>!$-%?+4xKtHE9-@|Yq=C=^)!0n zJgx{S2T$Bw*Ky-nxE(RZ&_tB1t|(d3O7%- z3wrhKOPWvq174OM+M z{znX}i1sEV{xDOb9}>enB%H;bJ8S(X9Is?=Oc$Ce{}yDck$3s)&UsgC$TwtMJ`ZC` zXyo&2`fxV2+PK5mWhX_CXwy@IC81__t-&*aV`!>L@4n2wCrK0!sYf~{;&Jib?mfXB zMJyGQ5CZ#^JwOk|j;7|P>m&HE$-WJuWl^{L&9AWiP3l(x&fmqP!vqw#l})U(PU(Iqyxm&mF$&G%x8O#0 zTBw<`x0|hrm|UYj`0%N5xc2w4d#zqVlj0@sGK|dlho1e1xuv116vm8kQ>$|*OJa!!< zeQSQ7^>op`n)P(506H9`pcZwf92=|Fg&GqU+J#+8J$~782X&DaNy*szX_(kLh7_J{ zsLa2Q+;i?R5^DbOD>qsC>`Gne2$&`AJL&F`0i$0S-O~x3+8G5WT;>1i@c;L&zkQP= z_CGO=UPDSW*`K7X6``Ko-R&Hq3%X-kV+~v7%{-ElF~f>1vj?P^9;?{Ms}AiCV1cw} z!Fc1dNevIqhQr((!0|-aQANyxM(ygt1wNRK$a-qbQb^x*clIGFgk*IB?s{Hf>9Bmg z7ZbZEzFEEVt9Z&&a342}Bd?{s6K?;F!b;z(*XLL6n)=LbC$*TzVeMDwK4+!etG{zZ z-x?9*7Ey)H1WoT^;N3c0(7%1oj3eT8O_J%mj<)Eu7Bk)EMxff}idMIiTkVf!r64~F z`MO`8E>qasFOq^%&lfyBF%eU1J7s2SnmGydVI&AU&IS`aj$@vzSF3dat;rV`=7`1C zqDg%e&dEI$AQifOc-X1ae)|F{_t57Bf5C)q2g|&QBhGS;tsPd6iW9Q zG+tU)R$9weTG04}?zdJ7=-$Oyvt1mNQD?u?pX5r_)x;G$$k_rDBr<>A?r6=j)KRqjIvF#WA~e~U>;g)nfBXTU^!`9BEo9>%IM^&N-3`ZAm{{yY%t}6cp&`eJ!#f1T z(YYtocXs65mk49}a&FHmedkCep2qpfrF?NfbHwzi$eTUUq9J}?1-LTS47CHSZN_SS z)_3H?43lRbk{veM(IdzG<1-J~$aN?tF;+zUpln1hpENU z_bFuY%{L+FUj+ZOz`~6!PpJwg2l2~L8L^h@UrxWHMN+ptYhiRnf8NqZQDe2m^Hy-T zoc1jthIKkN$1v~nLaU!nUXU^F^Q`brTmF#2uQEQf6hssTq3KL_^vCtvNtXUb;CIY|?(ENX<>l<^39$8l{@-C^>ka( z5B!m*>wn+r(yagf&FjCnJl+X#ti>7UjmEhoD!!Z7I=Y=SwC0!?ota^y-&l*4=5pu& zdI^csT)E|H@GyQ2*m51=jQBh~EcM>RNAXN=02ayv`Dd}xmm*DKVorPM4)3ZbyIc@0 z0U;qwB>nGF$|Dpq{em;=N1~IL8=m`JnmcxIKXipH{~G&=DRDC>dOv#PhXaQ|a{kd* zS)+MtrGs5-i3=WnYj<18f$6v1C1AI1KvkQa*-(-8{PD8lsJrKw1MFZN%b7meT|T(B zvSlpmTL-DrD71QAsbV#QoDNWsC7=2H)T_k-a-Bm06?f_t!pBkV*HOBHH8?bhd`)7_ z>-oVzTelo={o|6sVJnkvXC9joEF(abQ*qx1r8- zRdCRS>w_E}wZVsi$Xfyqc0=~6)YJ@EA2CZ0`aH-mjrc+iapJow0hLAIB~&x=$t@b# zHX3lDs|5mZp18?XAK3GIR+&;7Mn0k~V=&3l_U}BgDcRrMmK{ltXjtfxb+U&n^0hQM z?WNITLo7NR^d+}Md!2!`teW#Up+SX5t#rv;PzChopUcSst>$IHw#?uVuLnscv%b4slTaZhj zjkX@CBGzc*1W7pD@xoGRni@Azu1Vw=6|=t%C~GBten!>CQzkJC4(e51s|0E~uvWC7 zw~D0lbnTc+X|2I?err!Le-0n4`8Dl(z9$I9u(3EfkoT;L&@zBdHZ~3Qy34=`lRudD zGCIvFCc3oLq=J-)`B^`SjgHO)g|9k}cJ$|?NG8a|OVfZ=w`SeZj$_tj@YBs`)n!`b z`Q46$(7;!D>PDF+^;U;j(x@f8U^7d?86K`?9Q{98O!ONa9Z2A@G*jI)8knfRmX=mZ zYU&4L<=iGLS!yv?tg&kTfnysCQQT;5ryGl+M0n?Wk@_L;rR4T|i$JLR!7LDk*$^*`$eqw3NRR3N$m7h1t)dXjjkiZYIT~ zYC7;Na@DT%zH$KRN`ddme^x%*MMK@2?9RJ^ih9#-vDMXe(sM1tr0_7RTu5c50YKxM z@!!f&PxSo+#Q3|^%+&O(FpKo@U$Q4hx1Qud;db*J8UYQD;15N9pKZ%$1YU~INC z8qzr3{^y-=uPEx6lAG(NQ&&!Tov+kMR_4H-@MVJ99_A0BVhLm0)cmT2O7l(i@vcC_{%frEJ94T`RjENkiHlPsg-< z*K%PttyQlsT0I^`GVi+zL1WVF9WxDvleSo3k%9}^+U16uADh#CO>sd}_+N~eU&N(+ zb7}vmPsR=HJ|Qmrq73!UGShzTn%!I~dv>Yeic1od)I*<}-ogwEx+KKNGc0+~Ze`$U z`N|hSle=J}n3=cDw?C-h%te$UjO_62%Vt-ql4QjIFiD4Z3(K8B%oV9tPnCrM>5u++ zeqI7XC#`A^LvgMcPdq;B_jOuo^YyrDUW)k}0k~dgk^W#S$bGk($OWG{8k7*%8=e1^ zE@5~uQ;BnitHkQ=zLp_Ex(=xDOLibk45dUAe-Sk;lftf;cF#zN5fK97~Ura!#j zl=L{+A^SSy#X5SADTe!d460X4S_Tho2*3^I3?0S}ro|x956n(@!dC4|x5ri zNSa#wwRR<~45fo$SDXM&Xu7$2M>t&SvT7s6iOb_i4e2uZsRHNU@PrDt|7P=ivm!cc z2WLJ8Y-;0PEx6Ndl5NJjyR72)?2iA1iL3!%jOu)lRwUQ4Z(smLs0Dn2-)i`kx;f5> zs*3XJvx;_#i`kd7El+*`+*c3k4Rf6Mvb9NQx%h_v++gVY&3P{&bWdw;*!O3~j+#?`<)HSq~sz^TX87IV?f zmSQfJX&cz7ntFQT03eKRR6=q8lW>UQ$ndz2@CSQ(x?qw&7Y%Otjg5pBp`pgAVoIIS z13Okg3LqKLXtx}~n&VA9CVrC${V_EW;9cY2xBHL(mT@LhIv%f_3dMcp z7_ivk?g9_D3&LFbj3(z@PUS3cxMq9w*UCV-@(}L9iPi6~KYG3rOir-G$OFrS7H8EB zN|%{+@l%E=d)opa{MluI1Rl3$bxS8loEpMKb{ZClrfrsD;hL~@NBPGrHSx%FwW(`N z54*0vABV!gX+9W4+B(CJH&Sq1n!Vsmw(W{xFFO!cgO?u2wDL|o5*@>Ac%h_W&O4~BEQ;|a53t#HQ7Yj6T@at(8q{_f*d~2eZM_n z+9W8<_j~V{84|t zsZT4Z|JhWBwQ`33+pJW?$v37~dd8xE*c_>X>u|GpYCD-rSN(cE`PM`_Ex@89l&~Y` z>brtz3fe}a2Ertq?#QXCYdyVe@q8eG^(2qb2g||lZt(=@>|rM)Bn)}2a~x)IbaD#x z%iT^w#iZ_iX{9_9tzYyUwtF3~Y8@$EDh+znYr;?orNRh(&Jh4YJs9}L*McG2XwOum z#G71|e$dcL{~0wd77pE+7y0fX;8Wv|AjMhY4B1ZPfSiJuE(Sa{mM~})49>nXk-Ob? z@;tTu7Rdnq-Pv;=U~p;pbUXXkdk;_6wg2o3K=gChs8`$4i?RRrd!F>)>pXdrxM-$& zHWz)f@q-&g4(#t-4A^HTelYv{`~{qbU7xh{gQICcIJBGqtG16HsyHTbw_gir;gXNU z8XQ8CocI8D5%4{)GPW|NN1qMA#%(Z-DCg+?xxZ`k%&ty}@Ka|9jYAW))UC$R+gWL~ z%mk@3kYVqGn?!i^PLD9ou{c9k(sR+fNXr`~WGsc?=jjHr{*!}kUrNj=jEN>a_PdAHCe-p2Ogu1Uk& z-zkv$ZO8^~#^_{kLvY^$iRbMd@B7&-J!PY}Arxumb+(v5;pA<=mdE)owT*o=BUiwY zl4b~Y#fcQC<-NzsN-VFQ?~xwu?{`6+YjG0WH$gtrpy2yt$M};IIhrbt%s!2-t?#Wc z#Dp9vY1!XOa$i)xY8KsH5zlD^&D#uNF9vaw!%1>Y9NOOB{RhlBJZ4&^6U;tdxRj`= zb1G(K@sh}{kbC02Qk{++axT>%{&QI)L*{Qt^Is|Og$T-A;dKT(AAe1SawZNV>FYN``9yF^J8rlimFpF47%6lecM`I)qS;*18mxvP zY*7M)?{-0C1@=2)1eV$u%SWkj?~&2I8*$jY3}Ii&om~U!6b*d^UCt@Swey&IR<;$Z z=Ggx6un(X3h}}Z+oHA?khjoj=@GpuGYTeLoha8(U^Eaqgtr~`@rN?+Ag%<&;D^F6}DaCV@`N}TboThYguDida zY8qA#wJIBGj+%+3G6J1UJrJ%pG%ke*l#Cgl36{TohdXqmVHp#aODbFSbiw_?IvFSKfAsn_4b+n3hxx+ zDGxDPVqh}(-9v6hl(U(cDF7NFF)#(i)@`&4I`Tl7nr}ew+zCfL78nx`DuBgc@At1R zRxY>JJar*Q8Bq5Y#7cxXthh0Fwzl>S2#1k28nPnBYx#7}S$SoCEVp1BJ?&f-_#F_T z@N}UX`jK2M-H=38D?XK4=B0A1U%GWZn>BC}@@(O-Y@l&p?G8Qel8r5xQtP*yZKrqW zxG=}VPp(ldU(17K7um$-O;i??G@`T$5%xx)y&2sU8?O|aFgREH*|JLW_FD|DuATy^ z+vKuf=t+$lZHWh8s#Vc3Z)=X^36vDYjDJ6c=UKJKy4=2YD{_Wr)mtZHf-_U*sY-VL zGP?9_Hj44t+329qnEYuRAfxnce@lTcN(nXTkQ9x0THeJ0X6uoZe}&6=&Mp8@w&N}$;*!M zfxp;){aWeemCjQoA73bOZTHVIr9+ktuKN-fe^`IxTlp;FeN4ZIAg}-P&CI>9*J{4W zodQ_4C48;JQq(9?wY_{f>rG#yUvk&fbXDP%Mk-Vc__GBS;XRi>mc8^AM(2*!YC6C{ zO-H%drv&!%I=*ipp-4GK@;VVVWzweMcoE3G-5p>W8hbR5#f$S-LSNAF9)3;=^Mn=J z36E}eXq@esb$#lP?`dL*X$@hj9DKd~GOR4gKYza?!^!DaD?!T#;d@uMQP_dzx6i$@ zlLgHXhBoqi=!Ga2Rg1aS9-3x7-s>FFR6*r_vtp;l-xSz*uFhK!GHH=e>6zCY(tb6; zGAgt1V)3x`Nk?zps%@GX>2^ zvYDoe`e9u3v40o({%`;?J}c(hK0J6UEhHe;=wAR+S)U6R0CMA`*IQ7u%Jy3wJ-_vl z)m{I{7}5IjPXYtQD7l+`2F10Pl`(>YqDoa~Z5REqbi0s8r*^j|X@eUZ%Y zhEYcDMheG{8kByaXf#^obq*rQ2Ov>x80)e(v!;=AOLo|CXAjs@N!tZW{xS!1s#-$( z-%FZ6_LMuEV90$(RlY|m)s=qF8tj)t#GbYyk9D)jh*8JMFBbD%9)o$j>1UUnCG0&O zXpS$&{HUVSJT@$&WH$+H_1fhg6_O%NHMEaS_?{ymwd+o{FGi;~O^}}rsx&k6*i=qf z8vB;Xc^O8|n7)AU4{rqH6!OsAKRm{~qbr$Y4$7%bF+XfZ(9d7_9bNhIEVGV$dHUA@ zQ0Ck9AAL_BmJnPjlrI*DP>1J>!25R zS49FOQECjL&hHkM8cbP*D_8t$SyiykZTg89d>M23D6Iv3fHBk5LwL?3lJLQDlul~^ z=~#9@;&IDfTMfBkCs&<)z5VEh@4lAsfmTi)HeufYxln3VbHq5ATMs=R>bzEH+QhGd z&Gk8Hr+;5}acGOaJ%S3TFv!*mp_Z%$?)=VFcoL_&tu{p+DZ*IzbW6W!wQ+-{jpvPr zcis!pqm}fg9Fd~~07i0JS|DzH>Y}FMp*E_^br0&|03{lb>ChGz<@P>QS_NdCK>AC- zpW2;Gn@{{!kSoL#6HLY3RKC%*O=O#J60sZtwJ;CErdA5SOA${4DhzhF0vjr z{I$uQ>*uv;J8Mwt!r?Y^G5!@bTJqgOj=eD>b4{MonGHd%O7J$wG$RWlWwnn*aq)w` zar8G^vN$m0p`K35eQhxKE>npCj9=FPfkqh1I-6`4z4 zpJ;Hdm3K(j4|se=3rvSZsuF~e>m(BL*>p{y{F8O)LKwTbaVA)*mei>$j#II43yR(L z>Tn`ZnN^y_?R9;#wp~Hv7mWH}f#^X%g8+ z+N>sJ(*ka9R!D?>pB0E*`aX#>Ej9asFY$yM7wyi^&y7aX z>xXQrHFZfaw}Gvi&SJPgGol`HxKu4YAz5L8d^Y)Tkd)X=<)oA?;)LP01i`d#ztHr) zC^vkjWonQ@#pYAQPmtr`G5X9RlgUf0z2Ak;WlLPxVIfQdrVRfT843+gGac^o&FJrQ z%E(NIvOaDTLJp?yc*P{R9DTB9`T_t}ho=!yg&YkJy?Ej!6^oGk-TTp+@yXBK^_Z8& zxm6Uhvh8|5;Q5l`-UOc(SQ&gzMaqI0yS};CuD?JPB!id85>} zqvX&NDs^w{K^!>73K23C^8GjB%0}rw5`Gjdwgoo2!sIFBCv&E;#{0i1acIRuwDkl1gbY;cEGT zQSvE`XMHPU<=cpEgR!?QHmXng#Oz{ZDE*Ca$NA0}98gb3*i_yTE5?lU*r4hs8BCR@ zjN2@3AM6x2Z>|buq-JNoZQq%gqXR34%e)4`>}<6g$;)Sd6nRJldUMCp*f$Z=%s6zd z^@chgJzU{b@70qcRTumYzSslh7wF0P8A=T%0ngZb+uhzUu5%YRlkY8Innd|J75RFv z*3)gOicSbt?~kcCxrcKkw_%mbU`6^Xz9ZbJzR& zD70HH)qTpJQL-wld*z_YzK};{GzBNDbfzVf*Q;xPKA>k6J>QKcVv-m}VnG-fnY>`E zkre@h<4*5Hih0!=B?Z)lBC`O<_T!afl_(9vMv~i?O6lKi(K_#@9m0RXR z^4-N2J6;*fNg|rg8of5p^UrQ#P+I72Eq1$thPWq@qvJzRWGH>aAEZm z%b(vgFTP>z>rJaN?jX;7%SR9h#TO|&!Nhxw>&!*0m|1qyYV}SLg(FdMAu&jNEcEbX zJ{*k6DU1T~J{&<+s3pCg$$6B-XdT?1uV=O5a5bYBmkn<{@P}BB!ft>!-o`55w*#cj zQCb}G5i2VkExXUrpw34OJ*Nw0N`q%|Ht`sPqy6`sJ`3D~_Viv2w!otI9FzD?N&Fr< z>3%x^X$$dTA z{dz6*WXWml<>7dmlVCO88B669Tq?URsU;|*f3rfUdAcVaK?ontTeswyc?lxVDdF3& z)i}Ij`#dIbMMBVVOaiwiC8%#LE}#G%t%Ovn6Oh*RXN!PYct~C})xUQkH(B}fpk3>U zg;}`}+nmN;XKARDdod~RH32}yELSRw%KHfxfZV zweu5j`o3Q$&ACRaY}tJU8Qsb}X+O>X^{htWGC&LC_Y?Om%v%26>4Lit$o~ouO|N|B zSYvUCzt7QtLkCt!C{$bzHULcC*$(g&THTH}g}3OiyTSM8ol>*1dtE?!tVp#Ko{qK@ zOs$J@Oga}B+3vg85U@E&bEsyc>9K#W#^T6c{H_NV?0(IdK038e zZ?Lx6@Lb^ijgMH;vu&RV<>|#@PDo9+t6Nlg^^w&qip@{%I-f98v(tP3wBMb~xOEVl zlBOJwv~~oRrMe?EiQF9Q6TrN4HL~Nb@q8P+_ zKmy_fvd!8g{*BsYM%jnu0@6CW(zVOE&0BFt)LnB$#GKRV+dDf2moq`l1I;_^xtrYt zrixKfCdOYel0Jk=2l*1^Cv>z{23b*Cbi~0{R~gOC5oiX&1&l8DcD?!D=NG_|;ejaR zR*blrd^EF3dfl1#qjo4!CIzbg)E{RXVmaIAM;k<{1U@UW^780G{$* z**Ss>hw3{%M5aRMay%7Qkr~y%f?{R?)pWfcO`i&hu(P&DpQKUCQJ6M)SjTUzvKESN25yZ>e>W=}?Ntk`(a1!L zq98@2$|}3axZPrBFBaZWm_&rq89&0++OFqSR`I3ohJHCnF;)wV1tmWpiG;tY5H5|z z8F?yVmgf_d3{}tj_J<_e^aRCzsRqZb?c`#c!?mlFN9{g?cZpU)uejh{*~YD6X7{=r zU53{y_t}gp*c22Ze!Kq`=(v%1RW>K@j+NYVtNuWtLRDu=h1pN&L=U5hn#VP~--|F5 zpR0o;Op)|n;Dxo;*~4J4zLAV8{+XLI1fPT$^D5WTCvVB9^&vk$O7?5c6TlSji@o-6 zfg+v$h)TV*+_+5W}}m|HljM8~}KVEX3Z!Y0*5ah+(9arq%= z2I(HFeg}Pu?IF!{{A_fSai@x3@dJa4+tv!v!D+nUJWLWbML+Aco#b3bet zxAGczI>_QI31#}#Y(9|a^UV5jR!}m!3LPM|oU(@dj|jE7%@UknIj_k|J<@w0lpD&L z`o8}@EkL4m0!DA}*rydW?}?^NG5i=e$p4Q&lgCLwtRlQAyW>3WHTK?E# zw;=7L@t0T{Jk39S=o&;kS|mzTG5!l$v-^boe-{}Kq+#8fA(&L6Tqz2U%b7^u@tTxl zV%KZr87NAdt4b*9J@J8!OEp;3Vew})6=!vjBs9M#A3B=U*~F^^x!OWh8+&$j-FcIve_Ia#@VGIl;E@$_ur0|`W4we9I& zAFDhG_RDj_RUXe1z`mFHO`mk93Z<|);vTjrHeVyX|7bT{ zZ6ae69YlCFgwL70TP$7}Wgd0u;$f|^7ovfi4D--WQ42w&ighP!LBt;T8#^~&*+V~> zVP@StZ+XR)*-VzJbjIfL;;ga(1tQ?;By5|I)rP-8*;X_^Pm3N>9`EiYU}^IcU-)98 zxEWKh!P^vhxgoj+LEo298AS8cSA|+GtX?7aD@k;X|6WmKIK@d@yL2rZvm^Q?wVRe7 z0mKGx_Fx~{YS4&Uj!Qg@(r4|9anc|ZW_K918@OfLzy_j67dbC7Sp8IE;HrL1UL&=rS&U^fxO+Hl5|Mu4=4YjQ`{Eaff@I z!f(y58wv%R%b8m;(Dv>|>mHjP`4Ey@K0J;(Jdhu@95J1%N<5s7`BG+B`2jy#fez;} z+0x&C#tDi*3ul}j^MQtJywOwZH)i5rF{C^7Xd{zM9%HIuZ$z zr}senOQ;N$sqz1yg)Rbn4eheE?d7*c{>LOUt{!?F z_Aor{FytLzZXeOfjPmjV)7c_`)45LLhv$=oj7&0?Ef$QMFv&YuZv0HYMe5A}JJ6D( ze@iA2!5gtkeu~23u+j`<+yzoM?sjrOQa2PiB;&Dbqgjg$-6LXit!|Cd{=hD+l+B%VaVE<&=nIz_;&! zAK6+wj6T?`)mai<(6!FUU2o&I{BWo;x7o zODLoDXkW8y1=^)q-e&u!0;bFj{hg<|0n)^(eCdLyh=m4b=`!i%fSeXav3E@?sn#z7 zQg1*(7nyeoDLO`;|Gb8M6)9t`6p4v!3W=QLY?YRW&*_$bny_zFExZla0Q~{D@SL2S z;RSWG1Z65G7|9mEgQ$yF@F6A|Y791G9WhH;&_0B0WMd6KdCFn+f{28C^fIlPPIcX9 z|DgXk+7FSvSc*#jo$^dfagcvE7W_eP6g|Jsj3p7-vTaxioF^B=dogH+wV)`M;J*@g z%cFWID*6G-iU5D`={NkjP6dWI^-#W6sC@n5gdW0DTsXQVmB&=pk6zy%H>EhENsuC6 zDY{v|cq)TxoYTXW3@K!ttD${M??eGdZfh|EDodNBlup_=?&O4`o)P^@S?ZD~^8={z z3Gqa(c_jML3OKhKVo)|#;Wr3_E@-nc9zn8E2%@IADuAJRjr`m#xlEd^SQ+1#FZ6b& zAk`rK^_w*sy?P9~a@Vf9NNF*`n<~`jgvLoh;4hS$`l%3YuzrPNYsGYtpW z;hUCDBTfgr{-Z*Ds$8A!ao|}2XWgzEQ+YG}<48KZ9K$xp_{-c$I3-;PlHe?DZ^OH@ z-UA5DX9ABWYD&snpG{@kKW$WhCpB_%a)1Y@bN_O9m3vk|YUo%{>a3g4mk8M8^%*{H zvYI`4ZRE+D`P3{5&bpROA$QIbu~D<%=TJKj`SA7K1zU_7?{#@D362L-SnMUvVlxS#gby2CdFb@*mK;wJHTzWZIQeF?9D(RVBGw10~QrpMi#w{xBU zq|hC<-qGvG;(nC%7@l3cK2PZg_Pgr52bi`J1d)gd%WgAF_wPx`4DUGS=e(OoCwW$O zhVBP*`C;GI6PsNC7X{n_@NihEEJYqHHY)7@?DT>zXFp25M{?DD(xUs{UdP8Ok^j6B zA$RkhcW-6kp0yG!7D5r zQy?Gvfo6YWfOi$Ba)%#Qa@)>TBYR!$e-2rryf?l2)0Nu&?`OS;~{+aXwTsDtnaM8~A^LDF96)39&09=PzipEAGH) zG!dI0H@1Ef%FsOtESs|KE{)B zc62sz*1CllT`6aZ`{2~ep*FVPjFg@`_X^Sp>d=#y3nAZ7o^FX?`ug|E6Gl$DSZIOd zxDDQQS-yzd9Nxz(HZSJ$+#$ih#6Z7n1xQa3fa|CpT7UUnzZDSh22fEaygjqWcC-RO zN?kK{y6C==K9`)k@LX$bOKt9r8n-iZfM~?Kj)V@ADN~hkfnZyx;F~@ye6kb-+7oL+ z5@a}ZWGhH4BWK?qwAS~bg2lxpt7`U(LJV|SS(N#F75bgoryc&y!RQc}*^N36(^q>8 zDS35y7S#Ro_P2bO4 z-O+J15(3aUux7PWg5f@8)1_0^m){@yK0+szi&+}tcX_av{*N8L|~Ifh*(8K)#3 zmV&SvK@Xh8bfLsqI{&q58^ONmbIVE>=VY91wrA{bfR%!wOie;%D4AN0DH(3=$ZGSO z0l_bHylg{W!+)JD*}DQ*B&FGV7QVj9(knob;8!~??%7XanhUv>m6u~x-)wK({%O!Y z?W!N)@wqSeyQqR*+)U;?)*+2@QMth{hp)^5iuBzdVAMk*KRR7Ms~5R)Td~sPcjxeg z@3!JX7$~P<9mr%E#MRME?oO+gIfeFc_?_%zW>!C_(UxAu$DX{=&mpZ(sU z^IB4`(+Pjqg+U-Y2$KMC4y!BWT7eKov&GZbCs$8k$*1PAHQs||@sM750#jpO;^lS; z?NZ?F{U{)HHb&&Zwd8?VzK-m?!K;+4W;pV?qPynYbgB6h*Am}S=&p_umzLg1`#vV6 z20=+|phRUmda?$VxSTWn+fEC5dhmN%NqKESPa2zK>>P74i1^nb^L;kr|As^#v7vG7 zlkRxL{P(b~-KG7^t}(m|AYWdmYnLhI_gZ>OPm<@j7>;H&W+5v`-fU-~-4y1+l+7>31g#D|s4ck1jEp#u4$p_9q*ntFk$^jD z(UBc9$%KB{M8(P$P`b}q&i|CAai#QsV>C%^NwP0|GJS}St5gW+GC#DK1 z7x3o^?1%&x7JvZ?IRGJTOBES4>Ek4PsmcW_`N-YNzF{<-LNwWZhF$jfe9()-GH-dK zh0S!mg+9@0B#TzR$D0`vicjGBhHvdJxNUuVGhR4C#=CxRJkR#KwK{iQ+-}&aaZB9UCZBjH0{Y&_*{ z>vTr|YT;VWBQNkNW=#)5e{(xX`R~ek)wBP)g|Io9nL8#02txcMJ@F8uS)^Dq4Ro1Q zBFsq3z|(LujrRkRxo@&@Z*8vyWOs@^zyuQ$^W@O2Q#Tjns%YjO*DW2gKpA7ss^>av z7RJr1BznY)!$weAjURA~5sDFC;<~1-ZON6tY&r4LGy9g4N;psl6*I?|U`*=f$FW<( zde>PHG+@ESam?qcVA>u4LQdFw@meYany_L7J{e_?-HhYjoLS|56LE9GOaLnzzkW<-{SPxh4+e^=Ej4l2D~zOWrOa(d2ycTnD*cgZsTVIGI+; zwdi1H=WXle042(9CFKVK6Ofh6S~Eh6tj?*9)}cia+bNs&soAIb=KNyy=sx_@M@TYy)~|YNZ0(P zNXpQV%b1OV9mIA}kTtZQs3?~i^$O4LYu2Z7A%GkC4*$K6nwZ9wBm4DWbS07zDb_hZ7B%iQ)q5BON1-HERScNK(6Nly0UX?T(anCroOk`ZKef(e#yTz)LHp=Z%(GUa<6w%;i$jz*c@rGJ{! z#&LY;pJwxz`YiwcR4ps*+p<>PU8CBH1g}h}b(6oND=~)b9I+UW2G?gMyv~=SzZD|! z1uZ2eapHtnd-JiO;^AbzTi4@$Yz+PUg-ZYm8KArUcFR-Ii!vwF-t(Z>n`15XkDTwYKP5$AuO z4J$|ghwWN)+xcEg#*e2nIklWWcdJEMITIYr42v0b!1!EJQVg-broB9v4>HMPRQ{fJ zCm>rcARqwt*^SCH<>Ka!(0$e0y$>H&wSo8V4!XGB3KgJ@e(4e-x6l!Q92QlHaKy?~ zve^$36Gj<#GltlRpcGtcibBqdWWlb-Sb=1N^`&;MWLxHTx02qEwD;pdri&=~5vDa} z1@Ax~X>n%{TX1H9fPwjFf8T(WxXU@(t~r5mhkw_X*%d4g#$Z}Iw5eznpOm326rS`c zd%xMZC9U#Lq^l_~8eFDG2{I1)*>FtKX7Iw)Wn;Vp_V_rmOh+=cH|09osAyaxt@-z$Rj4U6}5|8s0Aq8tjd3A znJ4Si=ZM+J6}Ar!^eZt#R;m1x#ltk}xJ*sL5}B-G2oW)R(E&;^h)R92qIMakSU&3B z9(bTXT;#ioYo_^J+H$w)&^!HHeB?Zj9`iu5A*Pe)1nO9*l{h(4dX7V=^JuPUYHu@3%hGF;2xs7fMBz z1(rV(F(CZxrFVvB8Kgf`b`|kFqs>AQ}cKq@<1NO z9tG+YY5eY@9gOxG@EzOu_E$8a)MD~`^_YAWS$*MZl$*~Z%7xW@oYf7T-KRbkkfNJ% z?W4ecpjC~qid@r+r%ysR#LdkqO1C2Fl)oM?;x(Ri0d4hT(sGB-ks-8gEOSEcBk4lh z+oha2xG03{>+3%&af1JuTAZ3HZ1eASl=^+)y_wxaG8L_^bp*t#;k^uxypQK9Ph>gw z8?0tL>kA`Y2)uFqoyXx>ZM@e{e~um`x%JxoV;|3A2MvI(F2Vhx;hhk`4Fl$LM|mf8 zUpld5R4xsCe;4W9Wyiv@#R7|J5y!YvkIHq-xL1-7QB55T;sSyFH3`>HtwY!Z@M-K% zreFm-;NpJ5BaAij^d+wH>!mp!75ft&38-WN=qt9P$Bo+eks?>PQB@qh}fa=Cyu;9j;xjh3ft%5#5wDb zhbff@GW*s8;c%|j<+BySnbVH^Tl+yHpZ^W1PhZ9UpZCN=0!L7UZyeK)qi-c7vj!Mb zlsY~@(2Wa^z>T+RYZd17BlJ&}xZarbbz z_JEwilrWTlBmMYSo4LZO(d-CzVarGTp88qCSP8GI+-2L<0T=_tb_SCaJ>i(kY_Wam+BDROmZi{WALynj)VFH6q4b z3v_?s)9t+@+1i~ocrY%C*qQr=IZ;F9$RmKI==|oPLA7c&S-*?wdUS#o zEawe?;3Q=GPM6=UYKDBm4SqgIM~5|!pFCRA>h4mOzM71poIyeni~RNbOSwedsTia7 z{T{n7kMMVLHiZ{iqHjYpHC$!3_VAPikd)jmol{;%n|w7UUM@l=qoaTnBu%kI%jT+v z-(;SWa+ z18gZn19|gZ6Z(OcO`(UeryD7l-{aU56+nX=YK~C{9dTSu?TfEyRcQWYL1QNfOkU4E z-D&~s;qn^S2hsccv4@y2IA8Jt?>)FmPXXNW)ayer3!y{vF(bI!bcZnUdm z*r#3)GZD=4+!ykKzcBl@rS+s3*lm(pC>xvGV|SjHAIVu11p@b(0ctwB<|aKa`U6bE z>w5|S!T^F_0{!>HLjzjzuXu#w*Nh>W6+I0Vi;%7Tv_e;pk8OY0LWdkk?os<{24+O7 z{6@!EB~P#cVi|p_+fg#b8-6Sa)PX0y^B^qjsvpZqXG5bLl>bYbyij5OKid;Oi!z0c zr%cTr4Lmpg%>L2g5*2`~5@#kR%)-jTCm{g1*4l{exml62>^{Rk467bMr@k%aG|9LA zh0#INgaWWEPNpP z6DjF4x+Y;UYk*;^O@>u`=;W$(ze+^1Zn5KRx~K5u$JoE_&i|LhVPzHZsLfMO$%JD{4YaCO_< z24#J(%t<7i(#p=gAOcwx<}b~(Uu&ARsfR9$pm8EcnWCw2J~$!hWrIV~km}C$G8U?b z#p?&+Jl=VP6=vn-F(D`O0PBx3?PP>fXn`Zuxqc-LGpZ-4W4sL`GNml?yZN%TtURg@ zu9+Luvx*qfy3WyS>+ibUwQeQ+9>oDzsgm8bb58K*?@X0EIsJPJ?-yxf8O^cMb2e)h zKjIprz?hNSM!?3x%w=cV{bS%(D*5onAFJ6y(1(XTwXMIw6Y2UZUWCg1{_fv# zPNlQ+aadb#^yUkvf74qbhFt$w_sIdQhn+N-86tS+SE!NebG4nAeY9`uemQ<6y|_gi z=AZV=3#Hz9y?^4L@lof@@*|8G!G4rPu#>YFV(g{!yAES?{rs4cIy>M&S9s`l>(*RT zn8YBXvv%DRer)gxV8xeCh%SV6cfT$Z`P1qysQ~2oUg)j)zPs?$HyBMdOr>zlVjkkL zZTn|qGuT;$Gi4|>GZXs%!w-X1_Yp`fv68pjLtTiZYO5{04wC$>6}z$CpcP-$%id+N zOQcPxS4P&^&D*$!SlA`bD+mSo{^^AF=C~DJce??{h7D-v=A-H!fW= zy92Wd$J33PsiZ$edhXYu)-SI+)u~p8pE!_dwJ_#A3|G(6GK68yMY>ljpQL^qS3N$- z{%_Ru>HI(L`mCyCFa6niP4W8q!Um;vYA2w_$GoqSlmWjx9VD)MDEv;K6+K7HPdT@D zbX1v>i)#&F@Q_g$`eq*)W5owZhuEOon9OIidLz0ug*fiw8bKy=AGl7D~7~aFy zU(D)w)Ogr`X}72<97&l)4@=GxLmSSfrIXzjsHA$X!qvh;XB_sL`Y5^HhDdNPm6EPo zT7~d-#tzy%V5JxpZS>WX{`DJE29eN&sSv~laf=8vS>ZvfnzE-jwW$U_s9((2iYx=r zMHIx#m+AFKgOH?zK2LJi61Ic z8!{QbVbYRzFq8uRo`Mxj*Hep6_!zBp8l)rM{p?|xXUDVrVDIVWg+^bg-iC@X=<&w` z6M1A=XwG;s5kWcVQd*(fH3BW0CmUZ%_MID^E86?V40Fv8Jff5TOvQvfC>Z!B9}8N zgEPuemkAaQtm+;SM9{kteT)l4@H<=l&KztCe0}NG$J)6Vkkrp_TBZ zU!PP0H;dn5-d0aptLxVIpJ1;tbYDr3?TBW{)Tq8`Z}^BPl_l}~no3*vodC(VoWHYAg9FNsd5TKV)}EZY6F;X_i>nqQ-Z2VPn$M4cQP ziASVZJNbhdwHrOY^%)RQ>I*)j_$?H$85p5O3-3{C7U6I3cdDS6hzmByh1$KRQnkHq z9#{A(XLG$6D;^NG(6%XW-wPyQ%?v$Hf+@ZbBG_648}Vso)>~|<3fjvla_-<1kfV0D zAsvs)W{Y?yTXFX%A4(bq+3XIl*I{ucugoZ(#@wYkTdrS*dmXh?{B5|RlOR%PGtbeZ zwZ6`4{!7c!k@Fu-G8M)Uv3W>pmNXzmXFmR%r`omMM+}f3umQ9V&EO`%u%Z%k&Jgx0 zjJwzAtX5m+J5EzS_pkk5gIxlVYr^(H-9{L=0$6;=#LN&^b9udlJj>mT2|bKlu7F&- zObJht67I~m61M9LsIzOmQjbjHH>(AXo?^${IrmieH->&FC+B2<_~C=2(TqgyyqtVx z*aa$TDOh+0-}YNAZ>*4y1ly-qVYKO!b8InLBBA%%1hvy5f;%}cBeU8|N5_ny*$wrD?R4uC4i&yWC|mE=P7v*(n(Km&j)f2{8Fbeiajizv^?nj^5AZ%t)Qu z@U=4p)F%mX(6|7g8*}ewi~tfA?inr|s&}{@NBLEe4kBGo(vsRN4;dCh`>@$`)(%LY zr5A+uW{thiNvIFVUu^=8C2|~(K^4kKSrCXwE}#i4CWn#^Y8U2&$%K#AFG$wYbQft~ zRDBMepSK#kN^O!66ZBnlS;dSpO=mB)`Ek9$3HwKD2pF4IG6mGkU!?mY*tjb+Nk`0O zjdVla+=$D5S2R5HCb_cEwLavWHV8ib`05$@xjFcJf8JPdr#0=82DeMmu1^r;3Nd$2 z>o%RtXQoPMqO?$ky=yODCzDA&tMMP(x7!;BpQ@qo!JL1a%IhBnEf=?gT}BDe<;a&r z)-yD&MIKzx?`aJmiDpD}kd;fW`lt>VSZkuVgQ|rkWUXFz@nDT-dUFKiRh-u&58;|+ zky&PGHwQv9@l#|YG~V5uvcoW@zcR^t9OPYC2=a-IRoimbk3^eQ&fz`7l!M!jsDj1H zGQy*UyqRTy9OIyF(q-7Ehj!lN;_8~k9_hZK4t8PtZ(Hye&pF=ISP&IYz8-7gy?;x2 z^1N*Z?RhIk>lpgiytX&p8_nxmyaI->$k0)vO5`7l9q6u1p}7<9a$O-sAx#MaY-5am zd8+y87nVHVjh0(3P=@&3$6Q8~VL&_T=L+;R>ZRBG{=ThxtvY!X!Sq*s^YMeHoc1TA zl>L@bfwiMdjpp4gZwEwXJo9@ZG<1|gHg&R>|L{Kx`T6Bll+euCQv=L2HptfFi>`7k zr}{u4n;ajjKT? z`dT~xG}zG)(20JN0$6fHDR2h#b)DpCq#9Eqozw7JK>6wmu)!C{7iMB2Ek@Ti21x77 zt<@wAH5Aq*s==3aSLNTjbx}F=Jm>yOD0l>*Gr#E|na-QSF;PS$ zT)LiLxUIQ8UUc@qIw0L=JDT|O)8LtA>o7YH*)SZY(TleTt`8jp| zjn87c1C)8?4!PfI#R5yGfyuStD(Gp4yLagjVBt6d(|m{zVZP5l7g`X%e;+H4EdOz= zfEYub^k!i|kc^qpUO0o8$j;fBOkZCghz;>X*x2A#C(s2`iHiFjif?V1qr1276lWZ- z202jhR%w>QTUQ?F|K=OMHglRa<8!mxkMDd#3Jma{BAB#yl)>ETLQ1u|HTY#wL#?^C z`2OQ}AxOUO<+juIf}nq-DFWi*j&|_@|Gle7NGJho_!k!RD)s*RbF@RhE0xvlgjiFE zJuQ0)fmBX!a~b;7Bvqm9AlsBlcsFh4-*=a8O6hfjb6yc;>A6!VoG&o z1Hp%t_AB^Kfjl2b40rY{&In|?7?AZ2dyd_=eOOZFk%SWB=}G?Z-~&f#uLj4)9CH8o zQPC62xYOmn;mlY-O7coFNP{l60Jzz_b; zucEDsUOAZ;FC*2$38LnZ7vyYgFb)n50L%v2b|PY>4JAOG>XOXu=taUfme67I*m5q%^$!Qw3atd5WO=mDNopFyR0ke2Rd=|yts?(T+fpKrcDzV~@w{$qw27-sIh&wbAMUDr=dHXBxNmx6Te$Qho5 zbM~w~tnN1(!(d}ux$<_-;_QgVFW;2xl{OeH(GZ`2=86O(z~=*soHpZq%L&%;yge_) z$iyU!Ii>{&hIx`8M2RwjbD7F4BZ*Zpz0cnp^8V0ttT5Eiom0t)=46=904w)&to%@h z5vmw;OgpiZXYtiizx9?+&gTDM1SUK;^FvMd{!B!YII>-tYqB6#4SWyrR4xSp!9YN4__b5u(60oc}m4N6d+a-Nhf)%0Zt}Y)uWnt%A<&=N?)N zf!gka((Jlgyl=Zo?zf>?Jt1+dLpM*p_AmyEnV6dg0Y=rHbHUmPXcjPIaovGcih*RP zY;Ji@qLCY&uz0?4t~Sp~kbmb&>H}x%9i#Zw5ryxuJE~~VgLN(kSoWD$2nj}H zxy?o`@1=}gJ&>xs@h-Ur+KE@;G`iY#PSJa4pG2JsE}i$Nj`-rQBp)M^?Y&m1>-KAA z*P!_D#n6WY^#APpCG6P8{a53!@Bz@pu!}^J|AUV*)&%H4f8}=%UH8T?7ff)hd`s&y z>pD(a+}zmUlNIGvg##xu#b64VQ3jcmQf5Ta89CF135*JfFxzwL(!x(2{;yi8RK8;h zA_r1RJqr@8d&xkyHRaBh)QO2&g*vW zqM9%VONVppreITghp$D_I`Onv_?m0NH5GAe2IQqz>`%X-;ERe}KVDQAvzqu^?{TQm z-`si-dpM`dx&knp=f_)#G7R&@COmjp3 z83yyHmVEc%xR7e5M8wLCL^mJzFe*@8`QgvdQNH<=Qcwl<8u^i!TuwBpdgaSw8(V8q zR})eQPRnk`xdAegd)+hW#*9Z14qux5t@JBa$vpN%3ZcW2C1$p0CliKQxQ8`VI^<&PK^ywf>8m3mD*LnGfD_ZX+RyPwvOxj!r+d@nb^%5qN$*=8u_9~-9kp%ldE<$$TXt8&$;dR8O#le z+OLQQvwO&lsa{LIi(-vAM7|Rj^&5e*t_g6pV%2K<8>ljgk1J;kj-7uC=b-#lA;cdE z&Gdf4DQpbS^j5NXs!jOJDy#5)NO9N5Qh;sDwfD)fHTp z;O_fo2d)E@pJ>Rb-g}dv>3YHQ@8WFmbqb}|<87337LET^#4HSnB?nKT8i9%~iH!;` z*10t9anZ_;J);K^D;~+wWM9%FgZuj4r%QX2JYFn5P&e()ANJg>FqLF5`5Yg55mD+w zq-kycl%0(|2#ZSxrljc1Im1!k(@A|H!f~6NW3mN>*_0w<1~~K}L(=;h_w}>lVIJdyQP4?g zU@=~m*qgNsvd;+(V$;9ybgItJm%8%s*jW8dF+c`-FU5(Orgi0_-*)86=xuW~`OQ>~ z_KuuVewKt(k_r{@3JsfmwX*eip?n{e&rjcY)x|@^w)4ZNyf3-*!iBEps zZLWKl*)5I?HNGfVoCH*Ha|>!yQ$e>tTLci0E{&TlDj+y3tV_7kLj#(!V24s#iUY*j zqiy%jXq!zpH!M;fUK*|O8?8LI+j>7IzChQ!GaiJu`ZVK+&2^WN#v{t~ zR2m_wW+YyI3^+mAR_Gli^~p4}@S}D1{zD88NgOMM*cHv;qZas4Jygyk_!2*@Cvm_u z#c^e+9W$QZ{Rsm+%E{WwD&@ppr*!tOynlX{kM1N{QU%lI9=_Lo!ixLyAJn#efL~o$ zRZq)NAa7{VJ$LOKM=%AIu+T4I-MDnlcWsjJ1ShvkloxyXh*;+lu9n{BgplUEH9#rG zJu`F^sokkUPn%wpo;{<-S^kzQu5u!eGb@L+)q5Ko8x{RyFf2fgS zDH1n&?9P7ws@(4*IDwCwk_Ihw&+T&GLBeP?b;9uQ#C#8$+Mgjb4iKq49G9>qHmK*BA&-RMzL~E;^mkt^i|KUz)atQk$c`br@lDbm_q21Lf@-Kso zboLz5ReqW9`}fObt$&~84DcB&{j*rOF6OXR8;I?OD~$uq6c38d9GInSx>|exOCknA z)|H~Y^Ajp)TXGclCcQ}@NV7x1-5WnJQR)|u3P_1U6Uu=wY`}Q_)Mkj^#Co2rGw94w z0VerQtS3XlBs@80`9WiEvR_!tnO?ft+tPj@Ahm1erXd{KTo>!&&E6xMmlv7usmqAmJ2Z&*tN0IpiLg1e6gq6{L?#S@~) zWIgz6yKk#)VE9T)G2gG9X4ACdTTTx8_;2BOW%DXB-ExvEOIs^&NJ~pSUPzW4 zT_U4*T{uSR?JN)`pvhJ5h#v*Q{OjLT`Jt+9c8`f>oeVy83DMF&)s)Eik=`42TZv9j zdenqA?FE3W#udbKg0>DRE_DEF`8{wxVevQjM_4j0u`#>+;1r+&GhgIzBD$fi)j%Ye zrQlchacJrHI1xB2<3q<%%!`7rLYRLDB81}N{!zjQF7DaNF78iKebIi_QL$T_c7^=6mA_TL=4;J6thrb!$QWltICu7>9S|6uG#p>juwecOsbFzDNOq!gW zE>~hmk#BDJ0a)Oa4FVBhi`1G$nYtOx^|CWc>AGdz&q0q`y|z$YIK74NV0w>lhEYsO zLn(KrQ+9=cP>!#D%#Cg#IpduN-7R~;vJV$H6_C?su+hhZ%a*XhZL4i)^+88s)6tK0 zGs?ECsb&b8&#AD`Z6QVf4R9zt%-g-!ZoMjKpz83h_VI!keLbTs;ZLdJb>>@RJ1aWh z8z{Y^o+2mR-7!vui0l(zTjEc!Cl7ClrK@;pBweVW2>lM08Qx)o%npouE))(Yrx~G; zVaiiF-BQdTzwiP|ed`zU^6RA)Nb#J{BZcYjTO0kOp~8SI&P{ zn`VC0MGF{tRJ+%?ll`rQ*+ovJ^&bVmO+=vg0tV7RapV>E>^)J5JX(X44vwgPN20e`PP11 zTtlb1t^2M{J*a4!Sd=lk0OoXF7*!+ogKBG}s?VJ9U|{2GeEh@pO8kIoqZ3}3C3jW7 z+iCr?s_N?Y6EKhfXp(ZUj%;`H&M5sWSc+Gx+Zo$vN#mRAf)q}q&68f$<2o1-kd?E8<)EVlrq)^kw|V6#1h7==lkd@nK`Y!Q_$vP`O*2bm-ZJjj?dp$9t zU;EOW79%BW^<-j&7P;R9dEk#yY8b{pC!m{|Hk!hodpbn@F{Te1tq-NJ;Mx2T`Th-S$JN|dT3!F-r}F&-ZFTHjp3^s6X%>>lk<()rt1anwCzkN3sP$PiRS0Y zusSBZaBKbI&a3$uOP0Rj4tC5wPs5Cr?XXJcdX3jCN%DH08pbE}(^O3jfo45o*0()l zI%o?V-36&U*{R0CXB>aD+YfRubV)S{6F&E?y{F3hVYHJU^LZZYXPhR=ub8+>`s8Tf zMQCua0@RRzlCOO4n=n>QSVf^uIw(`FPt9aNjy|2M+jAlzfT(E^)HNxpV+A$0$#W#f z)U3(P6&83VLeOt*Z5?Y(-BuhpR^wXMPS z-n%AMRaLpVMJqY>GB3?{d-$b(b69F3*S-zb(}FIcc9z+#gtdzTuJ5GHHB6BCMdO_jhx9R>5 z+ohxFUsZf-!v7fDS!)8NP9+;J1lD>+<_F-0&!VgI^03bv!v0ZMZlu}2c188(y3c!? z#a~)m`)3vnc$WsZFQPhJZ@l}=Fvi9NlaoOdpqKE~o_X z+i={@@L~c*WqA)8F-Xz`_PDIkrt9Xn#L)q{EcocQXxq5IpM8nit$luL)Ob5!@o#p%t7SDfjvwqE_zF%`4o8D!8n&{1~Soi+9TtFsn6ZH4$wa5>qS2MQbgE z;9rURY{HS}iSy}@rHtL>yA^>DtWE@_nSfP)Nkf-E?T_gvX$IX;i+q^78Ay*UN(^z= z*1b1ZEEcQ9CYnNYQD7Klkcf#1_sDsXT`YIUr{2VUYO#^-6tPOP8Hmoo{c}&qrZrSw z;HC1jUNQgUuc{Wr$i2A?u)qhuxE!yc=g+A=x(NK5EB#b-WR<>crn{1=`XVO##v73* zGj?SQP<_{H%9aArz5EOqj@;0n3pSB+!ECfE_Gd^{?rlQ88*Y2{)dAB_1Yg+P|IU14 zI7bSfsUTx@pxi6O5!Sr-q#lC4AAyq02=MB~ON&x16lPGj~BhvLVH0J4Ze zw6Klm3* z_o(i2pM@ilApO+b#7U8mp}|3+QvG_S+h;${hu2qrCfsc{vz4>aOZO=QwkeVlVY~aT z*||j&Mh`1m<>lo{yK|kXdS=fJ)i?1kH(4mmrWt^K_Qw*A-hGX(&AY7Sk8Oj2NFYpV5Jsnfv2%KZtcpdfNm7HxM3E6|2nNC;FzgKIYGAm@}-rP zSh;~|n`cQp2C)bfBaK)|Fh%7R5u+rjBVZuO5h5&cw}gf0TG z&A`1PzvBLC4*>}Ur9liZjqU2nXP}p;v6csX=3n^b`WRhk1ds_BA5F!;uU$60O}Il%!pk)aMRPf zySGQ;EeJm+&CYvQhzECJf1`kI z#mySffTO8wuE_ymD;+i|VU8L?&6CB^1*V0nQ7YoJs<>WtZuyBMNNiEGJLw`XQl#9HN^PJKV#hp zobBV#btgJjZ-nWINCvWgV3d$}D)j5=FDPd@MrgpHf3pub^TSU|;G3V82LdKfz_xF` zT-GpFS@-7Ysq1ogDvvZ>oQLjuY>hSn;-lycczJ*9X#Jg+c?o$Fnp8)xx%Ck~cm^CM zR5Pw#$t}vK4)iiy(gFkw%>g2uY3P40?e6ikIPZQ!pLKtKY-HoV<8>!~8Scz>FdVBWaUVNA zj59eE6bxTA@4O@H9z6g1Qb;ttDtCD-y&}s8xSP9dxKRFFRPtlzD~a%|q-qrHRVE!J zdSoq>tzGny{C5_v)%JfLKFmVkRCo4^&cMB!yOl8BQE3X>l6{ShKnlZu@MC|+#+dC! z_VxzXDeWw3w@^&U!r*~n1Y&s_nuP6GP5B!i7~gNpH%`%M%hfVW&{{z%v|HV_u>oBX znCcuIGEdvJY@q;;!1RD}dKZ8>8y#x9zf)(7juw#y+0rn-JSekBiu?vvRGj=ouZc*U z=;}L*Q5CF?H!s$w<=S2~E-MT;!>5SkoA6n;l>E;G>GIErhSI+PAE}a&c5;JKgkBNd zPSoJZ3gjCtWETs_z)!UhA*O6(-|$(dZaCz8MgSOnqArC%Ly|z@{C2~8W-3{fej6uX^y56^aIRkDJBW`ld6ReSERe9 zXU!R&Ha*?SRAPSPl!E&^9*<}A37+$3Co5Aztw7qk6JJ|(Qaz1~PH zZxLZ$vI#xF`Kae}O0Pa>?H?>i@!H|$gzT*khAW*ELz_$)zK^}(z})6lIhG{T;dEv2 z&5ZITtxkYUM1}$Kx_+`oL?@VYcUMfYzEbtD>6u$p=(28z%3AuqUQ;GBZfZ}h?<4x$ zLr~-Kk!Y(w%FDS+7AB0|x*H$x^-Zvd)WCFdXnCK18pMV=*qts|?a93Zm|b;y zGQhivqJ1YJ(sM5tePw;FlYDi0;Onc$Ct_}k_eqZ@M*VchTg*|THw916261TIq85^^ z!4p&4*OOnK$zC70GNm|-OfFz2UF>H2tmCcUz=`hpRd`06nkZ7z3B_bSVM*c5 z*^efi&V`^XwP$Gpsi#tBU>N(Rn0~Vdd6evf*g@0ju=U1&Db8jY{%c?KcPmRXl}%G! z9UDPjA|!hvFc6!Im-ioGBWj*s)yp#iMDRChah2j__h)l-e)o!KmkxS&Ez!s*_-zJ= zTJeNOj(x&<+LLVqY!JpAn!@ZW+W_7txla@Cab=k>nn+HK6F2f29V*abj_|{5aAI z(IPGp8=&gyn&qw#(t@eqmR8fxVM_TLVuCK%ad96s5~W?G{a>QXhFSw1MCTH>Svv3P z^$D)M3~5{-mz&m(L9(;#2Q>Fg68`$J)8>SnPI$>ihT!T0<`j{mD*7Ak!cXdH#U`IE z$T_Q-?B-iZ8}%fXK29zt>WfKv6GRN~6HK_Qjaw^j)V5#?s>Q52?NM<#^{H2p4}l^2 zL7?irEgz8KYI_6=D+C6Jlr?fSn#;?*sq^t4i3$(7zouxAh;2L9zIg1A{D}4@%%^8J`PAw5 zVqX9OoI91r1AI-ge0)1ntuoVg>Lms-HO0tOc1l+jS+Ab>oyyJk zM6J?%)?F9;J!ev$kH0JTXJxuCA}cnODm@z_C0R@;<;61o-aq=3jWhLjVup(7Ue}s z&lyVC!ICX2!%!ZxDkbtTg^2>mFdrVHQ~lRKHtc^4WK+A;ErE2W(4awNMtsit#f8iG zd_x;lF_K2!-ri_!!P>_>NtG3=4N@1K1*tn*Y6m0#m{*EjmKGPUto6k?W}#b_`r6v! zmz0#?m&as<*9cH$kMb(LUKbXq2}Jh z+&J$+R2{UEy2`Hy>6dz)0q-U#izzA+lbnsob?kUHl9Hq6BT}va`-0CDJt5L%jmZ}2 zzk$qXaG$d%jugYA*0L;uMQn1W%w3Ulc5VY(e`&la9FXgC4)qrJI&6gh++KjBG->e{gZdC5<^iy z#doFaO>uFlkYjf~f4NgFJ^FYJL>~fa%Y^r#_Eo+pwV*~@`l}CfTdiPcZAxt)+UcJG zFSR?*x$)(bvx8A8bVIr8 zH2ZVKoiXU2xD*jY)Ryz!U#84GbN#X%MNTCr&Ua1n%_nZx4QpHD0V^iJuFm9|+in$q z^9;96k&JovGvEUlZ1`c;2Q*&^G)~PE|C1r1NR{wASfhfg+`(Fp6##HbPA$pN&T*NE z!!^4Tt`ZZbnfV*`;1XP6qs=|dnkc$?&ba^q;;hIBpV^v{*i|6r=|1_4 z(G{8^OBy+@hqt-T1#io$IBK)uiIT#v`c zP`K~En`T4Lx39@cC`2Xn{d%qDXO<4J-D-geyw`3^OT=@xiJs4KqUdEK{;3~@N<+O+ z(WV7I(dO-U0v2>rWe+=LE2!*p(At6L;;rG`r2TzjF^-GeL&V~h^W7;bZ!v_P;ARwu z32a#--Bb@Xea}IDylyWO?w@HV_0T2zgxQ6Hu^2D?PT=n#>NI}JBsi%U+SWt()G1tJ(Ie%grlCc z^Um|(49Ujjw{OishHIX8De~hWn=|i6s=E5?Za!mumhTK|fMwIGh~-JpkI1rav@dfa z;Fxgau@nCqEkH&gY^D<5W`|#@k6`3kMAkumY@e7|Pjkv@#}d~Rb-=Gge_YJcxP^O= zNJRFy9GZoa`|87I8OfMjmuka+eM+L#vNulXST4>a)N*2B{;x^nzWEK@iMzV-7@zq5OV6YM>cRI+=&35I4yM7cU5|uan{} z<;h+>@?n?)TQhS;SJG-OPbDrTPDKxs`zg?Okd(;R#mn(judZIDaMnpxgnD^W)Ev$Q zt|zSbc-XsGZP2{y*^R40!41gRHjX`vLFpMg?qycF;+rtuzplfyfPJ0gOs97>q5KuB zOWb8WSS5Y4_F~~__RW-!=TWE>F?*0m6{*IrhJAURGXsQqw>5w zm;7-?CjJIiSCdOU1kmHQ4PFn{yH6Ro=9HDG^*^A}j&yWBS0!F#j{_p`qX(5Y;P~R zaIzr^adgD+J!7Ts^z>cH*rErujUl9?4yBHTST;6}+tga9C0!0XZaQe}vM+OTxUcLV z?^m^XK3PTecjQ;6tGrmAC{l8KS&U|(WfBFI)J9e}g+>llSPl{43}2svvhM z^rB=$_4}$2w~+rx085>(hlj3y1uFV;nTIqm`WG_SLt6RY$lL_Bpo|W!+T4%#i<*0- zl#P6-J2s0_mm9@ey*P0NyqWeNN2{S8VUgA1O6*E^MLVir|4xkUJ?((0Ks2z%(_a<>!aQmu%q=l zQDZIivVhmEb;S_zm9NyBZ}jxi*0?3Nd&GxVL|!ZaYsLoNTBH!XF464@JcX9Oy;prt zOAO_9(l7dG{y@vw9)gBAS(ad&+?C;Z$Gf5d2TA}u*t0-9HTZ4^*ee)xfJC{R zR#>^FpQh_w>ocsHi#6{|v1waxZEed5V~5xKWhpTRlG_~oDKE@VSlML$JNnesC3a<* zVRl;4`OmIv&_<=gzSeT$GxZ{^k%iWTnLKShaw;(SCl3F=%dm91%$}8osFr*0-s&|} z5Mi+wk}}mhow^xz+k4${+UWb}4;bgMPU+#76a-&q#9%&Y@_8K;%nzQ@x9^NV#$Tqn zbf_CY2ua6f6S^+A;Tl>6_DuO_c^)<%Z}WQNS1K3bYq<=QXVTBE1P}usgHC7ynm-A_ z`MS-3&EEHQ_ZPERN3tkBA>HG{>dMIArOO^t{?aZ9v5ClnZ^~TOTVKe^XrEMyz)x=#<2v>@2&8~rQXh0gh%WeTPg}vbWW8ZnEpfkuV*(5=)bpD| z*u6N5nRiF-1P9gD9j#V-4OauO$-a`nL!3TxU0N+LB|%|-32poI#cNEqV5ch~OTzvU z-ocy6zCFM({S!pO)utk{N#+OQ<`Ms2eV#JW|A90S4q-F`yr!hn)7EZ7b;+>5o3%OE zI%AWPl8U(d(UQhnWe zE*P}7eyb0^*q7{TuFcQa)7Ll+q>1el(tYZV!m%#jF5b!rOo!KfcU+{jwCLqO3czh} zE_WZZ*juQirp=(W%&!?u5z=^riOvf8O(rKqhSs3T4so}70?CEcNeM8uS+m3mJ8(!A zJ3bcJ?*X{wz5wU=#rjpd<=6cB&x8C?tR`B)wt@2_C*bk<`Gx7gWQo8<QFn!xgl$V5(WQ=+2{xsk3I+bls}md@qquWEmBJ~y_s#N{sG?j6yG zso)#a)>pjFcA=34OiX(>sO*zZh1EG;KmC=Tk5wG$5B>zCIdMiftxMa@$~6J%&YM2x zW?`J&0FaS67s#R1(~yjO_sD!JFQD<|`I($bJvUC6AYPr2RQS`OV6=koG*Kni^9Al) zgHD2c{q+M&qEmf-;zq0K5FCStL4GqonGTcpJS7AA5m8ag{Ru@Fj~0bewJd0YYGRm` zbL^kLc@QKqn%qoOuWM@;Y94nSvAJA)`3j9)wdZH1`i9Y^YXYhd4#xQY)PWj+Pq+zE zjaWVg{fMINW_%P}5(p^yKGzF55+Ba6;H!F%!{oXG2%>+f+Wl$p)-9DESo+@)W&6>3*xC2shmzT&eR?l?rz%2zG%4D98@9~EaVQtIc7vegeJrN7BO)E_*zbtuUY?&pn zm~T6K?z)B_nX7ma!^8bKL2gD?nVlQnZ~uLFH!JUdBP5n5RY6H4UQ>wCIe7!I zlLkIMkF|3Dm0wGtRi@vbG6toN>`Po9^VCHHx3Jh%HcPK?%*u|iw&kGp6Q}^OHwe18 zr;e)0;=bXBnZ{KMkief0SsNMgO3cjiOU-c*EJ7i{1OrzJd^jB&5lvsXflmAj(Yr6wnQrymsYuA zwPGUrP+#9xu}uM+VR2_v#suATYHI4aFNkE_16d)`cO+_{r)Ovp0M%5EFVl3D*v6gv zJeKSgWFYR6jvyHF-Fi}B_+vM>$XZNRG|>BgUTA@YCRDLMNBq^A2Hxr~?rhoG&v0Ww zvtoc(kX%m1Sfx8bPswyBA^l4RhqyG@nSV*i&?RorIi=8gU01?4i^!D|eh3G`p|$JP z9C7dwU$L^C78~J|)}(shntF}|t;gqVX8(P#`|EUK3#-z8PVF+XzqRXh+S@L_!|n^M zxUZb+^2|e>o?Mr{a}fAa)_YtuCIRG=kO^De?BNs9QrH}kIdi!j)zyiE86Iv>cb@WZ zd7xtXuOkmFY~~L8j^Ff2^T4h5_NFT_s+sSIA3w{n=}THRltZ%dYiZ{qnzXu%yw**; zZU`TVc8hkirxk_EyR5`tS&x<_#Qd}-CGZf4h#Q?g(V@HVJqgI*; z|M4pR#Bc0#&z#T1=Q~htp6kg|PGYl{rjc>OI&?quESREu*h9#mPZfng+@;MZ?!WB? z*Dm{rC;At(W4|Ae#ehQuJa9~_F`2}GWsOe zU!fN%SB_h764+RGu79M?^4b!c858M88LVWXLYIGCOj(_tW>CDwVH%|S{KsXr{FPV1 z%1U<_s)hd`{MFiEYvNkxKhOKQN5cPHSh9?7mjnPP`7Fwlmn9!sTn`O_1O)xAt0Fek zSE6U#CJD=v2M2aAY7yrb*q<$;JvzCOp?(I+JDUxlomB)TW`G+!s+CsK^*|spJJCO! z^iA+iX~?aWNF4e16cbsOCoPWap=hNT%(Da}1QU8?In{aO|3oOt?H^^(ACdkw2ld*9 zhKVhXtst-Ui~6jUr}~!iTF~IOsX39x=ZDgD6bYZPd7G|&)bZc?%byhE3Y{S{5_eQb zPv6jKeTkB{Lf0x(;fRYx@`by1;wyHc*A!KLkZX0USMb(_udMv&%3xGnMWacUQD1Cm z^u#)Y;6NhvBr-ZCx%Da<{&1)}0Dn-suFLiO@w}w-*eBDn0u)U5x%>F|o7OSeIyAZM z@sA98J7I08{-`ZOSx-%SrH-cGi>f&ZGMx6>A&Ip(L+33zdt;D;WhtIPrFel-6B#5* z4H|`^5c_m%eh@Fuxc>y|-<5~TPoUD@&X7S`C7XX5l}>PkKvzhzYb4tR-_39@_a^@) zc|cZrTn3qaBq?S#Jc>TaZSl+4ySm$h#-A+;%IN$1C0jc?3KJ!!R~@gs)(4C_dyn{R zLK1KmOFGMDm7tcx)BdUL2QQ!cD4o(GZBBu*wAn9V(jM*rZ%kV@G0nkq?4LF4fSS4) zDq|_)R0K?p@@mahd9KcCE)kVWg^bZA;4@7fyDbr8EF_aDKfw9HYfM8&mE%Kbh3#b# z;Br62CuTc+8AkIf_eHsp@kh;6$Kl$gAok)bg2a8SJryd-;^m$U@YccS+y-l9>qEAyizn??pc2S5$ciQy{Y_US1K(iiELyaH8U+Z;p`SEB7F^!~Xpul?n^8758 z>4o(|kfb~5+;Dxrb*7W4gQdED-1QOmrnF?I8e-5m@Wd=FD;7AsoV zli+mg35TGMJ6$C+YeltSKFA_qXJX+M5Z=`kXeLR%$8alZyC=k2W^?NlAPU9Co;)bAcib@GRctwtAHZqo4eb;$`q35m$o{|6PR*c9_Q*z{E;0L?jX?vqs# zW!>*lRMb_4s(dldF}yrNS#wtAwgr)u5Z2guUT(^zINyu>IyduYJ%00n3EbJ;j>o5m zzO1?xp$`jOe}tyIv>6ng;)hsZ4hV)nmAhBhI2TKONu|a%e*<=t*kqnm25D5;Pq3@8 zm*-GC{dss>TGulBU1RYNldF5B>k;}BnP}JmcdgKd;6oDTN|xYLg@5tO4DJ8dxK{(n zOh`zGr#&FY1vUdx3A7Dln=l#?)s2YVQe7H84D-6^M&x`5`VtX$R@~-xC)FzW6Vw?F zJQD$X&(_$2jZosLb*A459L-7CM232L!d zE}uV>pmwXeP2Zb#h3-a~v=kOvg|Ig^ALO*~EvNpta=H8xhQ|R9+zdtG>u6oPEv`~l z|1?!Gl^@>S>94KOzC5-_cEHrPxJqtUd>GDlGFn7KLER$~6^h!bdh#UvJ?0}{KNC7( z34+zDo(k}pR_liNZK`Km0_3k7{j)xdLR*H-Dc#n0KZafp4i=-8qy+)qmrR(c%w3|! z<>OYx@|MEdM#nsM+HQ(=75cJ6FKXMx`-ob%J>U5`4+{?Cw+vsy>tSc8T<6|S8@?zF ziE%@cp`IwA3CxA+S$C0cVOxnO#y2faXH33I=6Z;J8twBywTr@x*hACb8a?_j+I?kR9`p*}eKi`$Y2P>FQU!AUgAo!{KUTstYfwZjDx7oJJh5cOqX-U`o zQIowVV3wfmEt%gRa`7T9)Ca-R)NiF)*UrAG4(>6JcWzbNS8Qd84#%TdJuFirGxJdj zP;{OW->{QzQpLT9YO>CobU~@s+1h=}HNvFT^ad#bh11N0Td7RWT&)-R5uAS(9b$Zi zb^hjH%M6ZWDlZjfJu*EU;P{d>ik`3J`|f=zE>5Zts$qz3R^vZG)}+T~7fDCWUfA}G z?=h9C4(9emZeaYNCYt$i(_hundVB<~+uoo)%0oP2ufNyvAW^49ePP_4(zPA*0B=M& z!#8?p&dS#xASg-fKh^(!6%MVqBNmE}VI3cqahoT#&awG`GH~b3rHtjnuKUe*JOd}S zW%rXXtJHq|{W~41XJ1d-+?;ltap;31SS2d`aDVutu{mMIWOYH(zP(}X z{j!UA;rEs1ql*@z_MvHF#gNX4X_^{7@xEZOtIhHAQpE9$`qh7x1o8h}5Evcw0B7L}FdjW%G(B2L=Cqu>E;ZWPF{ zF5}uzVK(!3^-NJ#tsMgE{|p>dBBa9d)?XEum4ra}z|Fw!Kc{@)ogdt;h^Cwzm zhQ!8vE+iKD5J17=)h|)g-I@|p+J1t+C|`F^M@PC`&zw-_G!R43i!*mqMmJLK>)3}e zfXq+m<=%XN-@yBOAZ4jW7D*&#n&I3gO*eFQvP7e_B-bE&J}=Ll9gb#~aWf2Cxgekw z568T{?97u9Sdv8b$jgPm9sEd&MhIMW4#@X*acgF7E*)(dsBn3C`DAUtfft-fBMNY& z?3({${@qY-P2zI(uKpCI;v>02!crMbTpGxirv3esN^t4E{G3lovHIfb8p3%IBwy}t zfe*11-HEYZ`94251Hj0I%USH54c@Q0@g)zhhzk;ZVaw9f(?hL%=VM2zy@=CLEBTa@ zE)g7<_iEKtWpxdPyM|=&+waAY_$mJON8rw~#Eh=qUsZl<`@`rtN)SjvpzmO0k3l~k zWS1jeJYnz;e7+MEDfb_~5D7UQT?wu8oIU$IpfQ|J^OGe0)bM+PM#4#D(DU_o0x?0P z5&|_IOOCkj2@Jw{c_7p!A;1VkP6hT^u=_NM50NtY74aR$HnncwFAS3s!L4ip$s~Hp zGh`QcjhTp=;JgQkC=mHKM>r1I)$yNk#*w~UYJ?5E9{;72^gd8Oe0kc^cS$%4*RkLx zKB-V@&f!U8tr`EgVfKnRG6jvZDEBP$Rf9d@JDdJ0^AA#UB)_fkG(I$xq`6YXS-S)Z zqIEKpih26bD8Ha@w4006BUw+ieaPdb3sm?wH_78Cc8)L8EUXobHc{{z3la~bl($H+pVk8tuxK8GwrYr zJ#76ckI#)~auKN9h-RvAU%IhorBn8iPDNw(@m#It+j;$qm#my!eaW>09@RmtS#Bhc z+p-VPbg^TM?kVush9wQ(t0i3UNK;lsL`0wAgp}`bvbj07{~k7P^I7bkz3aR)#l`;Y zl%Mal&tBEAc%S!Lx;P4qS8N+O-MQ!ilB~gB_Pt&7!Q31!`SJ34Zh9gJhu%w)fTjtZ znK_A1QuvMaaigXEP)@#Z7$+2wBhwm=5wKRZ`E8rVQRu?==<%A>ZLcUR*eD%dPZ71c zAq`G8xN!LF1_&`D=GEs~fgjSmWNZz`S@e*6MkgJ39kU(eTYvY;`gzi@>DaKmN~Buu zr(e~toEu|uok0;rlr|P{>&nM`s#}X<5ZQmG95f0I)kYUl-K772JM&&`9B@`};~Qq0 zWcct=4>Y@_yCE20=2YnfqNa^&QhE`)@>R;knauNYk32ixU(iMCoN@uE0HZfF)Q#Pw zG_^phudKwozrX*dp0&5<9o_3mQ|YjXMqI87Vio{p1=R-^udhRnVN$+l;`&Vv#FgVw zZ|i|tbD<`4hORagQxF%<%(M)Yt<;6boIxT*<%zZ@s`ST--|$d;&9jPHUFcdLU}j4y z(iCCcN4e>Udy}&uwVHq}5>3PxlP-K zoDAwut`I3L1)yyp<^aAM0s#S-mWP(4XpS~E`ZPZ%xi&ndCXp{h0g3pywBW>?s`hLL zkT?dbxg7&vL%5`)he*AdAxs*uc0qkGAw}P3sXqs}Oz1`v0Pph4(Z3$C;sPAq-rQgr z54|5A8XhuP>nEp*YnUpcyAq0zqIzm z8GS;HIbUMg$HHcDUw7C}ulTDP@3hFePR{-Gq#`bvD$7p|RwYHj{y9Iv6q(4GonJ)C z&5eCEeoVEv*>dIlyyLr)%V@#h+m41XCh!u8Su6Td59~LNvm5%Ns4*0DJO6rk(w{u^ zLzUe6yGyeH0{kwnm%f%gJm<`o>RT0_U-WC^A=#PBmOdS-8dk>wzpUw+^+UuV2*yLw zGx)FFJwsZJ;ks$5$$c`e;G0i+J(=&gw|YlLNZ7AFJ+;o`7|pnMcq(lZXzO>D{Wfdr zJuh!(|Fb2t8KO!3=k<|qiJfN@hmBIY)~Ctc`nXEs(0^LW7?i2*jy$I%_r(Id zIEebH2c4o6YT}c5VVc%cP9aT6t^DCborl%83;6rK)w~1&9$*v$_?+yI&In0+#|qQA zcw5?n7q9-BgN+*o@=8Bfxf(5r z<-_tw4*+}KspIqZT|M=%G}e<~lCbyt|xv0uB$qKNUsYFfLsz-j&}ihz=VfdLG90h*za(|0KBSOCr?DtKY{;}R1v zsQkS9eG08~}#gEc8a_rAjB3XbdGh$ znP9r9rM!gDv#ZBPQA)mor+Qwq9-+iasO2b)xsUY(bPSkZ`E4wXkMJ3ix0OtIXsJd1 z4`XK)5as$tdm4eEM0zNZP#PqM6cG_AMd^~RfuTEfkQz$5B~`jRgkk8E?nW4?k*@RY zyL0ybUz}U6nfH61^{n+rGk@=U>KTh^Ycy&j7k)}3GNpqj zKTy)tzWe^ZfDE_|`CkDC-|*T=(F}K=N{de7ws95y&$3p&D@H+5DndabM0 zo`J`=@rD>tIe$px|Q^vu~8kV-3vWW6Vf2lbkz&h${w1=`t5qU7IJlkx`rH z{rZu^hO1<7VdjHP`e+WN&1VALYvWRdTT_B+<(rA3+EY4OCcsz50NMTP(ThUbRZFQ= zeDIZ~>DD78_oh#8gDieCSvk-EWQ+f}zqVA-f5&d9t8}{3SieA28^^C770Y%?^SvnO z!kzY=zJYOn2nm6yKXxqr=hIRw_xXZ7kiyj$>$Z&qtdVQ3kR0z8;~k|l+zV1kUzM<6 z!bpYlpuPNa5jXw=khp7>qNxo(?I;OIPvl%xz}UBCOD3Q7rEdpxz`b7u8d_#hW2|VL zr_B|;SR^&zLHyPj2g=noL?Th~IP{Od-lHzj{xb@DB_FY|Q1l8j-Zwuy`DS@X`jc^K zyA$F!{syK629%d>ivkmOuNov|!%#8g>;2y&Z!?=m=a=x?b#tAkwbpf=5fbX_73zMb zL70Ry+*^86G7-A%<49V5vx%*trv%37WI&kd;~l5A?ZK_}-CvV6Rg(+O ze)kBTAvLLipglJ&w^!qG z;g**G#@X-YjjO`EnhcOs98&8ykSbEdqFQO;L6XG9tMx7VeQKEztgWq~^s%||2yM>r zi$?F=ZA09;O^>;`xr)F$W7~O*QHG=|gS|a)eiJSp(#+lS!8z}7Hbk2`SePone=u>e z1#u`Qsys0vP>^xhED*UoR&s zi%12okJ-4`;#B3>N2r@>&2R7Hh6?d7MGl=K)+}<3-3v58a*Xx5In~vQhTk1`ts4y~ zO=p94A72{ZA5Af{fPdBN;ht&ePrIn(c;T06R5MGIHju<)35yZ!sHP77yil0>c0c)B zR}$@r%9F(028(=tkDARYzi6|+7US|W1ch}RVj$NkJ0h@L0GN;roO49b`^Ip4dz*uc zz8}F8JbhY{~G%ypu03n%O1_4=2V7SH!J4I@2~xo3Xhvp=MSWd~%y# z-r97{h!nwKZUp?%c2{+O4!M`VE9u)mZ7-w4o49U*cHN}1nJl@U9_5CVLG`R#IKBAP z)YSSeEj`!tZ59SmJ*eVu5mA{a>U)9*DV3dAr!X*>!6A*)%3cE^dwzQbXaaI!F}D}f zW0Upx8d-8->DdsK#uj+v1&1VFhx*Z{*TkL&%MqPZkCt5T_~>^3pmb>Vc7{2+Cuhoo zFP}A_+-0LoOIhJ`_Y5uElFg>culq@KZY%cZ7%&%>`w=B zRqH{kcS)(v*jSQs!F%ONQcIKbZ)a`fZ)@aaPkz0vN+NAxc@g%s#D0zP1qLE6K0QO2 zhJECv@o(CSvBv+9C|R^Y5lBriyVv!_dF48=oeLf6J`7Gxr7tNfTRVk7Y0r~a%>qix zMZVZiTrs>Vh{y6pV-4}=gGz)zO@N!;z52)ZiH)VXAPgS-1)jn@zZM->7=wEZ7zp1AtzxDf%wr(*p@GAPhR?gM>rJt`J4`6nM2^}5(tj!Ir zu*z-UyJr;9@w*0Vw9FoCi9bWihy7xfUW$N=-e;-RHyIXBQ#{l2EupAjS;%eB&r#Z% z7qb{*=H%ZWUez(p!=wJiUQEU$(bFSji%vAdt8_+GDqI!7rpi`ciGs@%SmySK>ttghn&o--~JFIn+wt7SmeX>J= zlYOR04ffZC!E?lQb-pvAqw2mmi6dK@rHPt(()RQ-pB%nZ*X{kRe{A(7%=lZ#Wm=?L zpxk}i{;M4I#ZoODJKa*Ok!-1$zQ+4vPD|l!`vJdR*)M4QdMvQm5!C$N)WxE!aq%ax z`dgAVL5*C#U_LFz(&uj)!fc||44H~rLtMTaCiy=$Hd_1s+P>F}BD?cYs@cYO92(-{ zW(mVDifN4^Z@ZJd{;_=ZAWQa!_YY4;;Q7Wh#rTXB4>MqWh|Mo51-ikeZ@hNXO?u{D zFAn(v;At3)RKIo(X^vom$yHw(>?~( zX^&uAp1zOzc(yomBVnv@bjw^W6hNyvSBd2CZ`@gHzwV zZ<{A8A1Vi47jc*Iua-4Kdu|z_NX2EJ?rTF+aTon%N&V#d_ErAt?)gBRJCxjoTwGl| zH-Vo;2da`J;e0yq-2L_N$ogP6&ijpy&m4P~>C2bc>KTnei-f1ExN=vrU*t}_TVBe! z3V%^NTe_kFsKZ!*{~~5($SFXibp8E5GC)FhIU9oXP8I8H(bHs|^^*z)>;IM{0D)q1 zPm5UoUxy9tK+8`!h770`(v&Y_uLuqSV=&LC5oZE?yXhY`HQ+iBdMfd;(R;mNaFiZ$ zE$n8%o4Co)RSWSz%iG(h2c>}6t6X7?+VOMrUeu;jh)%tytRr-D%>)R9g;H4|A7AOO zFopd1$AcpRrVb8YiSkc2Z*Ot0@RlsjVP5BNa4#)q;#QTd*630|$RjZamtd$zs0W_& z@HWZL@83W|h4x;bk^JZIViWh*WyW+YiERl2D6T@FXL|F zlPv#T+XQom+QBG+1Qbz(=%$jjC)c?56WT(G9Jt?ju)()H`AuIYlKKNEvW#z$Tk|Bq zn@P0ayq-CN#Ptc~hl9HJgC!R!hQ#V4+h_3ugV^n zW^6RY+7WDWI?_J`p|dE`-IV&v>wQFOEjTMO z&Lw>D3>Qmlk+vJC24u@N``9cs?8&BvXM~`ja_UuGdW(4iZlRs&s}o`OmKVWpf*t`6 ztob#*?#?%uO$XGa!41j==J?*r9QMxILg<;?Ru;@+>xzoPj%#$L^dELgnnn&tKol`P zKSWAj!`hwqTfp6>_1ov8p|D^p#vSfkHr#F5@h=dSOSg$SY&6;lbtIxQR$t0ezvxb7 z+;m;sU0k(0PQj~9Z)3eOliM(9%L^q+zr3uds|a~(K9FLp-;?E-UQt?3wd{KdN}7)R z;)Aw%?MSi{1A?JCo7_C(%K-@7jJcVu;VSU=n*iM7xIQ5y`7ZiC0Mqrynl8bZ` z@~(wG@{0{=3XThxtjXsEnPkcvDAO`~Ig`Jz$<{5Dkxf?l;OLF*6wqY^gijN3Ie;F6 zo@*CMg`aliWoHi;)s@{=ai0lhmg zJk94DW+4-O80Ff*e}eNBd3)5o*1E;f1h zBt2I4Gj91Lx(R?4rt4PV+AAv)#(#Un$;pXyOhe`{K^}=54ju{zTFb|o>z7VhEPyN+ zlDBz%e43G&NsHOD$durBZ6i4!gj3^vhss8#6@8_}4QWlRXFMkg)$WJADtNNLu;76X z85;fP#Mof!y1dZDVu^q8UOB>efzVLVpyvqcJN;zT(4RwJ&?}VYle5*`Nv+o!YU{(0gV-ke z2wB(t?-KIuY1KS|Tia`%(DLePDnNRn3Ca$3g?o$qmIg+F*bggLzffO7U#miz2*Wf> zrVj@J9uUp02G>c!$|o-r)1MuukvH-Ky1M#`1=>ocpVoE_OGOqPwHyX)9@m&_*fS=* zk9aw)IwSfkbZrMbWy@DnSI2HzEOn?^@@q#jLcp?~sgwf8uiA&en)z~o?wLNhJ&(Oa;1;j!wS9Zk1=3RbG;U;dV+WLE zMN`=zNcx3mM@_FmpFGw@Vyi~k=N3d3P3|B=@SO*+{?@^Yzf%Ak%NmbdWZn_>5&!o!1JPpyIVqF-;oU_9%|OG zA0x8+RTlXmaZ~Yei_@=f11%?ycILAEcdL81T+R#HuXxS@a8#Iewl~4sr&BDx^SHp} z8^9kZBUgPV3KjJ4l8?H&uc)S34niyclk#h2{vQ-(9c@r%k26P?GbnHByJ&{2J8^AV zu-9yBs!>e6<2-ywQBhZbhSb5PI@0Nl{Bw9LeaV1@NF1X*t0rG$@Xl*a-0ZSRe_9o| zBRh!!B!v7=gW{Md1fJ^%aj7OV(?xr|2_6rPip8bLSoagYi(6CJfM zHTA?*90K9Pa;qj0!|gIRH$V0ssN>__z!Wk^DDWpM$|lF`B3S3R2^8?sA{8BmsOSE^%^kPS5o%<@jLWdN;3R;#|sqm zIL+ogqq8@*+UN)BSuz3{j>BYHIhCyk0HoWq&djki3i$?~M2TEP$+Yo)&YA&^Q(eu* zs%FbElr+kqQ5eLiW4BxRcdSHF z1i^}9z?Xi|71WFtjBWH40|VXJ2Txg0-1H-7@{E zZ>g-ld+*a*pAmx;inigmX@uYKDub55hHD!GE}j_m2!0LTeG@dN`v3ncMDmmn5ca zhF$#)D}12BP;~&`aqKg#)kb!a%LUiBuJcKVw}uGFdcQYCj}}S4+FbDgIZa!V<~t

?P9Dg%3?Al!s?lqLip#XaWD@p#4BZIRgc;HP@HtJ&bA^V?|EXdQMH7bEH z2|qv~1PayZXlQ7<@h<_mYfOm(L7VwXqn_Wr&1fgTJh013tqahauE%wxA3lv=o_MbB zCzv}-A&x&+(8Ln(cYf#3@2-6#$Y_#AIHM2Dk|>XAf{rO$;nOr7i`@$GOl~^P`SrTS z2aQ0MIfMCsDCbHYFrruY-V(u>w=0k665Qw7B{Pc!l$J!exdzH&#`P)hmhW!v0CXWk znIXNBaH7x^oszY?-J_9ExsN@CXk$QbG* zxkpFx<60zbnnIyud3o!{nZZ+ldEkpG2f7KPj?IQYY{5;LBq5wUz@h!b62<|2TyqaS zaL5KBFE1_a^Ork}bG~1}hx-)OQN6f9Hpp6oJW&QR2Ngscwp=Tt+ht`?;O9MTY^)b| z*$6sIFp2@r|6_|2|MbPw{rJH{`FHza`Vf6=TE9@xK!r8M$8ySa&5Y1l?=P7j_fFE} zd(fPYO~d}DNIf5CQ>=MU2dbRoH6)2PRQ*FID-XmJW8>?4)$Koh<_C+lYDgQPX=NX95wKe7b#v=z8G-FhC~K;V%x?5@xy&wRPEeA{bo4td@B9jhmV}QvWC8d z*|KI5T0&kS-{lk5v9j-vJWz`~qfM(OZk8+Wdga+MT&HC~1ly=XQ}CZ`k59s)hRPl$ z9(?s)9wZoa{^8!G0U_JPhKOH%5&sgJCHb_Xr6mf;8BC-(C9nza^kYo&y#ashGZT|` zaEmGHd4J-7^Pc;6aT~P%*j)oQ><9Td3GO(X`7s-GrXANwwt)nN$V4Rw7=LsOZz#=K@O4&cBu#XFKLuQk}L=S5K1yIKaU^B;{;n9g`B zEGhmXvVQ}nrt|(iFI;Bl>$oOr-?S?POOT*gpLUf3_9W_21e-ow1%>Jv?H~P9s79cx z&Iv#m9WZ;1Cnp~^ywIO{_I7rtgH7odN~Vn-cgdGhVcR0)hX@?%9>ykG0$+Gmb*{MB zE7CLdrX&^X?oV?KH%%#YEVMSs*dJ)>Q620t!F#4VX~B9lUCotmh3dN~YGA~5q!h2p zErdBvjwAQX#3zT+#Z_BD2EwlgVQUTDf(O7bzece*I~nX#ibzd;kKJodH*>c4%Y!D> zp^4~09Y;V<++1I}PGy4xOD*+_O|=_oPc(X< ztPeyW>^!)>w#SGl&Xte#%Vg_slU=tM!t{);9v*U}PBb{-91s_`2+`mQuroyRKvx+3 zmxBh)S>u)^r;hwEU|3Wm^j20%jG^-{eyR!c{NxXSTh=hzJ8|AhzZ=cucyR?tEtP=7 z8bnG)WcoyMJHPno0dXogX}jp~`mMkRZ+CpmkG&LQW5r6t7ZE)Zi}BrALw|Hr44yAS zk)KkPUi13s=9!&8CaitVKm#FjeQooSnn47D-*r0nOmTnz_w(?}2^IUoqUw*~v-po@ z`;q;!xS{j!@d^#VB1Dd+3z_s&GX8LrcSQWx~I{4erR) zVM@5^7&7%ThsnH4@qv5(LLO_HncnLEX%*^&k z{aAQdDIz+t+T-4=PF11qsp@WmwH6x6)NpYK^+P9^C|LV48?`!mZH03=ZwF@rKdzo^ z|0^Zf_Nm2$PCK2bQ66J>_AYUIE_ z%fMOp$)&2vIruyEdeBYjt*kx@a=6!i`r~}iS~i+1%TNFEOn=hdEvw5#0Y=m`#vDuB+oo zvw+A;#%WaSLq#u;T+tg4*43dQIU1YKxx`VKtmJ*yZ%G0O6%HYzO~Rt@URh*rt^x@3 zx2v=n=Px2AMj5M`KCQj6dHUzh+u+L z_@g{vRaSJ@;{}vJ?(GR#f1{y7Mge^Xn2t_f!>OKbr%F|**|M5$VUx{;9y`w426XXL z!NA}L)+~B-uxtq2Jd-L4ut#jl^-IIq1ySxc6)X_XKhHHx%DZ89cEovk=5IL5y1F~< z04monX)zD@X5-q`AdX8bIA&?d*v;MjYxT&>q@(d#r*rdaMc2JYH5Hwf zVR0mX2}+nk;ak3FV@G*=l+fl(oD@NUxqRBHn#W$Nv@9O4rnf+(VM9mryLR*s@gb*gZ>Yr4A-L#; zU+SAi!G|NSOHAElI?`x=s%n`~)kSq$>5Bd>R@caIHkwwRJ*_k>XyKb0ZL|Em&sE`$ z!Jo|WAV=F5bTfr3Xwe&Oi!?7L(aClLAcRDx6Ohuw`6LPJAqPJP(#WnCRa zib#(<-lk37T#$5M%imBR-MHl?nNr=C519bRsYaRlvj;REr0|zZ(K$bxU zC@?B7*$!j&dC9tRu8GyGe3uvaH|Fr>Aduy5*W`w0R=ix&kTTmU*6^>HlJ|#PGfn7e zY(<~8;i(1*V84>Rs}(TcKIJPOH_pz-aZIXMt@w5WR2e#k)vpz?_~DW{?I{j>2#M{d zgkse&D_{mGe1Zc!GGu%SP%*6^eTEWyinzE0A>yHcM%~XGfMW7o+cf90SoY#Ol10fe zuE`Y7Ug-b7VWp-_qG4@_Wl^)J19z{}4-8o{+e*5WJq~BHwK+_-R!tKSeMfjFO zt6{K#xpTrX3wGV9W=Zqoo=FZ~8kvmDvaf?=zg6Z3ziD`bFP(8di&Sxp2&I6u{YWBD zuqG#o=Zc*!v>DQU<^`8zLkm(>Ko)OaI^Qa-xZt`WC@kUWrM+>IQQ$$!Znr@0(PFAi zZMxiInBK)}^9RYsS=(O2ydsK*$yAN*SLhP(3+PA^TN%Dr;i-QL=WMiuEk^2CEAMtH zUh+%ngYR{rHS1q%SIiHl)PAuLdhnCR%$4cb^*YUy78Q^OTdOcBXcl-FHDDrpJsdx& zVfs3&_e6v1bf)*bP76rrvU%7N?7L1&=P7ptgE;drmU5kf1z%e0ZFy zwcs)T=e#rU*0lx%0zA>4EV4&Cu#(p$z(RDdssd>>)N=Tf=v6#~Mp&DShLR@Jz3t zP|FxTZBgj0hZE>zk%fI;kDi5J?FTXJx&NCDcEb5@E2H-J_No#BLB{3+W|_;DAkRKS^^PqJE&s|ez%Cmd zrI7YWOhh)rU9(E89c)^5B%r;sDhS>G$aVzR|2-U57^6CZM`-17ZKI zx@Yj9-w=q3(|n!Lid{fbW>mRx>#G>XAP?NnwfMTmdin}FI?0AWYAU@!Lx7JD5F(6$ zUXKI3kLshX`SNw3A>n(2!tQ z)h8&7iXCv2xKCI-ErVUq2;)cHzzC(zYeBQ6*X9S({f{ z`>w;Ot*x#uf@GV&*L8u50O;m%lk%voD@!-{JTDU33&L`3p|e@8u_Yl8Cw&QVAaAK^ zc~Zd*dBsS(St31h;`GZ>esQLtiM|F(P=0X{9IRmeL^XQa&b>eKVXF@E6C`_N3|yvk zXvgZWS|oxq0bp<50L9QU7myM55$uu1W#>bGci5n}132bwk-ExKH^vm)Gy7QbLH*`b zl82kS=eSeKTen(JSXq6(1A50%sh^Bu=5Yqt2x*te%rD_g=Xq)bbD}v+ZCcY6>LvGj zV4#uVM^)_I@oNF6PIFk8xz&8YYVfsT!D+%vuFQMS5vE6+T#gla&Nim!Gp+QbFQz5( z2`L>0%uRE2roLE;%G!#La}pnNA53^q`VzYA9M}Z!O~&%f6swQ?9IjzoY^2UzdnO=Gn6GlAYbNSc__gjQlv0z8dNPFs{D zC`-9^(6)(1wm%U`}_nCRd`sA^)Yz>SnS@&+myMMrV=c+3EzT0~tR$ zR(i<|6rsci@guq(>w9w%x~uX%VK(jc&V0`tj>C;w!noV5t!ZHKb9D~na<}I7>s$v? zA(MS(pucXp1+nrwiK}7mc;Q$`7p>juJW=`szJEZT6C)YI!WKk_?YblkuZgfn)!WC@(^9uaO&c z8N5peWE0cPJEkvYwfpaT?mS})%gr77E5jE1MA3gy#;E6L7K7ikB}G3d*1N*-hC?XA+&6U1^BHo(Yz;lnDKgl$g$)hf0%Fh|2MrSaegk9|sJD)QH_e2M-D zcHY*Rcjl@J9|}+?WXTrn`N#JTm>PGfX?ry0T^n&1LR91Fd0rENIQ(+mEej zX_70vZK#ALm03r@p+ye{p7ARk{lyog(WT!K4E8I$pxsI}N<@mLw_m|aqY0T4DOXal zM|ywPukJIcDS_&GhU>+|7W_(kIrLe_Mh!{v@Cr2$Fh!4sm@f%yx|PHO`uWM%oXQ06R!~@oUS_J|*HS{8y+VR>Q^nM| z@?66oD;u*Ubm@AEeQK*6w~SgvjrYFq#f!+|FT1|^3qmG;$c`1leW#BJt9K{>LpNVO zM;vwh{Q1ys@=23av&Cxw=U)Z?QTx4nH__X?LpRJd_LwK9&GIq-JfYO1<+h8_v%}T1 zR#K-x%+PtaXUvDpwa&_uRPR&JUuG8^y(l8znU0U^q4(-KHr)H`Nz8c~)~Cc;Hp^~o zhU!gy+CuzBSf?UH*Na(v)}$Se`yyD~MZ{#$M`JMo51Kvx#N#rd5x42>7wKLHzRNse z;^(6;K(_(AZ?ofDCE=^d$;q&_ZlVRBz0!Y-ao~*4XG(gEIK<7Qu?rE$p``$+19|7@(JgQOCrA!*(IM=A@<1_ zOrY!<{w6MSx!IK2SLh;J@+Uz1qe_mOY)}{3OuB7Zm@SJJdVV>nvNg#o+I4WIbqQQj z!FV79F8%*G0LZbzgQ_5Bp%%QC?K9 z^Gf@NXmGgR0=b9zlCuGs7H`?=LL&xe4mw*!UFyiUCN=m6RraeguWU$@1vi?qWbklt zW=6uw%BuVEoCKToBhDV7Ijm4aSr1+6Gy8cV{-!prX;SU1&*1XL#YgXTbh@LH)?56q z#2R}}HiY%oxrbs!Cr8L?Jn(`sCzF%zFon#F42qly2@6GV-h}P?_~4(OvP#}PhD*sW zxteY?!bPV8X$u|%&Z|FipC! zv#H~5rOkB53dj5?KuSoXV3rjyi;XI*%vlHikhBCG`LeRI2zD>@bEY#6X0GW@*eXo; z+HX74GeV>+xa_e>f=X(nPi+*!FS6l|Kluqj4nw6<39L1v3yr>iE#e6Gi+8DK_~6p^ zq%rwr#gFR9ATu2<>}=GtY`!cW4^24sC_Qwm;!wYeiR46|9+^n(R(F=iQ8P9>)cz!z&&8dkX9x~Y*W~AVpIJDzeDp9ANu4t$Ta-}W%04qQY zXASM_qdSj(Da~L*G$xqYvwF8;pGCA;JK)+?|2QI>OFr*V%)kW=l<^9v?x)oLUOC>R ze66O|nRk)k$SL*LbggT6k+O<+;OTR*Oa-Zubv4iBkgR9-$^9_^X$(elwot{$#btdo z(?_Y|i@k`HmQooh*fHBJ_fiNuCYHZ=CgdDX55w2ceP?LM{lTm>gmB3Xi|gBPGQ3Eu zuRbr03AM3O*3E>iLUJq~#gCr60bYSO+!+FjJf zH}mvHs%@D59grw|7JnKBzTf5-cLY{S}#;x;WGH zD}od@j%J-5rD*5D{aS=2`v9U{fptaLMB~w$o|6e;d>_R5Q(X7CD0a@l=1tZ(mkNN} zS!y6gF17ej653NR>%b{ zk_t!%CrylhL8DGxK~X(b6U~r5p23PW<-5rQ-Tjjs-yo(%gFEz zFM^s~OC;bKBIuXH7)w3Mnfd&RRZg;S`j}8A7jh=4M;+Gajv?2F;WV1y$2vUkXbr}_ zAGzvf$e!YCng{VNLe;q9M)C#Q-t=gP_qhptoi{_>!NI?F^k zWHIb_`MO#KR^QiAo ziB#*(*0LC-*L`qVd&qn{u4Q4fcre!(?!p`T;oC#eBQ`Q)$YXsf(P=}H2Q0H5EpMeLUQH@$z!it33zO~)C=%x-0k0CYqxfYKr0dKIoSpU zE*`yLu1QmN3-JkxwT%q5&2Hp0Mh2JU?Ufji|D_K0Ah`VNz`~Z0>ejjoBz>%%H+S#x zM;qSvG@K!l2!9m++hN0yu@fi`Ctb%I7&XX^&6AxHl=8vXDKmshP#G+SS6pqO8i z0Cxo;s2Y{JOfw5RKbp~2D$^D#JoU8{PN7UxjK-+FbO^>m z?CUnAq}9Au`~C6q258%sH$lZy3RBaC5_}27g8E}PSO>u^gDBX<9~>GPU!XHh9+*wS zE}{y53bvC=73oUMilsr!tk8dKp7t8fI}hW<0YiEB+m9q#OB^$2}; zCZ=EIHwI$tGXB_&leUlcsIis4DO3Jpda0%Yu{_ihh`mrLypO{d_LRt&E;@DkM+y5f zRz13hUxeY!MSjD3LJ;B-{m5bE-3$E$71gyK%(P_zxmsuz>0_6kJF#2NHTu?$v^)zdppS{+bAX8C0zUcmVN8{c;bV=yStEo1ijm< z>_B36htCjWbS?htn*VX(ozi%IKOrZDsXN?F2SJ6KAB|K1UVL8Ue z4ag!V>Ru6)^8^I_6-Aay3TV@B&XV%-b#s<0I%Z$A5HBxp$5-&HzIM=xk}nm*l^JMC zL{P4+>fYHJy*53khE{XHed>0&vT=Sk3q<2_|q&SMErlF4fX(f%tP*exe*SBJkx{D4}?EUI$|vwcguPu7KZ_b31r zM>&uw=*F?P|9jB5jQd}M1}GTG%GBkK6B2*w&A02vz@Q6B<}>iqm1zHBYNuQBr#+&5GnZ7sIz zKj}f=5Vi9jq4n^0uWE+9?e zI+64r;zLKG1;&Rh0X6p-;WCm%vszb6z6Z$tMPIypG8|l6u8c=-ewCBZ3aq4BXLkZX zc|mcJC!?~cr|D`B6Y8{%6-%^nwGB;JZP&x>#pkNcu^zNxt<+stK1?=qja=`haLX*C zo}M0Eo@L+56Gtn|=_^Tf@{ftx1#pkm=lNNinz-~8Uf=VFS<%W%E%fGkseApwUxgbu zEv`$SQV4LseAIQ{ykF7ucxzQ5`+Z>5<34H?bOuM=uFu3Yn7M=-(sb?iK!a5%AzNpzwq)Ut}(KwqJSPsrR7P6Wc`pP=QGT zV32F@j|;59o3mws5e#SmH|(9}`CWSsZUdt2kTj!jC&bcnBH% zY}47cm#@q7oQ3SjE}I+wim_4avY5MvH{%Za zVsAidArzIPMo{qQ-k9jqeS$CcD=iHz!S6}0z|Bc>)4|KK*Fo=b(hwSui9o|n-qQdjtilsTMZ6a=L7=v={XMedCpkaDLPZr9Jc^p zd8hAhhrWRb|C%%nRMzEct8w*M7@3Rx=7zPQZmBn5ftL_hJZvoO{z~ffyO!bZ0%?k5 zx1?=xK|h$c_y}qV$<`9fW;;Q?OSG#!Zs}?4aqhuQ!_A)TTxmV>{?WK3v2NCPZZrQ= zaNY7HRHCu;X%?BTEV8jF;sLU)rVe3f4_Vv`F%b&mdy^IEquZ5QoeZ<|Bbt521@!Mc z%VhJ66ba8n%9s}oT?Jc4s!EnqdmJyhdI=miX258+y$37=-7TLRj=8#JE{-SbB*Sj6 zCHsS2Y6O(1?rV*VRT9oQMR|&K^+P|MeW%#`8pGl{AVZhxL%02cON)f=xiz!@I_ooZ zj#J_KJN4(9`Tmj;rm3}AFJETe6Lh+`XXE(Dp?ZoQtLk#?jgjCE++b(v>`UNcM|tB?RloHBDO5Y8C8%1DtS{fCFL(YNCypMW zO}LeEuYA4^k*xd}7L(QEyX=FD_Jf?uZR;{4gW|_X@=6dH$Biri>ChTkDn-!NSh~8p z+Or2BNr5)T)}Fpb$=f05ETgN1=m(3|%ffH}Qs!==|HnN(j2nLXA$dgm2}d?P+nVvd<2FC~tFr?|PtW=w82aZx6=Q`y(ytz5(MOgKst_{nkr!o$GgBo; z_03UuTERTY=wDP{HpKXXEl+|qb+9JFQ zT3c!E+C&Pdw7X)ZDl=;q!R(80s}DgiD&eFsT*NDBbI}@9aNA{w7;>N+WDgl}@TQb> z8v$cw<;E38I-AL5Gp@}h_}Z@w&S$w5XqGEEat9Bq@!X0hDbOjk476wgK>hb^`tPH% zIMdtQFX&yDD0R`1NBAeo-1a7-!Z`QvWU9W6&i=p_=%`hGg<*pemQK>fDq)u+*bt@^ zq=7Grazl1!fx4NwSD z_WdV%ls|pDV(8OXt2~kDFa#kZe85USceT%|_7U7!Wg@$uPt`j zWP-8DeD4iI*3|TT_{aeLDA95Xa}R=O@DMlV{f3;}Wtr|Rl4Iv}quzkD7@qJl?~}NQ zNQ65h>;2AKSlc={I4HP~^wxVj4J+bIxCH9~s8-w8dHHkPi)Jkf;D2QEG`jACdfNH; zMnwf!sC$FSw&pA;5I;~;3Rpf0k6%8j{4(aQNXzzMJH zO1B}xZ_OPCn+BY)sYJ4qI5Iq}U>4s3yi*IApDQEvkH=}{T6~&(S&~c<5hj+r>%k$P zJkNYQ$a}E?dw>jF_k&FiV3u_qnVg)Qt*w_TuMVZrm)W2PRpi|Gq4$xN^oiPre3FiT zLRYX?gDK}92LOR&W+_DqJ2T8J9`Fy6xBX<>dTw`rM4dibDUtUqqwlC1ds;@6AM9Za zLC-h)NXf~2VHzDKIDw;oYQ8bfE;hT&0ke%8h>IZ2zKv(FrEwMnZwO|AYyD6A?0@Bg z(wV-A?U9NY(&lCx;v(&;((j~KX*+((hG|<(QcPy`oI1X3XrMTCm!**(bMSpdao8yJM|`6+zK!gS&oZKb z<1w7~xlg5qM#w-};cfXIx~m!~IOe%?$+`q3gDP1)-1I<=9q`@cLLghAHC;R*Sgt9? zKBd;40LU9FhDxRd*r1n0Q2TD8C2*q;!yi7*3}g}!P4=5l^^PJQf}kp=0(NK0P%!rQ zrzH3pq}S-+5S!xWZ0`NBTOKjT7Z9t&*TnqE8&)#epEYg>9?CmIzJsD?N8Rs8sh^4R!bEaQl@dD?%gm!uQM{(Wh zW>K8tmNCVnbdIGao-6#0w~xs8%%ZfvpXzBMHKBgt$aVItx+yB=JDMFw1I5_skIRGr z@pib41R{n_vV&2sh~aWa?+7t+m0C4DW|*0l)#aj1_Y1@|`a-A>t0^5>KijSJjPJjV zi`f-cFA>rFtUSu^9i#EPl7oJBDe7y13|q(vCeSP(BiE?!kH#cqYoG1OoLn3+Yrhe` z)~CYd*P=Mnpa^7XvtZ3naekWgatWw1%^-utglX8 z5l}Y`PhIQ|VFdBWz?=n&HVM>e_)s$uV9}Bg+ zf7|QMd!WEtSbktn@r+pnj@m~Tu-f%w+1nV5Xj7yRBg{FQYvYN=o zvz`KDAk(gP_~ArE2#f?o$S!SaZS>GuciW;3N$0GUr}6{vNn9*2iumPq$hf!l?$AQ2esD6ej=$ii41#xr` zj@ig;KZ)*Bga8?ABc9Qt5@CoO5xZdzA7TP_JG%bcoL+|kn1qzUy+uyO+NUly(%g=w z)a5>39ha6D#^TG)g4_-8QlRIP_wIN|2OQd;6!?lqg}|pQy#bMMX>EX~1kZmHwFnF1 zGyJ4%tcDL1(00Z~1?(-3fVt5ex6EBvaj@<<6Z%nrHLG6Rxg7B`bAiKZRTSvI^pq^C zEgQN&&)8yVaxGe#yzZ*p5$2mWhFJOxjgzabH_vC8piW-~k0U2;cKuF* z4oz;91#ZDVrK|c=4^0@#rsgSWGdfCp19j~q?2T9sMcc&nBzWqBP`0|MX=NO0xjQ)< zSNIG^+dk_xsppj;eRv5sxd=cBG-CwiaE2Z)+QH05)AXdnY^}((! zZ*grZPtkvjRM+=SlqHq(kaM&g2a-o4+Vb!{6)?R3JrS!t?B(@yWH`BmSQ`3_&AU08 zF#C|hK9CT99e^5yf*6Nm=s;(fFY7J~e;1H1PP>&&Mg0d`KueLWI| z`y05f$|dgq-uC?{j$5&hfcC!8?r0Qmw-pCY4yg1~XxSrL%ePm=-H8)gr)^4Q$`%t~ z%B`F)eiI5z-dnBN&IAVg&S}Q_p{RF-{Wd|#gWk8pMPpiDj`c4!2QsA$N@s6=BNLs^ zVj)DDXotnsyu1YZFs3S+`P#zb*A07pZ47p&w~ds2oMN+#YGQ<&CR^C!n?$`vymIwi z#w~uN!3U0Q`&Vj`oHsSROGv_i5rza=)lIDSxhiBiro`mNf`>l4(S!}gkw!A2coOte zx3R`R_^}pBt_NL|)t)i0E)7m2_uQmnZyQrKvQ>VF^CoC**i?nltqmfrV3E!>!^xil ze{v?Z)VRR%6Sf@hEFuxMbuXy}m`}a+O|~8$Ckl_vaZ7C`nTcvzSi^EP zqj?ABKb6|BAz#@G2V<0h)Y`ZrK3I`yHpZ<*Ca$((EQL5Ae2o;?Ni>R-bu)SouPP=O zYNa}vA5DpUDje%xg?z+ncdj3?fMw5-D3p*y1ra7Wf^TgTskXXxgdZid+`Tu{(J27& z=!%M1cWP{r`fon@2v3>tSK@FwTTI$eW>y1H%+PSHQC?x8ki?xY*MOLEkK-4AasO3PruoS{BmWE8e}Rhn z)ss1*Ah-}zRMZQYvJ(%PoOS(mn@N)8Q8ETHGBFl(7a8=Us*2;DtlD8kAS6G}XP0bz za#FB+FOGdsI`w9@w06m&-?TpTKL&7S%b27Z#z`hcUFLnWie0qN`uYmR$R zgVK~;FUO-&duPoBOp4#WX%5`#kBXS}z;uV|p5_bhUJSe@_gqIqOnYer47qWu{-a0y zuXb*Rep*tQbnZ7p)udtLuJQW_e1l|(M{wQ~dO^)k4Cr1c3f$V8(iJKZ?5kY9im9etr!#B}T^>gP5 z!V|8KJ((lM;xpX+t}j(T-p$p-Wx82O5I%hnZ;6j0>^FBaO`Fv1)z!v=o>NJSQ})9Kk$ywKyPcnyDWD-(8yeeIDb@v-@~W&9%D%JvT$d#OWD zLMn89LQ1{0rRTIL^T0IFXXSk8uir%4CZ=J!R%#mH={PUp!Z1VxXF2S(9~--N!PaBj z7?lG-K_`Byy|e{7iV?I%Mf3Q?z3r*cmfzir9SJ$S1yg!@}uneNw!5E1VLZ z^GO3r=6=#bgt{U~$5u%zeFU^U`OsR4Gm_>QyxMIii>{ebS+)62&*iE_>Fx%nl#9pC z#Ciq$3d=q%jDS+LNwa8K{H^d>6;77xfM>FLd%!WcL1`B!4K!Q?oU!sR`HL0lT;eI@rXOIGng z^vjYuU`_wTVqp>r$G)r4sT2dg0k6}g=+AnQTrNI@1Q@b+a~w3J7Y?3aFD_S~>`|kE z$)IyY1O%FZT^9SbPNl;xSjDUfc=Qyz=qCWcRROekpT?Y_{}h)>l4Q05k+{jV0N%NA z4;mP|Pd(1rBQB2A4W?!UBAC}+-%yiYukRUcABt_6SNJ%r%b3aeye}6u}I(AQ6p?kB=vZn=_$pWyH?@XDCga+*4yCX818Gbj)KtUj44GsOkvq>o{&@ ziZfEMr$OGR{An|$X02WwBD@k}r&f7UT@yu?NZVAdGF=wX*w=(Qok@;c3IuHti$Fnj zc?eIRN>ed0c7~%eFmOJ>jg?bTQDIPEoF1UHWK4j0pliNJr>-?!-E4GkGR(ckkhb^} z=`}g@Nd<8l2HwBDIbMxpO^;MBPji8?3z}K_qNINM*4;+fz$+-~_qifa=&jZBlq&$4 z%}DQRKIDLAe$6huiQZ6i>RbQD&Gil$TqEiG;xn{|H5KU8$~+d}t87Hb3?U+b_|n0l zn#X1ifrKoRDUib&cZtFSeR2OD%Q<^f&yx3Zu2gozQfhjEV#E;@ll9LXyFbb(s$$na znGOAkc%=Jpp!qcNo(;A2HE8jSzcQ%OJ7!1Mx4VW*hlP%Fo_4=p_>Z&T9R+c0fl z%cJsg)Q%kSxdjneC^#l58M$YMv_9m!gDfoVHaTVX?#w@{VW9duo??*uGP5(r_lwX(k-=Rg)Pk^|v`HT-;^eCCjvnws{}(v6I4J z<>61KNaDmyKWsSI>cqtI!Y^d=aQclof<<{U?IMFS5Qj56M~1Nrt?gztcuKLb-muV9 zMnE=gkpj`3!ZS4YlNBs2kx}s(#)PM}NLKl@VX+Ra{^+L-3uRWw85%pic5|y!@BFpM zdwx3n@t(1?fQ9uH@@p0xn=m=O#v5$(^rs0@l=t}l@9|HEn5L%YX+cq`x=gwaZzq0K zcrhJR#bs5uE45bQt*9ZR!N+yI?w0~$7}>B|8>gR0XTv+1pbhL6OBzelB@#PbuH~{q z&5;PuJ?uQHYIDBaR$NLC`qj*-zq_*j(pVAK(7@l4Hnvj@OI>pJ^h8g5 zg*hoOtYb_02OwM5=TFc{NV(DTdu(d4AFvE2o5yexL)@Vg6m2CZmE?z9<&By1H`fhR zXO9`TTch)6U(3ppR7y9)QKW8H$&Y`=p~pR5%xZXV%_Ot^X=wQ7f26nGd-gPO_U7ps zrkeWtu!6}BT_jr$91>ll^Bb6%!O!(O6?SQgr}~qHlimLuvw3A{^*^Q3|HUu?JHS&h zJ>r2ivwPdyZ|#SOzq;C+D(BZWbv>UT$nf$Gas?Io@z^N{Ysng`&kB!N38zzM-Lyy$ZoG zwbLtKG=1Rd3aM&bxlkowxT)UwXg6o;z3zqncTB}EewAO(N-OoImKP$@7KjNiVJ3%0 z49bj2t0~>#BF##(*B3-<8{k~hL~k`g=df*ueBngp~%|g z@)FKoe~L0a#-P=c?+=ojC$FDdL|KVHV3jF_IkIV+FpFzrco3&BO)|6V22L_^dN9I? z^*j8@NG9@zP$kac;pKVf@@B0j6%5nHEZ{?2a*iA@TL**9vq6}WP&-FQ6|f1}W$alD zE6>27q8fE$qp6KI8i5%BY-?{-^TA>%M=VxupcGg|@#d}F66R-AeAa&E3l9R4!L=#- zB6|lR^GA1fX>RlnG=@kybp-skJ&Iqhz-prk`o_$*WV)DpB7-`aInh|=6ryT%o}ZFnWo3jcp)L zoos+%1=oM96$I)Q#cUB%>S7i|Dtw{bkwR$z0^HLc)OEXaS68M%>kK}}A32tfZzlY{ zbe3hATA6uxAeX7+6pEyiPuXRs=L2DR3cbNa`(pTbY32I`#-mv39!6bwo0~X z@=wMchO*(7#w~`W0YwOTo92h>HD*KSEt3J*UFR5G!7vKA^2V;^uMEh;0B4{&Izr;gw7LdZ3amCWL{^NtLeHp123N?fU zGPU3YJn1!MYf8^aOq@<8+5|eb&$dnoD^6A0)auvCE1O!^AtbyhVliqgu84j-Mzm!x7SUtum-(RjtzJ<}=wh z24&>ATKFRGcfO5tviEr=|DF@K0hP4go=SXI=7`y8uqfuSITo)*t*rDBySTe`*&e2Y z=|xOnoTvk@5`Ec_<%5%>fyI5&($Z2GpLITXKd|``8e!N0HX2+K_Ox^4=nIOJtUuK{ zTWB~zyYexJ`r#I?=Vh{lZEq`-qf*dRj>s}L*bAmaXGi|d2f$2C?+~$7!nkU|PN1%U z&hiay@>hP)wsXwN@u|y*6%ym!I98{^G^~~JrZ>sXz=|bTSgR4OPf?k2Q}jPdlJ8Fv z$WR`QSvBU=Z~qY<9{#2l)5vZEf!d9%Dz68KJ<5^`f-QR#bL$A)1+r58`FF?)Z7on0Y6W3IoEGC#6fxchQ>ce zhFF*myg7-usD|sZZ(}pABNfcL>*pOlZ|J9WB_&QwKSSAtkFd|No?zfx+5Y;GNAS)^ zMuh_85FI`lGsd;D^)S9K=#}JSj7`*}S2wHO$fI4i&uf6W|Lt!JsnL~-h9$0p12WPd z%ENCYQOc-^BQqiuF)|)JGN~n0E@v?EP4|6_hWC}IDr8eW_v47F$?T0o^Tj!x`O0!l zb0gBE*B=9n`A<}xLy_+&Pw6OaS87t~Ip>jM*V~)~cL?^{T+D7*5(b9`6L%5GuX4t+ zh5a-ac=rtJjyQvE7XRcfUqJS!PzAgl#~3fFnpPv&cJ{1jeBoaoB%evxzWzn%MfLBd zH`a1C%8p+`%LvhP>KnTra#>uI7TW4t(#?}~xZr<&GWFiw}X z1_X^rLrA86A+GeV^T1v2loNQLhXnjvsN8nmijR+vZ@?4=X%CV&H3?`>k461{!e`JY zQ#GN8a2VoeKsp15gbNO6%uhr8gD5@0Aic#QqWJ9J06GB`1%*7)ntVg71D^~s^IR5m zX>|!eeHx7n!gAwvTkLX@^!BwMq5zYR3F9-RpSyM2%6Ur{LNk9Xt`UkUsn;WlYg>#QOide6#(R%Qs!bf;MIZ_tm)b zN`L=%Dbp5xVl6;y@3lUqguAveU*{U;J$Lu0F&Sb-=h-w=L)nWyquNBCmOSitY=#S2 zI#od>F4~w}bb%_T6h{Vr7SUjSYgU*JmzF+El}l%(DJe#G8ZLTCmY8U zCOnV1C@eTtrH9*^Vt+M7bkaM2#XHtD3+2>5nucat8nL`vJ< z80@0zxzw=qfiYFiBY^7Yh;@Ip17jdR)MF0MJehfc^-51GniH`OVJ%{?M5s!oTZgpI zW>aD*T$rW0#yLKTm19qa9uI!Fz$JY52}07B4x6<1JWG0ZNu~QcSb7qh@i9G#qY(GK zf$L<{M1Z5%vqCXinoe7FxX&5m$tQ2S_$1gTWo-Y6ZVoJxmaeH@`4ySN(ppc*L=yKm zF#^0&D_u3a%~09iNkm<3NEYzwyp`(|Xo6(NOMckz2t2kNAF!{JLo&DY=Hsibpn2FL zjAf?5e2YDDl(?XMe8(0K#4`r&_&8|KG`$J5tOg58x%K}z)%sxyb&dr6feVs}%{EvVSIIPTv<3E~lSC*3YNN)>&l=s#q83~X^FztF)CJpQ zc%2aza;+K4LKwlZH@BmWOK46NQl6nb!|{cU$RdvSDi^q`R?rY+bC;}dH<4j^H{S%z z#gh}x-Z&y`7CE7kyqNHFcJwA(7x5+LK&~AOr_`gIl6jbrdhQ8B<(7~L#5$)GWrB+C zIrLd%<>nbx8hg89o%-SpO|TbzYI2llj?ePCfh@TZyh)4st@F*BP%!-=sW?E$-6uhB zpf5ozIw@()8+OwTIaz6fU>Mznb`Jx6o^7K+hL}i0oTqxQ9HNtJVc~q|&E9YnYsy#q z-_yCkNrz@DRM-4qoKgFg#?OXgnx5l=-25%nKi?z7FxB~BI<7?w<@%GqZU_cq1>Gej zn7UXbnUW6EA?Dq}F|lBSHG75XXvCW354XZ_F$JNWxJ{Fs5~S0Z7J`VAAR65$gH1?B z6Hfq>LyVZx!isN9I)`aTbG)($V$-RKrzf$W=b5ysY7?9lW*QH( zgHF8AUfL*;i}&u)l0S9ESMMik~GjlO>=F;$_=%&jR%(9kp-r^N1?7)O>--@km8~W zdM-_&c6qlV6L-B0E=<7F>};={KU@$(2ah{xB$m(^PaKj*_Jf*+f@mtGN61r(ikht8 z#vsJgOzT)1fX;;hwe2VzZ>X-nZ{Iwp?5vd->+Ykga)2xg1POh8Uat0psioEHpw*;L z=u?it@}4IuGkYI7Vt_JX3VS@wVuCTr0~c`mRSYJ)tgFs!mT3#vK+(s)wY-j`Jq0_i zXsmYSJl0I1GP6ImGL&_Zh`I8hzu~0> z<5(j&TI4Z@F7;d_IGb==QF`x9}t6U&1&?D zY88$01IM_H>&#{Wd1Oh(gBM`frljE%)8jSyYN zj@xj>4gM_{+YPgEhsBv>31kZ~n`}Q(qLTuz&uLIPk%Qy?@OKLL$ll}_9mB-fBQM?8 zv_CtmS-pXZX6))x!bt@}OjGkKVKYzoxe~(h;SKT^Y17;-8oO}_v~}4{ zeQfYjAOy|cH)0&w6s7!nvZW0)k2q`56pn3(_X`NotQfyj^$$``zHh;uXeh%>NjVBd zh9#6P-mk`1R`U6$&yTmWBBh?z02k6rk8}*h@_VHY@?js1&P~@CklZsoowvv ziX4Nq+=X4E@em`aLWCd90>UbHiY?_*=YqNCm{k8AU+EJ=3sb#a;KceSNX12p}^yl zGpWp*lZyeH`rAfRM_1~Mx9M2we|X2wXk{PoDAv4LJx{kdV0e-0@J3 zb&tBaLA8yHhW>R0?u-o1#<<@R3g4v&;5Q-zzQ}0<=wQuqm>~kTM#nn;$?J{?^4#e>0;f2+U#efpcG&Q4O2u;fmT0j>K* zNi(!DW$99qn@uWXMVj*lqw{*G=bbolY!Jx#$CE z^?RJ|27Qv{uo&(gU(zS~^H{{%0op&owFN@^m+dGL&%o|#D}8eZQ8M__A3+xi=F~`t60P^U`@Uw7|GR%1#VGL`*VVony ztas!z=@UG0*PZHRLoQ6Tc8AdS>ilK5AAprcDg+a((zZe8Zg0Nm6KKSx_J%(y zco=niU>xiBC-7tVS{!3lt0=zwEs&|e9&VE>@a)FKGp*a2w^Wcb3G$;euI+(>usO#{f_8 z@`O;7`fN|~)u;XGatv$ZRu^U2T-S``#{vorUG+lx*3t9p9wOJ18(cU4lpChZkmDGe z1ZiI+{+-BBKI~4nHh6~%fA;FJ%u8&;c|bwgP11Ek1ipw6onF`It|^){v2@LF&4aQj z9@SHYg=IOz467QFV+db)6M?UnQpX#VVL-4J3g8UZs=E~K{(|x|J!qktGVR}G2m$Vw zNDn*L#Qz6mNc_Ku8uDmXYXp0ZV5UKby6tXHaz#0Z|e#13|0 z6MxRlcN=t)<_Jj0_{zq(Da@ZNqiPX!JQWYssoAw!lpM8|W)dSJmsj8GY1USlk9=xW zP!GXX}U`& z?q{vIzR;*AR<9hHRL+KwjQVU8sWRz_?4jWC9869_nA9jmLjubH8<5!q@jv+va>lxA z*K-6$UL4&%q4zCycQZ2xu#q($GPVB!lk`X^T>YN0D!~1-XOCO8oqAGPxSM!Ych{$R zaV^7ygU-KjhTP6{p7{-KPv20Df;I?FX!X>8T|8sMO*G(i;#b;BGDoQ(OG5o~eqe!l zi!D298Q>!hv_5nd)N2`k&OPF&+?cV|TMb=FKBC8NAiMTb*%=X2vjXLFJ|{gMQ5&?| zC6oLiYgO>WK0URFEnsR;lNSWKyljcJJ|kTFVgyPv?cgtdC7su*zWyh=E4f!%^joRa z{y8bP*c+w6RPFSNk$e{KDJUNyjX%1nh>wFc^v{MYa`nBL+A;uzL}qo0 zRJg`+ERWtB#B?_irMd-HT8&P>kT59S^@$%@W+3A1%w*ME4x*1ZUolP~`jxPx^?`md z`I#=UbykJqDCZHwt-i(kd|m2cDzM(q#;vF0QjS+PR4Ug~^tqlp9xHQ4Cj23A87wVK zBOqZeV%tRGH{?SS-Ls@G zz);oYfmm8*S?yv;JWC;HW8nJI2=fFsyDJX<{M+V5AE2C~Z^y5+uL*3v7fINfW^5Nq z^hK}nzMzCP+x&}M{FM|$2s&BX8g3*L2f8&Tz6`z0y(KXi-lz9_u-)tS)|Z9$u%K0Y zhJGACDa=$TI2+eIo7#f%RCMOg9_Ub+@Oo|Z`d$vO6fOY@7n98vZGv7*qbaK>8?T2T z9`Zb`u0OB-3wp;L;$C}LXgb`*>wQf1-Hv--ArvNp!d0(n_MVD5{IowMM6iv3GrgvVMHi`qcEnV_d~qn=l2(EZ*W44F=wBIF8DARXAho}t^$ zIQpPTf2;OGEJZ?-7FzvK(pTTwtpvUs!9pUs)4%R=Ro?Y#l_f~q_1OtEP&5NJ)hz@# zI`4m4oj;H4y0Ba1YJ;b_Tf3!O4nXvDXd`Eerx`kQmETHEzg zy5o``?;#N73xd1*3sz2;rIiI4RZC>mq+TiGcHO4__nmuMY$D<-hzPo5Z&Zs?bN!55 zf)qoJ?q(Pp=O$1&SGcZy3vAb+98dT|CfpR{i{>wa;|KqRv)?w;#KUH(OjXCvFvjV1b1W5yJ3q9c)GRxgN@OoI4ZeTcRTIT@^QSf}pv`w^;O_wkaV z2y|d#?~aMneh*v>oc9d>f>t)OBm9@(95}6aV(Jixl|<8*lG$AEs=kMmleQ8S>3ImZ ztGgJMclDPi(dD5vqg#5B`iW=lkVn5YQfsLq`2K?}f8wMru37u^2RgG`LfzZB65(O} zoS7(Hy|JID--HBd^NmE%_Ns>Y0Hew2{F&(ukW}O}pO#i(UQXqPKw|9ViMNpw(kHdK zWRK|Yt=M;yp&xh8qx&0x9HU__`%09i2TE9R(=PU4?l@oeFV+(2>;6$bq$24a2nYOs z@OlrjQCA&t%JCoh$=o8{Cfqvq>cH;#SqSK@*jf~r2&;TOi@er3YviS+gRn_Ab|ppV zkRwA#B-CoM=v~VmCWb}+kZP`)*Y4ZTqb-LBki$XXMQFDVB?}zzUT`|vgLe=%8$<%x zuZC)^`Vs`5GUR=How@8b(E%apPzV6WnUTgX>dk@R7B?n|r%fC)Sf~a%auCd&_~q8L zO8Q&ep>DTFLU4Qg8LUhtG-qu&G&v1{U1_W)*|s7mm^{SUP*l!aLN^Z!IYBoa)j0Cl z)E~7L<{ZalPPf>wd;*(j-Krz57I(W=1we@?qF9XX`8N0T!C z!X2XjtQuUp&HmP+vQm-Ik}?!r95eZ{QwDRh@WTQCSTlj@QL4ql#pT!g+EfYt9yALJ znsX+tzFI_Go42_V0pwjTM_utSj`UGm`FaE>+?H(|{&CR5WD)OSMRDB`!vFss>wm}i z!7?3w>VF;c|NWR9`;AaoC^T5WgtM^~j^S$v0M(p@li`dHI_04acCLH>c8X1-OWzl3O_lcy_Flm!53xwX~o*1h*lL5;#RC# zJ5`1UK=ip!I7`K~`O%x=B~KK5OuN+{8ZN%WK!m*``P~F9KW!0^o$hSA6G|zxXA)Xq zxX*AU6Tra^J_{s0?C?>?TxO0?FkdU5vi7k#S3;IK>T5y#QGQh7<_>tv64SjoMJYCP zyn~%Y-Co122ksiAoiz>>F(Gg!sO7-OWqn6tImg3 z?8VBey0hZ~3I@?LfTWf=Ix30b?|h47b6p(6Od+dpNXzRVFS}}6ORGjrfYQ~6BJ7i> z$uzC5g7TR{F2~HUh^a6tQLiGGt8@BbF* zh>wd?bO=Wu!GF!kTnqL)&G<`i6LdW24p1NvdPM|Lp1ZHFO|6 z27RkG0@RA8dtLgWBd{sXmsI#x8)&Sqsc_I+$xmk8fJ#0);CDSqJV1{=QLT(a_S_5 zC0%{F^+lb$nKraDgRM5~b63A}p%Xa=P5q=#G$x3*H_-gG+d5w|}# zp4^Sgs6v`vp6FtmY?A>9>6)VF=EvkDIv81(0iLsR*iM*oO%Ec&WU`@B{Y zzbV|2&km2jfZR}h>QHog)6+y3rxHEB9SZ!8Y_=UDjL&WRJ|7GFzws6Q1H#vrzP=B! zzSVFi*xNnMhLrTy5{C5~33QHi28NN6+}wv&JU6X+W>FtA>|w4ev~sY7GX{M`aC zL=4jFLU>yw;Zn;5^ebEaCyiRa577isDk&-9H^onYV2Yyf&6drLUJcce>~+97l-6m- zul&qJxD3tVY^4c@NhN#AQ(KP{SwQOu=}{kkb-p>m?f+MZ|L+sp@x>gj{{LT|Vc%YX zeoPc>KT!KzsfUkd0!u?p-1hdIpBq5Nkij@@!z0@CpjBcyYE4mtIdXoFZ|{QTez}ya zNT-!C*CIRz;slb?LB)#00o7|WqvQ`PR0TV-(#2fGw!en2T?fPh1|+B|4vOAJZ0c)l z-oN8BK$tpwQI_J-x;>Cu>e8n&?-*OYUC7BsO>Yk|vxWVlR3DZyhn-_n+xnpSej#i2 zg*n(jWW=`w>N>9liUmw`qE{6nncG|*X;qDP_S;Z>`6ktPTwqt{uD4s3fPcp!Q^>82 zQ>2Bky8HzV{V^BuR{%0SpgWIK@_8kOJ4Mb{G4#HKsG^Dx{X#A_QeTy_CVCE|SmI${ zY2xU+GRA=h;iu=~H>5Hm{h#twb0rEOq&5vx2RB^8L|gmQMa}L9GjN`PBUK8bCI@R& zvPrG&Ga*gW*3ACG&VSjZZM|zmdKaEV3VnlBf`!+-=zT2!Q9&%67J`WEanKwxO+- z?Dx<5LJFB~tZ0btB^m(-)~AulU!n}OXR#2lbXZBvddpz@tgAz=+{>F$I0| zC0aZ+Jh@^Na7{#n;@Kc{mAH2RBrTugX8We1AJ1vHw-P&ZcNi zg-S^&rqyyS;CA(HSYD#g_?#c4Uu4W-_dE2ba2k5^j%0;~qkTSxuhGIVG^YG+0<<0% zXhlYIAE%nn&Z$foj5CLMrgn)h=3DD$g;9m-N8N^x`qi?p88BDN|K=bK_UcpCH+4yD^YBqAPQ07*I+F9Rd8u>gAoMx`{7+hg;YHW=KU-2;f#`$ z01T8mMp<~kA$EMM=kglwjS!g;Mc+0(;eMt7f_#qr2l;0p9pp1A`0a_Adj*VKv8Nc)`>c6xhCFpC_S4GtIx`~s`UN~YxjBAEIl;-c!I~fu z8h-zdFa*O~-qx4hg@4|5ShVm>Oov41aQef|Cxx}^@G8)D9@>swJS&{eAQ9moN^08~ zXEvXlU%6>5(}U*I(y9_NJfq3ghK9^jMk${A=qKHGUnOJwS37i$45@m=QD-e>(fo^$ z3aN09ShY^ltAo%K(q%_$F)~u#r{%gG13gY36naIL4P)a;=rImm-KY#n$9VZkw0s0} zI?SRh`tJslC@=Lg=|AH~1r_8wRyCet&wzyf5V)3hyCYAv+TGKxr@r}vU(YBu_5=gG z$p(3GoWjpkl%0;v&QhS9h+WU6e399s+%O2~LHzXOdS5FrZ*H*6cHi#&fd8^GL^F`^ za_$A`zTovf-vkuVDAAcq6+6!UX`D-P>2TiXN`tX>5B&@)JqGx%LQtu@$H!xPr;d3y z*9Hb;PyCQ^>XV)nno@ z!c21>$u0!c(i0Lu%LQyGM5r|SMC3ogUm7~i%C-6*b-OF}2EN1ao70g;$|wD3+`V92>QXNy`=()3g*?Dhp(sU{<0&P~CF;j8fV6r@af`?XKD`_o0r7o)40@wT`eDJ{CPmT*qelbG0M48O zB13jU)2m+mD>kaC=NYVJjV9wDE>D1ayQjay!Nd_s*v2==R*HKjV}pou%YsVX`u&4} zOwMaoNKmTAb>h4`zi~KM4g_$9{H9Rk&?Mh%5@YX!lGpxMd}-q}#}?`0m#ITsN{|^X zJMJJnHjTuOe{&`FYq>R7cYDmdhq88+kkC$cE23? ztHDZyoF1>_%X(f$91i(Vd=fW{F{=zVY%g>;zudc*( zJHaeo-JZF^-@Sy8zkL#N_%wNNxs2;#r1?>;_u8G(D|cm}`}3;!;qMoOBe&{vnX4yq z5oxTe8xHg1kP2j^b%-vON0f_Qakyn2-Cj}!jkM0*Oi)WgaBY~;<6-INjinYi-m(&? zTQXL+)!)(E>POBHljx~K7$KMNy(L=_^MZ%f%S?OkLQ2+P!%0cFPq^JmhvUJMA*KMw zM)ab(^z^L9Mq?IWpxkjSEa7E`W>W?4qECpohFKwPll8S{Kg{3Y2Z@() zB1#3F*coYPXFR`Fojq_o-RHfw-JUklqO#jHC{B(Gt(k_auO^sr$~_L?j1Skzdzw_%<4c7Bre zxXVDSMvElo&~zls?Okan9&I+2loEfV&LH%@OSa``eI2+z=eKm!|D&$Z8GF)&iXlbg z*HZhpgf#zKJAQMdprSW{l)|^VgI_ucwBIeZ);7m7L}Htnx*}Y+n&*F~>bgt?Fi$CL z)P%leC9M;wXL<+sd2ds$qrjOF2VwewqUZCyI6IXw(Ra(dF{nyY3&&Tb`0A8H;~>3&OV!HL5usQ5Xzz zC*X@)!FCmIg6=}fliz5|UO~B0LPXcGl_4|f#4ZBB^_pWHs6BNrY!oR}?t*Gz?;TWt z>k-e!^0rj?;ZoTsSJAuUdy<b_&~>Jpzoq;a z38Q9-8J#SS!|yq>d@NMWAb8!ugw&MMj9pE-{YzdNgt_t>JY9<%TuMf%cE8*C!>dYX z(ML`X_m^IuIf`p|tiYRNvhkce8Kc8UGe4Gd5@%?mCZkJby2%+9Xj9b4jg|VL*X-`H z?(tpZr5{i$RvQ*{!7;^Wk=FbvNWfvOLh1~r5+NGx*v+&%he@c9n%A#N1#O-VzUzmq z&Q4Fc{vCdDc7%=8?OriAsaSHJ5UnSYoq$fMAloA>)+ zev86e&)jonD}8%aG4!MB%QxT`VX)3h+vyu$ zHTk`KiG!-V<$e=@n4u%-9tl_u;NDpN&$?JLOqu`rf1kzwby|(#YQ7~F^k&7&^!Fg5 z*0i=pPira#F-*yII}F7e9QuFa;Zj5^!qYv0y+Wb%s)U}|zq0%1wttzz#1uFfW32nz z$i2Ps_na6IUvhQfT4&N(j*+ZJpQFdAm5l;41=<&eXwfeEZYFg z-JW?ZA}6PZPRzlRq zb^PE>^ghSJY|k<;01eV7Z<^$P$rH(3h!7;h%n=&a08C&_4R1pTN7| z<}(J&$P7-M*1sDJP_94B*Ezk^MO`(dfoP<>=_NHFyX~Ff!S~4v{lD0HtGKA+?faXQ zP#UC51f)Z9hyhVRP)a(65&@BBXodz67`nTpOL`c(ySpWZ?#^fYJy++P|E(8X$m`2z z@4eP~ue#=rRE}mo_7-)eRw|ipWZ3BEE2ji4zA9r`Mj7SqD&#Q!clTE(bL8V+{zrRS zhG#*QACLZcWa5vQIqR3fv;QnTdq{UAZajTV?{u&?!&|b+o?bM70ZB3m%w9-i1H+zG zu&jOlikUnFz~2{RxzUSsUnsj6iv@V@SN7PQ{Ov-eDZJ2~w#78noWbc0;=g2gu`%}> z4VuHmgr1<~TtgRxpUIiQRG}aRTe4YieST+uxSzUF5LDd@hP%fy)c#tJ65*-2OQcd{ zo@V@(`%Ouai6~1L;;3P2M?eNs zw8!mj$z*}ROHzD(EsGVcjvs~6D4e!oUuYA$g z#93iVtlz*!=JZSeA!HG4$obod-R=GdD}EHeQLre%`m=12>1!eso1*8{G^cYv1XPO! zBhSB_Ge7CFoWhU9RFcjBOaT%ie~2SN!I>PRaag_@-4Pf1qSDBM18bFH_;frK;#QEK zA7FPO;c#9sIf5BjQGB6H8=J?&D7`NJ#R5M6jHut0HMup5DflF*<(7RJ@|1Sn-pq7; z?kKl7nQ2{HMm?;zFeS$=idUlqe7YIg>yG+Imay6jVNGxS=Z7fOtNT0hxIa=ojJ@kn zg|kqPY$LW?8^;vCnVc9m-$|GRMPF1K>MqP1s2%!NWhKzmpphrU8i}6q2+CKaP zkWA+9hO`8q=!OM#vh2C2#_%C7C`@Bwv~~2P(++|pqor=ykNo_C_tsSw8|_pa;+c1E z^@=Apg;jjUW3H!g9KNxgg6MOIV^7A2oB-D6O>|VH&om!CN%z77xOUJQ@pUF}rl|ue za9L^7XW}(^B>=va-$+8AKQ9q z*5A);a6?uG_Uvu0+7$7F`{V zl#7S9Wt}QLW!qMtYO3gzs`=@wUb7dzFbTyzQIsU49)fiO{xd?e*SVdTuKYPHhYd_| zsYPUFWr2w!ODzd|Xbuq6+D>93AT84Rq=T1!Hs05W=FZXK#1)cV@M20)yD9Yy*BGH2 z(?2;bs%?C*IQ3NrD84C)Ij!66`a$q($ z^+WX~V+6EjX?C5`iWP;|)B0idx07tC05>(O!^Y;l#^X&MZyn*U@P`B29%OSX8GiFx z5cM(C1&;o-*>X?rX6}`p2PZ#8*b{jDkYDy=2oU5R%K49Ah0Xu3nb@8yryH!Dw9PY?VR4SvhPEm~j| z)Ps)IwPL& zQW_vHlaUq>K_?L;+XP*(Y5Z`M-(jU5I(5*1)D0|+Q6FjqF<$fSTsQJYOt3<&s1XS6 z%$WfqVY%2`PKA=Br}*KvD*SeiF-i9s{APky{SoQ$fUui1!phspEz0=#ms4?9gMB1# zqDj}oUq(*}#d8}-an|vS@Q7+@@x0Gp`I2w0mfX->;u)hXGBP6+1JPQ^aGI}9?bfcs z2P#ex0d~MDxs)b=McF9NqxJi_DqOG2TiT6r9@qsj4IZgy*{V6>k@^|qu05X4c*51W zY79ludRbV`iggb6Rdu$ubT_PeLIA%%sHg);y`%ap9P3p!jSi_L@lRqleTOBTTdDK~rpIUXGKK6M`*74gO*GkYh z{I&+fUX_zM)QSi%I&VwO3(9L_(CvM=FG>@Wydcs@>ANVGe~8@B){dh(c1iWxLOQrB zJ4}|w?Gseix3?*feT95Nl4Y5%$rM_cAoR|js>H^g6k zJ4HArU4@TRbA(Z!J`qcAIEZrDNq@JgnP~etdDA`iQkE%+2}5v#&ZO^)#3f-Aef(@E zQi~mpF=yF${GEC=Z>{e!MAzr>6X$%DXP7bCg#H&WD2D>E4<=d|wSh~1xr9)`mn{N5g_v}wHE$@k-| zuDa!x(Cup4J|ac0x8);}PXdsg1aHcxgO(>Ey2tj8>nXzr4e{$$e|}}+Ay!6BnApUL z=u9{&8*x&LCHsleA7X`O8ow=VnNl6)rI5nSO|!rsNwxqUEZ@7CrvTgadhQvswCHO( zQD;)tub*F$4m#tAHWqOqG*ru^^$oYFQi*a8vYYDtgqOg0hYel?l4tEWGyu_HZEtg* zSA__aBp4?vad-6(nqL0ruPYn2-!Xg>o}-eDN{L? z$#^JvhooLUe@U~DAoPW*AlOVvh9&6OU2Au+N9?Ex6?o!LusYS4uZl=b;G1rjnlT5+ z5*&oQX-Nodz3A|;9IZ>%V^vP?EPt3U|9G#oy@C$Ye!3rdg8MBY)upJG=XGmO*T6>t z_<^rROR-eJd;Q=A#bDcUSsXB)lli?2)mnGpFN!+Vk(5VXz{V<-#wl_Q)T^W)%Nh2j}MvF8@oNvg226 z9p7@Gr77Pk_lgc?8%$Y+Y*RlFc1OxZBIqnK#WLAZ1Le~c9l#*8dhM3go4n9J2#)QK z2_W6&aj=`GlRm++KudkV*a;5uFg^A$7pO|x+#W&n>Sp=iVs!5!Dt9<8@z|DB4tJnb zZ1@`h%13GbW~6qbYUv>1vB=45=)zrj9@mNpu*Ch_T!{VC(hlqPfX5s;=JjgnT`|6| z6NdNR5UktRrqR>o&4UyL`?0*%T6%Q{QJS@H{T^J)sEcgc&d<$LP(*Qrd zOpy8*?NXxSx-+qpxl^~U6o0nE+H3v9;Z)&zjvA(xMuzK$)eLW>`#2;(_WqlGsELtG z39l=uEIO;!kjKaG=>Rv~bW0O;hq=A{d4AOC=LjD7>+=#@?((Y7({E4}{YEf&snt?I z7iHr$n$j!dsL3~ohhi%$5r=c^m)q^ng9*g^nmCuDY0btdrO}o`^bMl{I*~Wydo`8T z-)NA1LSb@ruac-iydMX*BK|(5Y=}^URqKMW^0U7 zPJ>FEkeCOpV22hb=ga;?dys3^@as~wK)Fy~rfMxw{W}@%F|+hoe|+3JqBa-7VGsY) zw@&4%|v4qO&r#7Z3>Jv|Nv|`Q$U-;5OkGzJO zTp>?qB5s$}aIvY&>S?o+bFzXh*${`vi4T%{S@?wnN%X}DFDn}~SD{NOW53WExKEh* zB&6UH>Uzw-zW<50>K-U`@ZCZnaRn#;DrR=42OZHCfHyI~kc@}9jIMu3uhr(e!-u{zb8*I47@l$0kkRpvCvBY}dLKo9Lll@0{8Wq)hx|PZ zj}$3S9d$fxcnPm~zI~i(xz(ya>88daJYVVTbDJqMnD1GuF{5ZlhVuU7fB8p`aXwXj{==pdm%uC@~KQIDNd#E$Y3NkeCsQ=xmTcwPy^3|8=RrzFTuLd|B7 zt&;SMniiod#VNVGwFpysWl_yrSN%(ogZ6yGpm0%`Uo^xf{_njpe zSI(S*aHuiF43FcWxmwzATf3dUjfVoOPp@qj16T1IXl?lmT~c$El-}tdmZQQ5pOny7kB>IKD>04Xdw-EhD+7FOOy<7d>rdsJ;Zqal)s zjHhYTLK%G@d&~m?yFtc2ahqf`B6gWh;iZ-b`pJgv&qrD^Rje@CHDSbdC9CG|;)XOy zzC`!*;aGB%HQX8GV+qnHa92A$lzt!@{+LE1;XsOmI=9@%V?)gpq2j8=MrDWO*%Quf3#SO4(ev zO@6rSM$VXFy9LZo_Kj)CSEcuboopX+_gf}Sl1#Bu?%Y4G4fTIWQ)*E2cUs10ewj6x z(@LO_7(4PY#chXW|B_$iC;6f7=F89PdzoQYEY`Ww0cA8Cr_JebwwUYDhopmU)lBY5 zuEJvTba!GoeA*yGSW%T^iWA`X-kDk1u+6$Orqr1o_oyY#`|=e%)QUJ6o;@EQjx~gR zl0Qa(TN?LhfD_J_39iH$k0pG3dWmf6m)9>4UYiAF&t`XH_frdKNso-y2$~TP_+c6K zNkYVahk466xYNzhDjzWeuJS7v1MmlqS4D^5<4Bq}Z29c=Cd(fuJa|qOg8~s5-XO-+ zyQ3DvyLS}H!=vSpizQ@3Y(xX>j3%{=o~C>QYIEA0Jmaw&tz2hGB{Cw<*!3WEpLaG( zYL*y{kVVtV3AkGdLVx;GN_6;orr8QvI)R5db^ogOp|Rzz@wx+eFY_=AG4Q&Waz%)( z6hcQ+%S+JPhcKFxc+w{rlh-}h0?A2z%I6<;wOW2X9n(Le(|O<9FR3^T3C_I1_{2C*a=czT zGS>##m^N52<-Pq*vEF`K1YpfsZf%GYk|lG%-(1EJW%~eBxk506kv=JI`wU%9$%<-G zL=tZF@8aIpP&7fX8pNqxDFK$gvid#!pYPcC? zTLdJ6-qnV5=7!q|MoV5qp5N|%UmCf?&a(~hU0A{xV(8MXWZ{dwV~%g`OwP0=LpW#E{+nRE1wfuM#K5Aav3_tZ`wi1h6AYKf%TZHCVDw6@$bSKCY(#Il_! zkS~VWv+sW zzax9lv^{A9$C5SaF~|G z=S8O>rE5#&t&eyy>!$Mf`tPc%3o!U6GF3!Z3$MALRqEQMI~H*F z>-^4B^R4JO+$}rO{rU%O9`8HMYAq``l)x{8Z+?bD3(;5`qGK6pyQpHK5bX#f3xc{%u-qW{Mx zwX*;UXvFp;GGZx^{dq8$^cJ`BL>MjidFXT8DkSxjK6-@-lhg}V{z?k8vCZmr?qM*R z;bc-0QcC@M?8Qzt&Fc6)|L~CEJ2}Xn2kJbW2S4q3MRrnLjtypF-1}^;tdVr8Nn*go zIUedS0Uqxd$Q8d6z^Zqgv@{CYNS;8pe}O$T{SH&L(TX;}Ez8bcy_l^&t#`~qXzVl1 zkB>75+?FO;5ZG{`R-rtlBgJ<8%Nb3Y!EKL%nOB@HuZ1K{Ntn_X84$FtURXD`7xDh~iX9-s zv|tXWsEXqAB}ay0$A!%(g1_WbCYt&wowW0XO0u-KKb*YjH-)__ ziVTX_dI5ePzY^?r+^ufHMhshhmm_|@uZb|+{YB8by}sw?kMB=sL3&-Lh8{0s)?+LL zBvNavtH>-mTp#3pR%c4{>MesBI5!EK7#2#jwx~C z0#qUn*Sz@jq9r~>-y&)d$m}p+RCw|fOekr5$jK>?kVKP4T3?ZB!mdCx5Do@h$)>}B z@83~q{9;U)eg#b^+dQ=`x>|Hc{|!Be>b=*?T`g#M35lwP>`(v(_tlVR1Z_?hw4D5y?OcHx| zu|3FX6|zywYR~w3K>Uot*{_T=*3C5Cruk-TJZiQ=&Z6|r7iN?p?)D;qd$4fe zEu;A`nD&dMV1`-X`H~jmRDbK@=j4d>g7Dv#`?-W;rs%q(hv7SW6MLM9ePD4z8QHl5 z!RzNl0(QOJrXfiQ>dVqna6cF2bn%dt$Agvb$A`O<%eoeWnMKxgSF{t+l(Nty|p=c`IVK5r#gV(~O8gVc?ial8Y^U-bmX098 zm-}-$qPP88&Dp~v5t6qBq0*vqX?yj;KJ&qjz4{L)^iP#G6*q+$w`^eF$K|635IB%! zP=DyCf34wraS+vC4mZGfgmU;D8eh8!2j%Mi^p*IlH>>#STe7+u2Xa z-T20jA7{B_W7^cZ59mDc!2&cMnBi8dmoK33p0ncDzxcN;WG`AOhy@TKI@igs9Nf_ z$jNgomX!Q;c1B*|w*Fekt+=&;oR@f1LNqq9UNk2KGx1jcmf7+1uWnIss{)_p-kR#hrmR1_B<}Q0KFv}a+0V;VD{O;69Nsf`Zt`53nnP?)tF{l|5;sQ84Tj!B zva)nht%fKcDG}Soo9?DWniScV7=^)iKUQiG(aaDY|7BG^^y9$-kYo5g_q zs#ou#8w4$Wldaz^-@P3C6t+4M0cY4#gkmk*(F<88st}4E2bmPoZw7_)|J6t%YBw`% z5M4)FGH?+NgHGNMWN685qnAh@+1<2Ta39Fyi7I@w6iRTvidAX3r*X`*!bPrsgAFWx zs6YB<5Vdn}5NDiW`0Sg@qF>)o74dU=tc;}yPfI#6Br}iwj5Yq&$Xbhopl6?41J%{_ z1P_1DzHDpOP7+BG_A$K8Gi-0UPk15*BF!h^8TWk|oh^?uPgEHnJQ zq)lO5S_gROV*v>S^pYhlOLiT2R59NjyW>V3FFt|2%*-(VHBhu)lmz#)CC>y4%d4AG z(vbj>wz8loB(Fg~&+tY3|Lo&z{8<0x&zu1Mgmd(c+OpgIVqi6%)BW7T=Y2wqRBJO; zoJv7*bqgIc^xkN|$8{j|bxur4Fhkh+7e2VS%=}SX%t+tZOQA4m2h&e^pTx^%=`ag; z>$`G)^)j)X#ZLx)61KFhX9_dsMQGC$vVi_?I@xw6pxDE_0Di^#7-*jtprd~iG%eQz zlHu7A#oB6@jtjuN`9ViG8a62*IMa;44FjBg!Q%30_IHmNM4eS7m488efOR)HK=U(^ zkQ6Ed-eOi++&kY8P>Y1O;UjI&v`OI9jnARD9?nk#EZ7n5NxCm`h=e!!5Zml&>hdY0 zBK95F%-S^lj+Q%r1}XZ)urWPw(OzPkg<1YJat08vIzcaJF_H^q>@fji2ov@Z#X7^4 zFT7QLWEs|G^^spg@cQ+-!ZmZi|RP<5xc)X zk$=NFVBv=r;!z*h^ZO^G$)+ajDd^p-We zqLRKlg>0Bv#jv7Gg{^rPjA}@C^hj48=Ke|xLBbY~{LohbKFhC>DO|s8-)?c_e@uN9 zPr%q0tk7bP%2+Qwh({KBNx!4schqmPyA;+z@`*VGzMA694bZXVIls7)aDSxt3R>)9 za`m2etuUXr*%?1-CdPe$v^PO5Qbfus;c!{{kU?{rbfk$FJX*R{6B!A!PkoTW_Qg59PXVk>5Od^Zh1|mEn=c0d+;iVw)om4Z~ge@JY!jtyo$2d))jk;h%j!L+$wV&6P}9CzFO?R5LG+(5LThY8S27O9_Y>-y9foSDjYgAzRL z=cDIiIFFg^MkQI^eST5yZ4kksfjWTkduL;?*_*prYZh$kT1Cu$Aqe8ONf0{(vHj@?D4uEiXxNjB zU*%x=)k!@0@jNtC1PQ{Z{jppk*a7)ha1e}78%yH? zss(=7`A(xCF`4Tn34ALNesW@8YTh!4K=Hd|zn-J7ZgrteNH%}K*lfC&TXIlDH{&uM z5j}wcxD{3ml~>xF@32R$p=++d_O>nz^Lc*5@1HTgP^*U7%GNa#DMqgFINRM%e)y5H z6lP`4!Z)TTUDfhScPydy52LU-R_UKd862CzTdm9%8d;ny1je}|Rv;F}ALftZ@4WDT zqQ4t(V&0Ms+I3m!dbW4Cw3r*o!E4ywmX+P~(+v8S6r;)!6UyujvTLZ~UYk$7q3>8K zhheSUH6OJttXw+(SgzKY4l5hXx7m4}8b9ioeWElW87)95H8M;IXxXQjT#jTquHHy& zt!dx48$BN%|FcY@;8QG&Fw_JY+%tkK!&)~=H5Kb=6)nMSYOyUw8Cu-1N7Olkte*+j-;wcqHyhttEwjFo=SndA%58Q^H0N}BNT{~8vN zR+#@2#jYSnSNQs!-7T$)+K!_^~$v@rYX(~5>Rfa1I1Qv%)6{u8G(gN+_ti%R4FcAs*o6FQy;uSgkH$vaN!3S#n#eUzg$eRyZGnILa}cx;kh zyx%(17_~rjN1kgH1OeL~u?;x>9sp}>ylpUQBm7~KxTNvYy>`-pbwe9G^a9O;e zN=k+U6BG4YO@ay!gcAHFotn>KjbU2pK(1IDz2Q{6_^&?Tr<1gab((nFPkp%c&%r{GSx6ufd(}b5lQ$D|jcfw^qm#(qXJ)&UG zph>as;RxfaL;u3N5s5Ch$*fE22-ZEMBeDtf>Z}N87f=S}$eJ|9va7AJocLxfhaf?y z=7--Xk`wjKcZ8FC9PdebYtGwJpMCt6(bb`45V&@j>D0SLbLzPT=s7&7x<0(S{F{?; zPxUx7^7yNSQWEJ`TNZHP(F^w^-B`ZdflJ&RNAl0B3;$CkyOCI z^_~b*q$Kp`Qs|^I;z9JV{*3#+7x<8Id^6%jcTuWmzy4WxEfA4Dp-o823QxCk|LV1V z%X9;%nePBLuU{m_--?1a2yX{C{v5K@J1gR`Zi%U3@qgoeOf5_*6ZZb>@-$quMb@Lp z>t=6H^!Dvr*NK$nfPZRN+gOg=g(Ubh&zZ;0IP*VKciTUMSpmn}whiMti=z3GHvgJ- z^9IqpiJbxxftfO~BlwqA?;@aZ+K4A6SSfu0)`fa+$XgS9>BZ9eULeZQ~LQph~s^3f~kyRdL&lW!(8 zl1Z@FUE`;JVEl+({ryGf9g5=POf7(Yvw8w9nl+x+O#3@q?AY*{4vd@`X-f!8JraLP zrh;rWD6x*X%YqOB$v+@k|dgNkDoa^$OSI_>nnFw-OIzfu7uv!fiC z3P1^iiQ?g;Aa+xB1(gwwI|EX5;5SGpa{?W0BW=Gf*aYR9mP3UreTvW!`)eia`l;_t zCFn(lu;QW=L9QaQviRYxLcqip|1S_wbz9=633#|{Dw%&d*$*KYLb4ZnWL+a$!6_)C z5{+L8@4ib&czJR@p=@gJrs}92e(>RLT~v4mY3qrKBI-Mdg#m-}?rH!b9{6yU&cmC% zvXv4X01$XFIpObt>};eX^VC~X#!IzCW|RKlH7BgrkPGlOn&HlkC&OEyTN^K;Go`dZ zI@FXs?;X*&EU$vnsO_I<7orSjgS%psjbyCJY6MiTY+^^@^K1x(xyG9=$?Ka;SuzB7 z+a^39bIMDEht@GEl-S+5A%CE9AT9Z2!r>G^&TFKB-eZws&1f$>;N)$gguGXs2Zo*F zbyWV@bD6^ts*L`JL=~^--loFt$diw+%MF}AEqBn6Kv*giu2VCSYy_o?Dzq#Pk=kEg z8Zn;fmCF^zx(4$NmA*$hy0Ba;iY(LS_>fvQEjVCIp^~L6=0)k>Y z0FKNWVOEJQ1MjMU6DRk*5%<`1kZa??G%;jd1*_+lV@6j)LlrKb6Tw1R#{1G^Cj(#P@5ioc`bJ6%dgZP4?6yk? zs$R5(&A1WaeZT%MU=iwZ*nmZUI~sg^27Ee$ee}DkAHv6v^xiGZKbN%eW8nR4FR8ie zh{>b1B8r%*G$=bctMk6A!@5vK9n`qf;C6Z|+Yqus(CB_O5I*)_ z4)5ERceU0l|M`#S*5&<4f2>@=;c;o?pFl^pJT(E#tNkGV+290!*xZRqx@M4)2T zq=CloDSD(NHN2gWAalb%Zt;pGSFUHXb!G*}sVHg>r5F4VK_&tnQzpR-Ukl4J6xbs5 zHOCB+!9x8GGz_}qP?)W9Q2T9W&v>#{6(;68mb?)d3}ZWEeAnyi=(ClGQwnpH#RZ%R zU;28zg1g@i>VSKgXA2(AK``JT^~yk^11Co2O`mh3`B3~3jZYd$^?+bjPf5uWlC5j- zGGU-~nRvHUviRyl*4aFRR0F&^?B>RelS{vI$A84gnX`MCR*#QcP`JtMd0E32m41r{ zhs}aSIF`*yP;rM_b;gbOy{Uukc(+qjc96OwtpcQ@b)tFvB4r)>owBrfc*AtMWBY}h zVFjq8gworB#%y~DOkakl9;`K;lrLCFO}!0Ox)upU`ZhnwjaMFcZaPcO@Ohs!K*h&BXKsyL9nA*+n<=27{FEWok(q9;0FUSb~y_h6A_m+ z@$tneZ%`>?y5YMu8^6kU8OwZ@73I{_4*L5s|N5tJg4JEC@^@`FFWq*(XSn&6n49U9 zogO2PvOIr!HE!G=0wXR2VWcTQdSOT9IB=i z%cCdOt&xQAI4sAD5M@s^4h|03vz9%BBejGr;~BGQOvLE;cr?iTN4tZK1_Kxq_@#vF z*T_Fb2vB$3szfn_yi3z}qCooh#D3fr%H21EC`Jrx;M$ z1etjqkRJMp0NuG2^A9;z6HHab-odlxD59@-$a^lb3HTQDE-YZ4G;@dpv6Ss}>EuIA zn~ZdX7eAQMbGRtO8j0`0qxvE~TCA+Blbf?$uj$b;i$zMW%-p{#ELGraiV~Koh}67M z1NRT%kh6NZY5!&ND(PYrw~U#Y#L;ZNb0>9o^=FzOpRy9X8Jwg{$QW*$6Rj%ha7bb* z_gv0TsRGzuT_gjp6}UYUK3P}Y(gpa!IIp&=IUWXsq$R=6K=i`vQroGOdIkon7Nep+ z%C-783%;)X$feY~)L3xf(K@3McXO}`0rrirnf z%o{M(v%!1}itj4G5PD~41aAZEXB3EDuvv&FXy2a}rKCm`Z4k1vpSoE%Eu$(bNud98 z29amj&N0?Y>jjt}@u;F{0|A(tGzwqkg2O?R@x*{t+*Xy|iY!~5YHQ*az~ zD?wi?cvTA%4$iKxwDW&QU6(c!WPk-$#{wfbi~^7Hd(Ufn9`G8l95(S;?Gi(u^+ zu{7E08#ZygtpD!Sv|Cg>8l~HCF|AjlsRIni*6~MH*!EZ%-sxI{SI$QInieX|&R9Gj zki)1*3va<3dtVYfp86B9sI}tltpmh?t*fg#NDp+zp4wa;_NYs~CowdT8V8!EXt_9s=6O~vP)LD+O(X-7l{4r9YG`PCSmOI1^Hcx-*cpF&6XN-IeK@%~ zo1k$PD|Kq%I9X{LGA18Xz@`2j0*E9=3!ehNRq0tzN}|;Fez-tSn8y8;xA!CZ>oypG zjY~k1!<0yoAyuj>=a`F`;Umz-*A6??!5DU*ZgaNb3^1M=ojD^DFZL1ebI8fzvIJ_N z#J@`IUcpvSb9&B#p|DyBDldG_xn})tg6s`q(TmTH$=POP)aB$xF@c-ShXNxCI?#nP z50dFtZF5A+T3^8hakVlblk;&=gp={YL(w160zZmQPfZDW#oOyXEzvuea#cUW z232KXG@}Q17LioS37b%IEamsfS84L^e9^aM2($VQ9K4)W-bzAwgL;W1nHcR&ThALx z4p6;*u`A_jxVCJ9-3}@@!xH~9@}rm$tB0QvoYm@jD0P?}Kt+4+DVpm3w-YB-+%4Nl z{u0a4W){<(0#f$39a-3J)0}WL@S2;e(7zM}ot{UluzMETliuSi*Yaz15EcGi#om}jV$GFuTF zF^<8a`+ZRfDgut;b`UxTpXTw0hF^R~<@#>LHDSq#6bV*cC_!c^izTg|C!+1;NXfol z09fwhJ|}b4LK$K|9W2v2XCm`x{yjJ7%6mrd9)^35T?r&0lMt3Di z4^lyG!s8ei0FYmc(~cLrVq`6-v{tfVbCKA5mQ)76^|XIt7C&F4^!!_yqrLqa@Ez^I z_lUt|o2pNc*(rWi=4`j?QS;tFeKX>~KK#)ic#Td2aFR!ir7WMbAUC}G*4C3W)4$hc zjfa46f;BK*hxb#^ou>4Vcz&b6jQ6{#dyiH`kuFWyz?zV2Gm@?8WeD~Xc{Uhx*J%7y z{`x<4&Iu0XOWV@_-26U08M*V>-Na=Z`&6l6z8GJ+Ztn_pUAytZeGa%nXmFNu7T-Bm z&8e-%Wpbm|^hY%;?6LPQ{6zbttYkFt{~t^fJz4P+iT%`hOwnQ?mT%$suZu@2+jE+m zJ5VcIXJ-rG#lMd^Rs{LS+kbyfndC}@js$E9{Ya!+1iq~zva+(m)&AohP-|{(PL{jT z_|05cfjWno7aL3`;bK!3aA5Ld3HD)4<}wgbRh4os>Wsv(SQsMJrh>y7C;5N=mG(kk z@;ApY@EOY_vyjfXT$OYrK#u(+OHp7cn7Rh$R~W#hchTLt8GCF|HD@Q~P{&hw&Nu%( zj#uRTMitDA7l}ppow%IOvN+>v?9gR2Ry(XV8JC9K2^udMPRPsBz`n9GPTNJK$t;B7 zuouoEWV=*!*%m-28o#r%LyQs7?K8@_riyhwEntHy=E#Zrenw@k>lOo@*89NckeHoi z@G}-@%l5DxGq4cnlvn_{Zao5tp&oET@*9Hv}8(-XxM%%^N8SdQ}Lh?*wT zz`)~YJNCsq;C`@@NRwVFqC{qzdMR14V2p=R4)BRy)ZzVYQ-PP`;o4qVcN}{~LQU1H z5zapG9oC&yss;55jpatiy>IZ#+NOGl^YhwHNgEo1lXR>6lWs25UDlRosqQZ(66I1| zCO^PAE}C9-@gk`%DyQ@$wegREG$}N`h+%ZB-J<6L#+p!65RQ#Zo7qDXtmlirsh3hl zTudZGohQsnMSh-Vq2PCse{NRU1f}s0!`<5DPAb~=3H_|?zaL8c)6S~=6OeD`R?VdE z_5H?TiI4{G$RaYfvF^Bes)t&wfu}A71B@wh%k^|ubCt3+Oz${>O*<+S&jXRd?KEhD zeH08Bc_wu9p+Po~I8@t2?JFktph0Jc;XUs1ElkXQ=u+n#``g54ke`h0?zi&?bY1AbLNs1yz_HaOG_Rh50y=bMGr-|ETb85uBK_BqSl}4$c-{lq?;%B7 zskKgdR5IV6X*vW0BB1oZGHRwGp`sSU4*a31-$`LWKjo8>@ExNz~Z(h|4C$fn1jYb z8!29L#RyCZ;YV@v5kQG;0pX>CkL3r2C zbMl62j`de6R0`tR4DZH|tnj}H{TQtz9jCh!f8|xq;0ll+5ir!#<0{S@)h6OJd*rwl z{d|Wl16aAuYplKJm=>U}Cqh8t>}GzAxcfL7UhZ(x1>!;w{E%d7lGfY>p))y@qz1JF zQ_KCoUC%{cgXb(=XiyY?>Y~cqO2xY~7gE1y01D@&@1P9i_%Y3_eOzf!On|On$gT6X z?o8xgJd{|&I1F*Xnnqt$rUk4*E5FK3_AMB?__OF6^fm%?d?ze_zMH2B@!1)}b};}3 z>ex~~gtDp<++u(IgkkgW>U}Q$j0&qg59j1(SBerYadPc$xustZV4BoO_hd1b{eB|Y zu>uyMGG00@;paQUZbR;`mfVE{7gxN|4Jq-~cf+PBc8$Y&L1J+(?@PK+ia$cRm)sjz zVy!;r4WN7p(Gl!f+6p!ybi|Un+O$h~$$ss2T~7=t^Vw#{@=SO9?O=4xXL}=?8nURL{X6h@9d0!VuV43(xc)6bxjA>^*3877Y*oG__0=X#a!?DGvyMq5Vt+qgY68WzdEOT|8&2fIw{KnxUuXvSwJ?XE4PXF0PB}AJ zKS==i{o-nB7nOM>RLqY+EgW)wTiZX;M)H+1G$Rx3zy6>!Z_i$5GTluE2)ihGn{pk~ z28^aDx)hx3UC|~gtP009e<;Cm&FkQ8`-asgHe-v`#8J!9VAKd9Yyi;s<>JtiXtSOx z5k}!Rmrnm|A3<7=Cm%~^nzEY`hMt46NNhOQjq#`EbWbJuM6(39qz=TghlQVaY8crZjj&Xz-SC#)fW4--q=>QQzkduxLbeCF^3ef{HItg z0H~-GBN8|A_4shj1N(r#+~(~EZKW(_?2^hsF0 zUNGp&AUR`3Qk3AN|7`9{n9joh&XMA>7{74c59*$~WiKE!0}z1)ykpkaKVTgCb>U<3 z#@R;Q-_?+9I*{Af?5Q}}r1WVglW(Q3p|$r z1_(EwgzrL!YQpz84bpHvY4r#D+!YD^AKO`1wjh~MU|k&*p;5XW6gSQVqayf#{ea9F zQCFK+RQ;!)a3|&Cv;Q$5u$^CFjuzlbmSplmSS@OiH?zSsVEHO7qego{00K!43L<{J ze)rJE8iz=7#4p24q@n!4a6o#xuhtm;0j<+AJJL>iRUK#hopEa`!8t$yC93r;5IH_Z zTM1F)a+Q^oWNSJ~@uSKDNJxINM1Hb}h7R3(IXEbIGimocA_r!?o(Oua)=0b8zME(K zLhW()k^q1oImi{%)8{fab-h35i2I)8%~x?`7>`;SrOyaqs zYdMlTZqpJPR87`RG;d{1s*}y#6P3!t$-{m!PZPCBB*L>$EV#S-9^QWWdNC)*lnS9? z8G+yGWRj<;srk&_{2sQH4$4SRA9u!P@<7*`g*&N(<+=BQkCIZ8)s9mL1!yyMr~^3f zbbdW2-d(bb&Y?udPF~yx0;h_%jOgzoTuOU#~=EbcYSR+TeLXQIadHH zAkm|$!~IkG6q)SdxWQ+;)>7!l5ZvF~7Sp}oLEIbdF9#KU=w$pcRpF*H@50J%e;;Qx z6JPPVDTn5?Rwu5;5?b$CYz^txicyZK^trvm2hrT{|3K>Kve=vU>t-ID-Sj$4O^IBw z!4)lacT(PZFS7h{{#&7@uo3A1kFT>(JhQaeFhuHuD*J|e>fRBWa9{7nH`LSn!o!KV ziYIvHd|4DCO3QJOiHv{>+;6CD5i)L9N%UvBzQrSvqdILZo7)raR^V$kKUn$c?fQ$o zVOYokXD{eup^ZXWd8pv`p2KBgQiilPkNF$<7tZ{i$D_th)M%1A@p$H)FPxW`r~=tk zNR3b8l_ci74Uf%kuFqv|WrWWmhtF?}%G$5%+Cgv9hCd0a6l;Q*Yo5b`h))pS;OM}0PbWu1R^kI~~;9jYxBP2Iyi;-?SGEG*@93qZt_fBGT* zCDG)m1GB5FgRx;|gG?&8?5?iBKLWT=Ld|e8&EXC8$b;K|>dApZmY)DlxZzO+ta%db zfeXEcu*C$fmMtGGJe!ykpbzi7VLx&LoKKKTO|WtHU=y&-JOp5U{wb(#*!KS4_ixN* z^4tIZt5(Nnu-qc7R^r=&#j1ab5q%;Q`E2iQZzdbP#Nf5&5&kWWs{1eeRu=hT3hS`e zh(hi@ks9#`Bi^?QN9r~|na^+oPiSiqk?!usGNsYqa?srN3Fx;(^Wne9;Q^XNf|RZ~ zHjv^+KpA8J&J@%q2ad2#M&M8;%^_f+2n8k)2YD9M!NDu9G`pd(^Yfaj_cUgWq2!F% z#;q{C^xWKB>bO;z47bmpl}vg7e6zKNPN?5l;~aboA)U5pcM2*;IQO7ox}NWINq!v_+g-#Crj2Vi9u4@5#l zjHft|uRwI8#pht}H82oS@R_d-SG=zKC@d}lUbszN_zoNhGN@*oGlNi)zxQtD_wFR& zF?;szD|82|8mgFtyLhHMwClc^k6$Pan&HR@VF1245l0iGdPLzi-YD5v=~>?p>^9+| zkp}GngN*VVh9Uih7bjPR;-BHOF+@7SBPD$k8l@XXOlx`3#J}{(TTdEK%8=Y{(v%E z5S1=ecF5UtA%E`IsmapW)1%J7u_O)RrVMAFtU(~SQ1`!1mdaqa1_j~6UgTT}%q6<1 z0c1r(7>Mr@$6y+OmQwElY79$n6YoR!}2sE1%T39z^4G511Pc zs-{ckA1v2Bz5>`*d@Bxi4)PC75%i#9uf%AGe3DDER(%C&%*G)B^cV!u2ak!KAXCN?cP2EDt%VO7UfzgN^F%AyQ< zFVGpf^Mzf9#Rz9^P7w6y%2H7ONz+A;V$vO=UaW^d2tQG;btVlVS7OZwjOI*iZQm|W zJZCw{S%)uR;xiD1Z)JnN8|WKhniG57yCa`Fzi)lbp@s?Jzr`vGt4L{fZ%z zh)?qLeC6^*=GQo3lkP62I|R;jf3<&KBiO%`H1K$lL|S=0sq)PyH*0HRI&<@9F3LNs zhHaw3z4$^gt`w^$ioWq`B8KJ{@FW8Z35vKim@_-~Y>L(U@FU{xSV?kU^m(GjL@q*A zG2DjV!U_K+ks*J)VjjNN*E!J7sU7{Ovq9DvPmky`yXuH%?^fr(yb-1Pb=HPMM3R(x zQeJMl4n}|iX2^(x8sz+Ey%#fPQ;^77##-;(BLw`>6IDhx(eNbkb((c2E2={A)2Q)W7s(k?X*)BKlx$Qq8sAO0^!7pY!Ona^htt72%oi6@4`K3loo z)}`!)yp5}gUc>J37LBLpSt2YL0pW=HrJwd{xyPfqs!KpzS3(H+wLO68igvdiV+RYq z&1AYYsQ{WBjrz65Z;HDawi$kfqn;a%eYugI9}XA7C7V%@tKSl=US4eFibEPe$)&p1 zJmlnF_By)DsdGT7BSyoLI#I0si zt=@-!kGZ_cyZ^0(9p%<;Yo3VRpICrr_STNIm_bd7GwQqsn|zI`Lq!VO?n|VIc{~E! z&ifnAxco-haj(XalZJZP=BA%ST6hh~RX@C>wF+m}Zbd3sSUoOOR3x_G>c(iL<-2X- z?zFL{B=+hqWldRfEhGE$(5BXVjpLa4e_{&vT~t5AGY|9 zX8u$&x1k$nOx#PIG-K%zOa^;f(qpTqhK9kYJ<}^Ku!+GX9X^YEvNEi3ts4v$BYvh( zd9S`ESS7jH>0KO^H&vE$wSH~P(ATA)4K@kWa>8Y-ASTn|HV6|7gGP2kr3(0xmf5Z% zQjwM0Qz|u{&HKGP`U^#flTUHDQC#L6h9&ZBZ0!ax>Pd(Kbt69o=!Rlv*SB`*xkXZr zQ`>~GHR*SzE)m} zgJaq{aUz8jyvHNyV!FZRqmrPht}3GQZs=2ntvAb6CJ0BThSFb!kI}JP37DrN8L5 zgmMdhAWZTV?pnep{m8DYjB6P_W9#XFRT*F*ar`XeJ(nQ3eGL&EFHlb_B10x^(Ki%gH6x<()z6zgUen_Wb^ zbs8#`)SSLMu92%GXm_T;t|tF}q4C~e@{zK;>8CkZkh-Ex6LY@jy=5)N^deuh_lrt| z9?`1mI|gK!>N}(Yt=bENq%KnneA`V$@NQol)^||dnV`Dgm}lHy%M{~A8_jh{l=ZgM z#ephIya8aXF|08ktOWkc3&Y|I596CBDI$*aG(2ftWIn)Ajb^$^=wE7l!5gI)d}WXs z`sdh>$K=c7LJp!cLNdkO+&?3o*>Mr#E?9KC-KAMXDUmi7tGB;=IQ%C&g4Kjl*VG^B zYzFP&Zo` z%*zA&Kp|a30~kyEPLe!sUBIB)=OW(6E%K~Dg{#zU54`&ed*E7(VgjreP7dp+}GL*Em zcN1z)Oe{X^)H;9IeJOE#$v{3FcAqS@gN^5=_{?*ZUzl*kLWnMB2P7fOuF<*3=kE0j=L1wzZ@xdG4Go$b;I9usz}$RKGVUR zsFQ!nDTE(qDF9odF3eeK52JCCU-q>9_2ej>RCTv}jp_bDHJ1j- zWhdNuHzRAZ$>$J`2#joC#wrXN;$|_$>3n8m_L$7CPaNi3+$WX#O@m&-QzvTuWwN;- zsh+lTr;* z$JD)Hrf5m8)yan`Vm?L;6{CKXU;pq7@(9(oD_Otme4kLir^quc0e!Y$_Rw#HA}7}I zbTe8ByzWnmwr!X3o8&I_s`xG@^wX4mGei9yz1R2L2VC4F&Nr>2uDO9?Yl*Ef-p6yn zA)J~vw&z4gd-Q2uO*lXMJgI=WBFZa1LVC`FZJn+A#oCU+H(f|}2V^mRhEn8h!{?#& z42MPGqZ9%*eiesA(*2K8{6Rwxk|KGYM)`bpF3@-EojqL4Ap&XV61BdD?z`tmg!R!TX!Hl_Zaq;qFbXQ6;j*BFmhds{Pe8#qHkIarrgCNiA8uWCSpSx* zrL%;W{aAYkB9c7JB{R8b#h*PxTt$wvJDgYF63&f>;~e1S=*HBtj*jb!{tB31axwB@ zH7Z9xnJxVyRu!S#vX?F5c=>`+^!!Dm=QSTG53jXB0yb=eR2R1=BbC#o-7P|H)O|n@ zo?;<9S5K2#NgB=|6}}_p4CuQG_xqX-`w+S&doP7oz`$f&TX;wFkP1?)CG70Kvt5#l zM8EP||J4&G2tI@k91|Le;m=mP5Sxs3%uivJ3!s)ox`Ud_%0|`$M2U05Kw4 zD>-5L>6snpJDqc<^t~))1(cnFi5LvT&W9GgQ}^;R61@a5mnhRLW7Fz#`YK~3s7@&K@LP;l=q`j!o&G939mSz10ynMhK`CmKdZS zXxEr#Hy%7o6%)?o`HPoHL_eaN#FO19+m21jAo_ce-9*ZN5(0q`u3oJM6G7c=NgPIy zQSUrG_^Y5WB8l(1JUf+j@ImN2lJHS3sdEl4?~!b^xqn4|`Se@fo-wz?SJ`s*xp=Zf z{1ngZ&M5Gqk7)+U%BC^T@d|c&bRMw@LH*b4gX%v4wLC$%L{ShW0B_TXw+)XiOGs}5 zW)M-n7SZfm?H@8Uu}xv0c0j(rc-qneX39s(w3iO}Iybrrm(2~YDknBM#nVqOloowW zH}#*Ig|DT~87X8QW_P#&2?+8!=be%B($hz5yud0E!qYfnAqYR2z@WxUajA8?px*2c z7QViIB`Fj>Tn0SB7(&Kdo~VJXZg_}}*y8gi{34E`+kh{F^wWEM!L_$YH%}R}M_p0% zr4b^LpuD%1RqU&g3CPqay>9Xkwrg*v-hX$I;^^07UWR=);lzhv;_e9vntrBm z$w)5tKwE!JaEEonJ2A1y!IamH5Y6}qW|vRGsnv=0r{LU3u*`3}7{B>J+7_8%;@|7= zk~HH^jnu5U%2{3OHeL~_6h(1}?s*V${u{H8f!r>{yE}Ha*7wK=xhBrdkQ|bdJU*wp z)yccEv+kBtsmag}!IuONlGMF3CMwq^KW={fgcJ3x!hmCz{X22+PVX#WE=}=M6tg+u z=eY@$K;J4Ei2OPjRQdU-K&wqb>XcgC>s;J_n5ftdRyN|f*EQRe(^8^)sMWDDbfwyj>)QXpG$kvfV$Z|#xWx>mN>#U3(#poC!f zx?8Iiy?27^zLP5g-RC3SmHJj0t5uIS+|yn>Q#8KiZ$x{{hv-Rk9t}!{Fo;!~VLEMH zPkSuN>ZgNf5QJ`tkc;qhl?mF^S+&)x314!C^nu+jzJtsYR0 zM7%LvJ9_baF5Rzpx|Y+?i9og~@W80!jQCZ*K+-)>q6^5&(7xs((+Q{K z(;2b9vrkifpU}Mn5bFG&E<<}Sj6R?j^pu?_yXf=wDcjT12OE0{8Ua=yj7#g_I4t1b zR^0`q{{Rrjwk$z+ix~n4;u1JKf0W(CNY*PbN;Mr6gqnp21A{QCr#v5YB8RwI<|{@C zhz9hivIP14zpsD$n3zZgyE*~cLZj0HzQq28s&rr$^mO!-$~r zyLwQpgb3<0qS0Fan#s*LI+yfKtjp#eR9@aq1w8jcy;P9-IrrAd6{+^m6jqz)?5zq+ z*Tedu_}h~q>qdhLTs-M2^H|q~n){P>!dc z>#58-&K^dP;56aAy}u(SgNjsHppU3nU~f*KAm<%oesN^d%%!5XOO$J@GKXk(wRJXIspQ*8=d2I&MU`d zxU?~P*;z1nHUsDW-c?k)qG?5vsocxv6Y$ThC0NuMYLPm>QPE_*Tfh{>Nc{S_VD*cccmQ((G_ko>{C*j^jK!YPlEAjfJ0aK(!qpp`MO+|JFl z*gv%udnHgybaQtr=DIp&5)^gCCbG9?rzEkX0Z)GMnzy#-Ek8{ykN9CXxN@(g_RMJ3 zdB*lUK*(xw+DVth=KKmS3|BASf1hA#~CFC6&{FoS|t2Qf~P6i@vp>FXENQMOVpcznWeA`lQjoeSZVE?;{YM*OC{jF@RHy;NYJpf(W4C z|0fP`t=!)9_@C8c)z_)tC%U$zul_|iHBPOz{VzuG`BF$t9nrJS_D-H)7#NC(?pI6! zHYE=ALX_jtOAm%px^qICC|n|@?H+E$mO|uG?Wh~yh6AsZ0?Kv|f2Fa@zdkQ%qyXC?&N#kHc^rA}PiMCw(`6!&A=Qk87Us7CEIKyp+W2*_* z8bvyhHt>$~{s*n{l&l&W8{HAZ-Gqpd3HeQ9)z12*f}MoP*P*W;{8EDP6_=1U5##<% z$Ahf7;5REWX`w%E$A{OyrXoF=Dhd?1Nag!%ggB#!RDqoAcATG!IPXqpW1R<+jM6@~ zdM=Wb^PVW<9&%iFWqEmLv31t)KK(O#%WDH899Z26l|%Mn=H>S>!>Q4LjGe7%u5}uq zVz<<{(wWXG&YB5BAs5s`t#@3Z8W`s>$8);3kh?(izMM%hRM8cXXqVOh{Rj%6!98|c z#4}j0^k+`wAdrsWn(RI+3yH_BPr_Bb`@$zPsaFg~qW_s9_4aOIwLcSY@IcwYve+~+ z29{IdA)33JrypOGz(2uYKvf{pv_c*H4OM&UPAuaAl_ZlxJ8)Mzc1@7i3BOkB(^i)Q z%cvu<g~=;OaF5$i0e`vA$p^K)*@Vuig$ZEmHu?G;bN|Gp-W8{#o*d&nT} zO{$=p)|$-ZifdPYp?5r`!ZLU-=y2!b`6Zs=KF4ob``R^?>xeyfrmAs;haVmw+-L}p zOl{~icUohyODd!}_g_*fLNSTiY75?5v`ak`D*UT-7U9D$(MPYPBd-wh=AY+rCn$DW z)>4TV`u08wZb|Z*P1ak*AQpWt0gSK`=&RXGwDJTP4+Vn=M|9zCE2{BUu&4@5-ZL$+ z+?DXFe*U#U(D&ho$Aof<^S{MopWv#zH@MxvgdETqdt)cF`_|b%=}0SNcQ^>5JWtaX zUb-A6RMr*P3cB+7<1m+raa($Lh@|q4+gdHi#Q1_*ecG1tuPQjmuBOAISM_WRzzG!i zQxf)(E7o>o0fQKyw%Hbs1T&BnkhNusYm&*mM`sY6`n!b6X9n2E8Qoo?Fz!o>ZfC~W zdDeaZ{-e#K{!1Fa)cl34J&_MX_C6uqUWmxf{(>*BKkx`pANq^!)ldqQ(~W0}9Lk}C zQ`ox?AGk$xMS{s#!zT1C<;(nscD!i}{ez*_Cq$>gy&tt_pOk6fQ!B=+cI_67f8AUS zr4WaZ=ix5#OKj*UJ+x-co3NgJ%sg+z?^QwgilZgLu^Xu6p`DP8fNNJhepp6qVlt$jjQ_@xMG=IpA=$TUP zW|QXtx}f#RQ?z=FhQKRRQkYA>$Ok%>~{|$z)Aw zx;D&PezJDg^IKk9gPmC*9)>Jkx^6Dh;YHz8*26QP(PBDL{X&`cyx?y&{*eR@2-zwM zR>W;i;%bB1ndOEEE>&ZIed>yXZ|Ng$*Bu&C>s0~h0lQ~?DPM$Po%3^MWz-%J-CLYI zSRwZCHrp{G73BB98v}-yZl+H3FY`mCmsb9k6!}rWHC<=H-dcWB06v>PS^;`O^Z2RT zofg!>`u047J2^xa@szL@F4wdD* zL3dM}3!f~x1;GkJp-aWd%vYod&dE50Oz~ywOJL-%9gjuOv7t5FP7bkiIa3e~>G>A1 zS_!i@8fVYYS#y6YRAz)1_L8XvAoVeFk_o7-6s3^USzGdr53`lty-ya)iSxm9(o}+s zPF$yB*1A%J;l>6AZ6!CzbhEb89TAAPQRjAM*RMyj4&-_FMKX_dE*+1w#P-urO%Lg| zUh(H|dU2$61{hz^tH9#R#&n}XOqG*fQ2jkM8e7uz`ZtPYVR<^CQFILo6H92OU4imztu>lo0;0h7Di0N2H0`X6AbBK+t5))cBV4f?CUa@JtOghcFF{x8;WM5*YUbULt~;>Y_~>EcvN$5 z?H;k@ohc^le{(fG#~5i7(n7t6C7jc#Llau=RU{=OCUsU7R+&J*8Zg*U|pNEpXy4UxfmzDt5belD@o0|X)i^ch&x@(g%mkAQS0-U;mo=OZ<^22SVmz` zqe~q|$)g7PsXbT}uuT0q|90G|n0=4!xL;@3)*b3ek4755(LT2f4`YqD0KNAg{{B9t z&H-Do(Du7xSW#di(d}9|NQ+~`zzYHiViIJO+>3!k`$2Q#0_-~K|b+$(RYTmAzpgC{39RJ=>bt^&_|2o#;DLrRj%F)Xw@%2zO^L~Hw z%vFV>0%oz)ADht(iUaMI6?()JhoY@zf$3SCFOIgkS5x96zcwwj0a|71&-8S30TAp# z$K#EM{l*AQ*#^?cvk!S?z1haz7Ct)nJvn9)5>m!akG%)!u=Zs(pXp<_I2OZ)Ru5LX zfU~@4FB@}`S0R&e8(p8S=5}J_KNU_BmlU@L-7r^DNtI;Vqn9h!g)D-)lnLWAs@wS@ zFUnyfZ6x~a4^P$~TG))78L&8~5+sflR#3n7TTgm5`tu$%MdeRAKlSpY#(7!>bOZvh zWPJjeC8X`!p~kIfBT*Drgc-p}9J2trp+s_fS^=j&-+%HD?pMgq(8S=Y+$0ZhnyFwJdlvnTW6Av%hEl7w5A$zO zv-LE>yHiB=;>~`c1b}n)+{KZQ#&OPN-v8{jZ?8@NZf{SzrtUjI)9u^qHI!sTWld;C zb)ZZAuHK_*?1GM_%!wJx9*Wn~~ULNU%-!uvynXl~vE1J=%VvRuV1JIu1uD z=I?ILR+gGBxqGaB2+!VuW+4j=ksVJw%&R|ny!QV+8Y9?N%m5^R$-H#m@Vj+Q{b{D? zEIUCYmrm|1?TWdq^hZG^XA4aDnO_^jSG9^`9U)U>A2>E(=e5UlD9nrIionyk%Z|!H z)L?T24{loFD{$9-PEHh#25nh@98$m9t_?s6$BE*l z9n!I~5PVk~)f&fj5_z?@(I!@oL_7Ed5$q!%?CK)C5F)D_YnwjdOJZYEPwT=Gw#Z&e zoUehX$vE5?*fY6sBt9rr2zU;zUykf?M~VVAI7$wcyt>kWRr>avp~azcYRzISyWNzG zsc-}ZwH|mW<&0Z3pU+R z{BSI&q4w*~YwY9i2Sj1Lb>Ann_Y5|jBR>;8iGo;YH|xyBPL>GJSUYK zy6(FJn2xfFZ+KF58enSkGSQd>-gL7oYAgvM{vRSg2#`&90h>yOoUY;g-jBCr3=bhj zbLm%XZ|FXF=6vQUZ3vC$={afib`j<5DqAKjMPIdNQu%%AJnUbSkTZ} zGJMgKN<>h`3k`1}_22e(z^~!qvq*fgK@H8*;j}_En!*f+@WEPeXVa6WLj2Aixu(ER z+zJ+GQ$v~F6sd85&q^}x$3|SPdNu-S^W8#UG;w4xy!N7gmwW(Z?lMJ+NvbqzDDlsj zgK`^|DXMOd1bQ(ea2Nib$=6>gmvqpjd12q7?QexsK;A~e!tIB5AKr<1^@No0bPIo- z`XSt~#nWDi1KAP1p1C{qm~aE984BBQ9GONbF%K>fS3rV3fgZ^%Q+9&rY5g2T$AbEflA3 z3L)+lEf$l>f}&yDUx<_d?Z!L}B9$x54@H_<4#bL_XD>j9$byHa2!d zbefyR{ElCExNl{7US@Iv#ztE|D(VZOvFyyFbu!V~Gk!c6-19s|%u5e@QXp4_8;gP9 z6{yfKMNEu;LVEiH z=8hLHUy>N4Zx+pBPN__D2~V{KDW#n=EOVNxJ?`9%X}jKaYW@oEANLT)XC&&qM|wz- zB6oD19jd1ar3Pojs+5cv70*i>lsr!<<|`Sa>iX?CTZ-*-IUHfVz`@bECM^4{QT8HN zCG{i046&?B^O<`}1@D$QJ?WwasP^x`0TFs52~u?UqD8(baB_vh?$4C~y6ygzks4MV z&Ou`O%j9ZHdLluE1JHx~(%c6kFU9d~F+UMF7iI%-ATRqV1b!|r4`;VtyrY>d5v<~s z{@Fp5hn=kBrd!z6^I=B|Z^pPGTW$a5zPsK*qE*~i}2*qLWQx>A)sy#!@g@T<(-s=mqUCnP#un9J<>T)@!Xwu`{ zD|)ZO=Dn%6+02m1dZWO|nPJ$HADaw^i?KD=?;h`xG;j2FTzmi|$6pyG3k)=2ElJL9 z2S9P*NB>L4xGxeL&K|fK2sQL)0v(!c4+Ji$R3O4H_g!(<&q>`}bxO8nh2S?R$o(RHEE-JXNf2`9yIB!_fKq-!;C z{pC$b>+YP{`A?rCx|*AQQiF0E-rc$r&~--^-Tw6~TQy@ZOZmMUrAww?(zO%sP(O{s zjihAO%CEC|z1}ZbOkh-R##EOgZm)dWdJp-H&ZhUIAy7P~yGi54q|N6_%C_w*gOWcR z*Y9ogCQ)7Tcik8L8~z9!9pbt?MqwmU9Z0Y{{jIYnbh5^TLh+VxJ)4~>{tQQR==zg# ztnuMrg<9iVKAls=vHICS+5I$p^k#6(tnFg+TE^A>^m;s`#y{O2PzF22w7+`MC*pHz z;q&O9V&)(JkSXUYfZOuq1gEBG;-1R%+QpgI!kGymAc!A-hJqrx=&ry1FM)Dt{$GUK z{;?MG<0Uk){4-a*zZS&wY2t1;fUAs_fkC5r8*l)0z)-s_+x-ug**AC}?wl z{^sb>2b5-JldtQBWisY?kf~A;yVm;Hmeed26xBRbQ7^Bwyqo;rd%qV&tz4-(+0Aa_raF%Llr9ZHgyr9kewD%g*cVL_sze$XdHD(fjD< zDyiZ@-qYX^qaRo$#+O^NnM*i64BF@$Wn7JuT&U<81Zudhj-7Y^=>D`SnsEoq^^Ejx zt;;)6>}*YH=y}yUN<1hw54N*+ySnmGDi?J7`LopLAN5;*>bNk$Q(PW5q0_wX(FxnV zq3Qr^fIw=n4WQ^VrNtQX@}C-WXrdT041KIWE)*ST7iMFMp_m6YOr%6_^b>9S zn)k@)fR@d7AN%jPEz##08sQ`n_sz0>R`Q*H8xEhxQ^#$cy@a38Tha8!BxIC$=9YWF zFI<@2)t@-nsOp$msc1N?wt#93L^0Y!URn!nJoRnOM~bOrb&_Dj2(Xq&;ij=9&^jY< zfQ{2g5QAeE)PUB=EB^DO_Tu@XmG3te`?X!9X0^%O-_lbS^>gt$C{}q^Ot<#5PCZ{l=NO)K@x65qn}pgUQ2kYU17*|o-G$-x zal{=Sd`>hp+1W72>o5G(Bs4L1rE#y8o_bTU=tTkPs-WxAwl5%ztl!cq8Bt~pWAa&H|4|@+ zTZJk8_w&}L^1mzRzC8a=StAEkIAohuisVqYFVt6OJ17rAoe_pul^Qbfee>7Wf@liG z>UoOm{&K1tBPJF@Yp;w3#E+Qw#tcqGZ>!d-m0G$-YHKcy?R~Mx=w%r6*Rcc;c;MY$ z@U8aG04!b;UGI`{`k}da$Jb9_4Mw7&APx)EgfV*Y!U>3u-n93Xg$ZXDRw0Nzrpa3C z{J?U&%3}vppb4u7VVwMq2(FhvLI6NVl)7PudJ)A z3K4uDv4ZMD$lBe1@7-^{={4UFgaWT**$jHsf;RAsR93WbIZRFyZG&T2bW$cfG zT@egjR&K^|ATE*U1ppu)#+(4vtN4!RhB z1%IoXj}o`qF8NKl3sG5oejR^XeC)o62?}dhto9;|y!K;_=i^?h7Dp4F-G^u@yMQ^zp{esUwq*77Xgd%frYNpLMHN(3bjb-6!HtDW~NP)ysyjzC$ zo#Hw#MG5lOi6_Ss3@N$+;B{pWVLV>r%~OL^-Cpy_gt zscZBqz1`5mIF!!CC@3V|i}9xN!FzR3gFsVll0DgB17kP0NdczQmo7ezv(x&m40x%1yB?R_-r# zH-Z8r>2Jj$hd2n()Xm6x;vVU-hmoB}e#TXc=LyeV#T6a13-{`dkRZ9Yw$$d|IHRv` z^%lmKXX~UcdQ_MH!KU{c&l<>@4(OUP?RE?Vf>8?9R_t_NKjsNZ?SlsY>2uD%g247l z6)UcIiO$z?{#EsGnG^l*mbD!F=UTp>@LfgERd3(wP%Vw+Ylr}DJH*_fWb0sHBT$0L z`1ywF{WN&uWnKp}VghqEvxeG~e1NKkD)TP0Q-*i+CFJ&Lf;+evR{-QR zG-w!k8FvsaLGUH3zH?!N(ff4v@%BtLnT<_hz~?7+# z8Pe|KuO=o|aL$DbY&z#O&e&y1Z%!8NJ0)?`>btT%koYxH;E5x}nW;e_TWC`Ww&Uvl za866=2`3e14W(#whN&=%jel@Kj^d@Y!Aj03ig=qK-m8sBE&L-z6d-E|M(b8eilWw` zSe#%!2<-MFuz4*cl^dmg*0(pCq9!4B&4W(f&af-y;ca^1T;J>VtB;;1nv`80<75Qa z*y7q=Z_6nSPBs_L5}j@y9h3^U?8CEbxUz#DQdNGXzq>ZRez4BNlfkIUDpBLxbUlsQ zk634HCYZmlqbeq}znphjj*9T4cG>n4+(qo(Gq~A^5%D+=>=Zeht3Ms_Y;N_D^Iuy0 zgodWPm)Pq%U{!DgSm%0xqcGc@bW9Oc-YPKoJH9w$ zrjLBhFl-(F^y^gc=-)VoMpqdpBj(MV^DMy4(hykNqZ__OzsoJ6N}hzFJNTk|*U;~G zJWnkq@q+@&1N8~?pQvZ=E2jgm8w~KS+>{KJh6)^=_X`XzW-;Ax-fl|W{x!M?XptTqLb%aPR7N z;8=Jwo|3`0jZ-u7^-F%d!_)|FdL2aABerJU6Pc?JA6mD0wa-MLE94w_$zBBg<<}=m z>iezBTbvkCn#CqB&)`4IR++o~!~zPhwkneXGB%I5#iTW{EqmT`)*dN>EgPe~9zTBE zk|`Hb`uA@bU@^k;&V#*kIND!o!<5lE88wRELr#hQuB)Ignkp-MRiURm;8)kfG<2jV zMf@Ekor+RT5eWyDB4qS_o5%G)%MGkh5^@>_`AO1>ZHe*qCWyPrw)yLy+9+qf3E2*b zYFyxW!3{BaV+5P4deuMivEoN~n1(6uMQ@*+VTOc7E~c$^+1*XSi6?Vo z=Uy85Ej~!i?{cz%xcs?`96vKruxRZC&mW;gWzcevLietH7m(!WKw4L74D&x>gIGw|Q?b&h<2kFvx^eOB8CpEXoy5akXgcwAV zJ4Wa$14BH`GvXnOS+!-cH&=h}x;m&wGjTdZTSU^4!0IF57z3ZN>jF#O=vKt7mq8%7 z9NILF?np1+yL-0=w% z8-?T2pZU0-OG$Zk<*qLa2$japZmFv52!>b#3`K z_IH>JbbTPjXZL8N&j~e)Mh(T{4>%omv`+geC_9s_qTJ_%W}1JBbq@DFdi98HF3sPSuK>Hy?k+a4wVo%nKg$yo8ASy~Fps)8Ffa z+m4>6l1T+li8Q|~r#v5Nk*gWAGDDw>Tq<&JZFsG~4f7P~Ai0B<9uMg=QxuDHpbPOH zrq3TlV3c}6F@n0w;32<-K5D|jLfS{;pfxPJiDd6b7;en=2BEWIB?%BlZ3_zeQgP%- ze#H&mE|3>-xk$YLY}Ze37s+n1%FD}ZcB*Ee0&-dk^(Foo*o(CVsR`-A@m3daw2SG{ z`hdI9O($Y_KsvkN@_*+e{E3aapGk-Q_gn*RlMk6SyQZ6KmninPoM!-gJ|`lTXX^wZ zYYLs8t8runusN0oWa;#f0y-!cTgQ)Y+IJ>g6|Q z!}{<{FqSlzI4dr^^X*iwjHVGWtxGDj49Vcoce;9mr#cEK^9$6~41%xPiveA5M?7seB@z&4AO7 zS6t{v{)=N(zoWt5JRh*Grhp*#QjNfV#-}!>p0Z2bz<$FDw@syV{0io|t+DxfR{*)q zWY${do(bX1y((X|Iol>QPoiMOS=P`??ASMtWH!&YGb|eW7<79jo!W2#*_B2a z=@i$-3p4h`fk}6Dq9Sv){_T_|?(;EQgLfsqNu!ypbXfg(F{z67-ie{^;U>8jD0Ukw zh79My(gxzd^;g&P1vk&$3raqbf+-C$(8J3dF%g{_JOJAsde*dOd(Pyu&$Kj9;!}yR zf}{n$7UQ`jCZzntm^|wXQ9SC_d)Qgk-#hzb-`E?&N?0--ZYLfrB>V=CL|4BWXrQ1o zg3a7)xUkkcta2`YR}ZsIYjMiWI-N<*E`BH#2OHA}@i3I)ogq{AT>14PHRdrdeCll8 zjiAatt)o0b%HQYEZ?E%i-_X9L^ZUbRz|Lz8@(d_>&sNl1*lW70q~-Qh61GlAO>B

QG{@1!G_Lm=YSnJ0W95hM}`4P=(`uFM8E@TsQt?7-JxCJ6dwhA$~e(<{n{J z6N%}++xK+YJ+r&DTWDmco^=YHFGNmdpI1knX`%|y#@qj9eT=^Uf6vg!Up{N&yqDcE zI40z6+@1g)4CLGtV%t@%f&?y;)udYU(x+goGSW319_S_uj+5!y@993hU!@l z@`lS^Lo#?ed++BlV(}|skvtFaE zG9!O34rjpupZupfzEIh~_n{|@uB5d6)cvQ5#(K+x67A6u-csYV_p+7C4+&l%3)c&uWZ^`!s*3=S2$Gj zJgP!<)n$ij={Q(+DGJ!?-+d}mw|_SvG>+~5`SX~lL3}t087!)nkhXWiCS>wbzHX)? zlQqQA4rnmwrHGd=&)gmC>+9o};t>tO5bH4gAGXdatgUcc*SHjSx1udloZwIj6{J{k zcZU!NPI1?k;!c55++Bl12<{Xs39h9$oUDEJ-sdd2%9C)(^Upc{F~0G>?o_1^L6#lu zbEfz7`=q?LtX{rfYL~zFS4U{OlMmA`3+!!Il{=*K=zPao$mf8`m%Q^+FrOklP6K~o zUTC_>)w6*5zN-C5PJ(*=THY_AD5O9?0V7r67t<|!x;|2aUuUr)7$A}E63vaO(0j<~E5KdGVlmqF_% zUSs0MDb5_eyWaYF7_eez&o>Bobco5E5hq%u4>T&xj$S8q;q-b2wz=DjuK5bt;u3VF ze;;9O|M`uBS(A7~%Y4th16^Y@<)CeeoBJ_ouFtJ-jBzu|i~T6RT*bC0o?9*5UcYTe z0ntX>mFjL17^8F$wuv8^lI>gIaoNSY?=?1}u5zmLb8-&CNh56al+uL5L&V;OEsaBp z1zXM559osiBL3^Lk1|ph!&J#lX=j=3E~tlnKYU)qu79hNidi#PMG83&|@1(QyK^?(&m0Qer~_?KubWSgL(K{E46X0?QR z-s|}~YX-t2^6Fl0;XCpPx~=N!&&ufYn?&kq6$AioeV>nYIUTMWle30_E&xmhVkURX z3tN9zT)W2uI@)Z#iw4GmC*Xwt(l7F5{|^(+IC3~I{hR$BKBmCk?cFKM{p-W3J7gN5 zL*m-R>*4ZU{kZt>w3xV?8Jn~5o9?yu~xnKZAc7Hg=sIF zjQQ!kThTiJCwY=unMC9>vi?!{gR6?w6%s7B_Y0qkJcjg(zkpBZ6L6yW*{_#!dub@b zu7!?w#^JHT3f=2SOQDvR9R}$pS4yJXmKJkz&p_C~-V`!6wHki0H}PSj z{qPHVbntY%*r)S(EnOf{Wip2Wv}Bcx83|7?Hd1_h8^y1arelIk7`l__bVIWu-amSyW*%IOTsKYUl_r~(BqlA-}x=N21J*t>3$5+!#=!_mj+<}e!W(^ zW)lMVVee_{!I;LH<^_x|8>6?%#Vwo3$vPOTDRSAFJuu3xQYvHa*KDm!!>nQ+*$vbq9^NTB;R9mGxF)fc5Q$=x{fby z0x!=GI-do&Nfj#xAj4kMk)= zP52H64}&*jqKrGr)w&;Dl|w&3X(NLew6knfr8=jFD+k)fEZ_OH(1C@-sRO+J@=(GN z*p7qN%8ouV_@Wox?ruX2US<{+XgO-O>XEuKmj6<)2MGO#)0~2H^&-?j!kD8I;~$P; zB=|}|ROaXD22!yCL^_Ww59WFIgOpKTart91-L3KIEa*%*dQHYzsx&H1aiP6&;#Qr) z33wA68SX;I{k|})l!-KtyTE2=L<&)Txbto)O>WBYjYm6sQo66*ANPjuo$r3V$DUo< zBcM)uoU`!JN`8KD+4$taGSNwzP00Eki2n5#&hmrIJf%);T@%ulx?p=7Tzy*JrsPL> zs%@*Wlin#G))1-N;vimjmhBRY*CZ*MPQJ&+ zeFxzTX|Kn9!x)jgO-QEVl*KGrEDC=A=x%%*4(QEHM{$thO>>K|@0VMJt*Y%3` zj}h`q=AT4V?2u^gq9v!77~=InRt^r%1_ta79Uo6-ewM($ET4!MA-xKbK%}!W{FZgk zmJ4Q#l-lic1;SVL0~$CQA2kW9dr`b!3ESWjxLa2kicT;eRP`NHSa4vublW`38Z;c9 z$W!`&FYM0K@*38PK2d%f)V3h8v36l3Z9o2DP+vMl0oz^ZmHz(fmj^jsD1)WRe5JBo zflF&edD3YE=@?%ti5ON2ipW}XvLv7-M)VHX^XMLiWrpi3RDA0i%7?hL`S2>83d1H56yXLPdfRS^o~N_nmn37mquID4yYZY#XTGq z04>cSKg$tw(b;RTa-?XmX(dX1bHn3jn%;VYAKr0S0Cv@K7oML>3uYwDs2_HJ|9Nn0 zmA$``mX?kq*_URzBa#3qTD88vBRW!`q@u2Vo1P~byEUty;fgzv>rI&V_?y%1Va>1B zX9ipW_R~xD1X);pKtvL+JuVbe{hN7Ep`{El$7Bsv6%()m#xz!RJo zhwjA409klEXNkKNXc|;KbP?WMuc0%Q@$)P?g|nK+6VC8Y=cDf*0T zs}($vAaz9jrvCTa?qnvKbH06<8|&g|lW|lB{n^?7+D`lM?;Cx*tggGCagzrcaj*#i z*9|(cB;f)`1YR-m;=|Q?qFQM=u?_t~5~&SG(tE0DDcxp7vJM{EHrWrKuC~0A2bptq z8FZpGMW(e0+FQ4YahBkXxXEnRzwX^~eXqjo=RO&9mC*Ec$gvXf^Bs-HHS)^{#)E%%{^U>Mfkt7Z%ZjSl3^U0G! z=XwQi);Qf|#(bk^uh63wtTs;wUhDoWOY89`HB?=2b44MN&?3-05QUBKGvRY>6V_do zcPYfe##u%n7*#QwdE++5 zi(n1Uy`;*Z2Yx!33RXg(nkLo7xbmg1&X=jjNq^0c2*G(mH*k*_EW_1Pj&I&loBZGe z_@v9Vn{8@?p5VS9XYv~MNlgn^-gG`GRRV>(M#8%^5!_yM)(zix^iY75eJvF}@z^M4 z^}u8mN*l*xD4g4*X~8#oRH8!oqx9*baAR#lyElL2mLZi2*(M33M0r%BGcJoAsIGoj zhQK@Mg5`R!!gXw9R}5#~dLoiyNlKXaM&f=7%eQWy#Dp>a?Ht3y^tn$Mlv=+Oy8`UN zu+x@2YP9CH;bR04ima4WA|A_ZtZNI2*BrwbrU~^O6PH62*ZcExrCM}m>7KTED8Xw3 zhSZF_9^$=^ZvtAbJYDLAXDpmMX{eL8`8>r!|tAAY8#@7lO)R*3OU_IkR2HAex2IkQXB5Yd#XDG zCHd_uD~8AhRb5|%6OoE)g1Po%Dt+p}ji1YEoQj1TB!OYkZ|xFr*7|DWbOwebbf9vs z&C1Iexu>VDKkZ)-yF;)LIsdy>igZJ;HU&_@T3DdJH=63#`vScVdh^ zVE#$fA^wo{TM!A7tkDRidaJibhK_L#04ITt^WVvW^7(5aMl6$ktsyovTjP*-#FkAW z5hFGPl}bO{oZ+4|^Uju)3-Yh_Sh?N5C$YkkQq@h!mF(i<%{yd5d^cHl5kDwM`h4-$ zl$h3zY3FY-r(4`=OmIPz=-76EzUr_qxZ~%i+}TU*Xln4MR5^BrY8rL4EGNTWlfqH< z?3M|sJ9asvl*-%vkfS!uM$^F-x1^%BI}Qd#e!6&^cmslDCbNqOn(=X$*S6La+<39O zf0lPI?{+N&`(QOW6pxr&fY!MT`^6j8ZY>8+@5%E zK?_`V@bi;EY^bF8&oRF|XE#XUQuft$U(sw1tpF2wbu+F#+$(h_9E0svAUZWx8v5fT z81F)c%Yx8;2953ww%z=$-_8P6b;6`S{Xh$Sw-kghmGYceGCyjBXFD1v^ff=OBn!lF zH_4^iSFhv7eXVJUQ08&kBa^IM&M8yW1VCZI5pYPrR+`MW?LYqH4+ee8o{2CJwf zxVo}(Ydk|}vEDY3o{pBbKl&@@cS4#T7()PPH{DeHG&9zG5N#lwA)lsX$BVHU6{LFH z0(b8bz%HUWv~5N%+z?|-OJcng`=~k+fEP>k+pkRBA0l+OX&c%;^hIdwFR`Z7hdCB} z?NL4b?6=W2eT{}<=7{{*27yGh`puLqzMT-)ZI>lVK{@)ZA3PgCtXImNPHuntx_qmg z_jRxD^s);;LEQ`89z{CweqyGMgA;0f36>~!=wyapTqR>a5;7VJE=*$QW<}E4I*irK zM+4~-?WMv)##Yir8OhhYq`JG-^$k##WfttwhqGGP=;|f?oO>Q)9SQs&lS@y-BbkuT zCfk~pUz>8H{%$@IXKe&m>u9lHa;G{O)}XFT>qVKY5Y1o z8plB%o@&d3U1F?em8~(;01Aba(tvi+%|G{~ndWhhoSeXtncf>4XiMOQ zO=|jl2YJSJD=?fyA`SGB+Osps#D``aO={@P-^Yu!8_`g(5OZG0ESl0mHrR6E9g zJ^Q9w(2xy%%LAd*dHmZA^$x5eV(UzWRhw*Qm1tjUy8^wJBSx;X2X%h~jt)$Ve81Bc z=1J>n686;J2_5uDlX+inMgp(ayvATXtKmk)nq}HAzkT?h4`0c5sCNJF>p%WDx1wSX zk2o+lH1Pt?rxUu!bPz=l0HRlBRJqW)Vw3Z8P9U6vc*7D?KR;Hq4=@%`Yu7c~Dl2``l z&T0ffYaS3By+@mt<=V0%canHtWwX&E!&0teb|k5+hc9h1%Ce#_pXdQeaSJmcC_%ZA>0O2RUq#0z{dyzfLzau3hzz3eEZ{-5k(FHP zvQ@q)cJAFKf<6hFV+6YPNg9MC{61uZ28_r%u81C;`yE?APeIgJ|8NrPr0U{ZWt9k) zoJ9kpft@;OM)mE&3T^il^c9W1qML&nt2T=gwaEbd#`eYb6Iz~eSkX#_B}^R^yGchq zn(nj+3rZD3(!QhFwYaoehaWc9yEpIxbS!EWsKdM@(3NU#0CZFPa-YIAk{t*7+p4QO zuM|+)K@%M8I5dLdfZk{rU3L|?krLmrHk*1gRMP+zCth()=-%`C0=1--nA4G;e})C0 z85c~?X^q4xZ)g%e+uzi!bu&k8_c|F&e7a6l&W7d5zM6m?{(=HU9TT}hd+shUapKQ= z$`L9j_`QE2(;5vLZ+#?v9=|8>)I(V!^JYwTT+IfIWIZ$2KT174^B?OMQVIK<>XoQd zle;1bH5}XYK9fj)b`QJwV!W5Exu$hY_KNi2MzGTB)IWIhhPG-4;0)RE{6fF_z4XZBrNu@_iFTe0QuOc?EGZstU-mBq37 zEhfro8{p8+XTePg@aw6xSo}r!*`Bdch|0}jQ>w}Wr%gSA;w{&$3|8gKdo_mO4G+<2 zdJ#3DNnuFF%zb)1BR&+R0VR?q|0VwmA}NtmaI1FyBf!oxQ+O2+s*@9^FN{*&B6rn_ zg{Kl1*zdkgb~W*r9yPF>)p1FN21ZCu>?fuLlPOGq1@Us_E!RD0-V1 zW9gK8FjXr+zV}#fJE1A7aP1L5xMT7@;-C~5y;idi^s@Z5}y&DUUk$5y+0SagQBVfq0^Lezv5@ zn0Gzw;S?l`k<$)BoRCOo$?Jd{5ME!O2oR@Ztx97`*njH#Nj-%0{sQ5)GkSOl*4bq{ z;)lL4I*MmCqzmI0Y=z$GH*@dpJHPv3VMk8tNykjHVKz&>6R|io#~Khr-j9MeSx(7u z?I*OoQ$ocP2`{rDM?{TnN1Sf%Eg{(WGu?+K&I2h^Mr*fD#Q{pq z3d%MTnTx}Ey+lJKPY5s}ybQSA2YxRt1qN2m%L-WKOEoKGW;vFZBNq)ILo$EK0Kj|C z@riL-#MQ2Q?&9u_VfI*Ayg=qbmZdX zO(+WrQF==5=VyWBRUs^aXCXa#_=WB+)UU1h&w{a!-xh23-QTyh{8oC;GfB5j6_Ffl ztc6#u%#GZPW@gLUZr#!xH8Z5OCG3kvja|B1XrZ5}=lzTLey~@sfOzN!45`J(nK^k- zTklg@f=elFr{2)bK`p6in8LOGl)*5+C&co{#EtQyC;fRz`3$XwaDe+PeaPm6|{KFF`7T`9+G6Dwd#VuIz#4hCr7r;k?R|DM2R^tywi~lQAm|} zP(gvbidsUE5udq_GsVD{#P4eq{BtAuuvtOAt}%$ z7rp4u$6T_Vmpf}>#~mJ3@6PN84u$EInZL+*ZJQjqwa5LfyK}SKfY<~Wn<>LQRo8bI z(O(O-?N18bTf8^(Ou$r_IxIQ7t7>_L6j)I=8W1%9@-5H4;(Gch`ygy%W5ZP%3}i?U z&7ae|4T_c1!E&JSw?Xms_or8YC}?x5!Ef<^61LRqo&-N(t-lecoybM0zGyPe@6s!R z{Fj#tCY*A=@B2c6>hV<&RW+`9&F26hv}UZ?vVdZ6vCWaFZ4G6tTzRqG9>wcd10H)b zZkll`8Q!-qp&W>G3!5E2XaL`aMRzuAF%Ht`v>h{kW;{DdOVaZKEL?j%KitpWOykBz$ z+3Etl?=g1%9m{U)e6t67DqOVXmpAKMLRnNG#L;B-|(^p(N3uyNeoqX^M`<*^16%>-_AJ;w$T&BLLHw`Pqi4RCR;kIo8? zKB#&Ht#S*i?kCcV4c2JNRD?6M)|SK7X0=PN>Djm@GZ*aUh|d@6P2TH%z%$?ZqQXL9BL|Mfxh0mo{CH$4BTZ4M+y{Z+=vNwFWqR})0Xz6)0Amgf4|%PZa-OSAiH5T7cr`DP{;;FkV5lVNn zF2C4w_jnqi<}9It{XM7+n^nz&&PhvIQYq5}B*o&D zSi=qydujtv9~YYziUZke?NO-7@h^2{fNx^mGb4B&eu zCNaAmZ5P5J8|oh|k9Bj+sPZ&{M`0RRiS2HSjj6q#u!_ov?u~;6X8~{NoXLWuP!Fc$aDo#Lkn$&78TnIBSY{9{l2*j)t>2Lv@yYt+4SJ&bY$uTo& zc6XD389!birthY^()8)R5O8`>dwFER5mlX%zy$`ROXsQ!`}6gl>|lnFkfyABbGS>n z+QFAd;VsYPt&bq*yHw~Y*Hvy#58aQRtktlf`PPG9XN0Q(R}5T}t1_T{PXog&uIbNT zq9la7dne!&28L=gnN-%?6#czf1SOciq4aP+kWjViM6xyf<&d9q06~_h>);p?@=QPX z*Vt=4{@bOe)ML)jRKW$pk-+1HuGeY{(b((>2#TV6^eg%&!qVJ_W<{A|wcS>hQQQY# z(RVOl!8lRTz>9ROB~e+%M|$HjZ{A9*Nu%^~nR~GXo||<#H$-G!wN%oWW-%-)WL4Q{ zJ@Gx2^?)#R(wI@h4-#dr2&rLP4Qi(U?OG4L&wz3Kb@9#$GDEKsF zs#|~A*xH^EjzV^Kf6&7mcRpxN>i_eJzts8i?{|iHYHCZIrWu!QWJ~JDg5%c9v6jWH zt>@sw#b6{5Cz{NW(4V$$?zPx1jEmztob%o9*KXuL^&K8a_)_Y8z`Z$P_1&SRV zR;p^YcDCJ;b^2OZfWv2hH{!48p*6dYYNTeJhHujz6fx5Sa&ayGbG2LT3ND&J??Ye<%cV#X5NX=%)H8lr1+%E-dKHm3@Ze+jZaFm*$&Fj} zT|Z)rd8xIn~SmYd2S7|EwG-oGaXa))c_nyQIvnEBl*yF{c7 za7k5sWDt%yHakl_xAf}ayxQXKud^U$>t7>Ssz@bGLv33Z2j4Pgd&uCjU}Ym})p8>` z=C9dFg9yWy`TlEj>8thV-KXWQnjuS!Ec7mtuYz(r5Q~TrWF>Cea~o* zs)A-~EP;t>w8;4|k4p@Jkh@TdcJ{uw4_Nf$!h+6e_ha=FUf@1ip4`iYoF~s2)|$8F=3g`;1SuoyY6ixUqb{=D!~o;i9t^)b+XX*&Ma_YLFbf zucf7{gSCDULDKkv!QS@yMpWF&iv0`tYY@D}E9PP|v#c9};TQD|WuDu~*b1F#pQD(3z*FGg` z`zYVE@A{PZWmlq7QH0XuYfUhJtuxs($fw!NKXJt(;}frN%WQk#%cRhQzX*3();}0_ z4kU=eePhnWFN8jl(-Jz`L)>(n-rwVv@tWGw7fgwmfSogNU8B!(Vw-%}cJ$=eG{-)S zX_(ssW6Zdiu4=I9(C_%x4bNom>dI8@`mhN{2Y zfyX;wJQJ2!?j2bik~cut4*c?!Ty8TP!rZ!g?=tNa%j%BXXQIxTqc*0^?bG3Xn8sYajSfZENYB1 zdx{D#%0!L1y-Fw6Qdw|^pWI05R-EaJ(Pma?Tt{WwL-QQZL-fcri8?YjC8Ax zo&nP|CV;arPi_J>n?tJ^MPyS3Vq>$a8mp_8-1zM z&zB=Bu5gu6Hpu$NzMKwbJO*MlrrWoO`*8*2H zH8pXRk|TKhxmK;PdF`>T;Q~po285?BeZJp9Se;IuBnWE_jJ@vvTSMyX$Wsa95ivO}-qNJzX?}2|;#ewS--VNG)OG37o&)4d0i3}r;I6T_ePPA10__4Za@>&q*uP34p)0$tHPJBUYe$70&pvV zMIOH>FZsqZKS%vpLH-aT2Rmq_EwoYY(M-$To@!Y#&d!O|0v9$C^ce8zo+4Kkqs9Q} zk@Cd(Z$!M2!6DRqbTBElpwku9G26BaD|Z2%o#s~;J^72E61hJKPQ=u-)II&XI3@u{TmvtA zkPWG?)GM`g!Hp4HyEYYhGPsdz!88COQha>xU19X#Oc1&|cj(!`L&mN3YLk{XyX2jV z+-lP8gi9;FgJ13%H+Mu~EIaz(vl{M_Rvj>3gyTp9<&Z79935b>F@B_Gz>k1_Ea>d; z7d@vStTk2qse$DhPQ@_zs2#gNPX)RG2r0q|+-lx? zzKQWa9ZK~qjwF!DXn;9qrymGK!pnc6+f=xIdRzc-H_q3cX2O{;pYm#@$&dLE>vtEb zJi?G-`j8R4Wztd;_JrgGf)3X{Q~W*p6|bsA_9G=m&+s6K+pBA4z@amKWa=pDqg4E* z5a<+~u-$!A`Ee7S=+Ux2D?+`XbzU!{q!CQO4I=R0=KdTJAUWi=mg}r1T^|wzpE`c< zt7$S54SG`--b;~`G1fb>|5bj&qE_Vs`VGo(2wt-jwt8$&kxgKEWq0_D35;G*Rk`u` z&h*iCsYBh+a7@&PtxiAqGhcSLNdb&dN$jIpH05+rY$ zvX84hqeiCA5b@-PD!EYXA*;~xWHC+TgL8uu2|sf?Ue%Ynoc*fFDd@G^COpakp_nHy zp>pSRUjn=aIyM>%O3Q$-b^mF^s9E+Os|1qv;5x~(@jr3?wowU1xwi)$Y)|-iN`+N!v<;qZ{c*o^#(trCkoUYe#cz} zD9xP-XwdY#6=1A1)*2bgkHrWydg872KG(@|1jtH;ehU{#Ib^|4Q6koqj0eZf;W@s0 zPF3uH8wx?+RZ`tI44Wzt$hNuK?C*gcYH|k_;=fi(j+0-_#q3ulDb^tZSw+8SuwJdj zByWsktwQm{cb`d1PL`L@c|+fb;p=gzj@0vomdI@*_j}X|XX0yGjK+?eF>Zy3cOvqf z9&^8A{qdB>UaL`c=0Hv$26~v$^HGtJOE>tqPuOfKLT8sYrd*eV3SqO@H~Sk9F%&8A z_QtU>lZzrokTOE%RRv#R78aVb!Qqywv&V}GFnl?at%UIlK=^nv0o`)fPXK_-IK+oG z>7kd_2Cd-ZKaZRIZppZShkl{?JYhER;7R)Y;TrlW54x5!FHWKzCcT(!u~eDM}P zw;sq}ywHMCztZ4HpN$DtbLX_z3KjJ7A=fvh&V>io%m`78EA0rXLoaY_0)KP54~rMe z)P)K`S`Xrhb+TKJ;kEmq&gVMHW4&WHcRNP;#g(%`o2}>`j6^``MyVU;iXkiHS^w=@}cs zj9Q=(C|Jo5X1O=7ObQrSj(u1$T%AVthnlQ%D<<23b}l5`+O{W1;ytKbIlY@d@c=85 zUuEE8x>Y=r6-)cr-JjtteJM>gw(H*wTA-0f9D#VF^VnbbCUht9&QDKjkl8`Tm4FwHV>= zO}iUztx;zVn3xVHeYb+C66K2?W@4jXA$oSR0?jR912@+AHxMp%_I@bw`|Q4EBVkGh zy|f-dI{p%t~s-YX0hnAKt*-+3_HUuV{c*lyyKq^wJ?Bl;T$1bL|{P> zpsqjs8&IJ(^%$gUBQQI(-iF$^V?a$y(>J_`;{+p({M~wkDV)hb{vh9*MDxLu0Sr|e zsPnR#;mVY!n*rg!7fPqskTu-;30=6ELt{w9AKqKSDZ`SA=w9l~+8Q}tX1Q#pQzGs@ z5e}8%MQ2*+tSTX;GnAVjr%BHnFne+N-rYrV>-YY(%)qz^VIPfm7J5Iv*V9PeeRNNZ zB=|5$KGM14e3TpDd&t$twHlPidoYS4%CCQX_MUrCbEKVr2P7MnbX z6{IxSSO*b?v2yR!dh)BEaWN_C@nRM7Zo_kehaS`>Z`x8M1wqiJzSN$}Y{O&SKpE_U zS68Det=9@=N)kpW|gx zAzPGjo=*{Kd-=(F^T+vwqM?sC#5wI|7n1GnpyS1XybID|d`@iPD!zwEVkQQG??h+! zXZ5E%*zHP@{03)dtF<#Y^h66xi;*B(HRh65&zVo{8|*T<0AF&3<7~@E%h8Em78Ny> zty-;8;`(Cm{g{K+k5k2*!N{d4IDtUE2!p<=?PY&_PsKpAzJ%S8$PI2$|G4c?s;`9K zUwY(za3D5%=%-OvyFi(pO9x}|G@q`!oc!v}zXpeE?ajI$A7sV;r}fJec_71VaTkkUs40`tt!TI3hcRI z0CxL30nHbd6$TWxhBD19Z6P+qy&s&vHvvCbi%q6OLzdptB;T{rHNLDW2vuNTWT!K3 zkKLQAr~V}#zGWBQWvFLRX6{@T8zap}?3fhcVf*I6ZIc75gxpRn#tr#j{c5Nr)FUF# z`ID894rgBSo#CHVB{@qTmUDk#uQR8UUUs40%Smt2y`mlcX$0GaOjD(Hwnl7tVNk!y zG=YwidS$A{xwMA_Z;W|N{#d>j$%{}g{CQsrZLG!GS%vcYmq+>;U#n5Zw%jvYRZ0hM z#JzS@E0c2RdtNS5=h7?mGv^ui->>=zJK!QqqFZeg!#{0XCTmKhL)7^)@o{nQ6p$3G zNacqE@Jf14PVd%Mp+xqSAkZmnKdRhpIlPd~KKvg7(g#CE-bJEyb=y6xV zwF#bz-NvU!r3DOgxM+6I#9IAQ@weq;&G_#r7ZxLtx~IqnV}T)~5TwL%8);2`At zj*W`)3`>oyei6a}v0}V$Dr1In&{$apeKXDsEu1#pmvDWz(ZP?L??wlet~T{dC!?;h zcaK*YVwq8v?)9U5KTOFD?RWQl?$z_YGM8J>OR-0b@huR?yfnJN43{j_8b2|<5M<6M zkOMOEIaiz*6D%CZmGBCh=(RJcZ+TixW(#1&aK7xF^^try$O}fOGH}|z@p*<}gLqUj z*UgR*hv5?p(Kx{xXB*q1K>4>{+LSL+-rc=G%6290w_ZIyH<9(lU%5I`FmT%wH=bQ) z$j-=l+gzLQumavbJCTYndIj59j;7O~k2)oK`o(#+(Yas|=!>;YMz-iRBeGcMOlCAX zbz8=s?dw6=eSMHu=ySX|0OAC;=_{CVqG6`R$W$$LV;e?rk_?u$SYMQ2O&F8Y?Wg6W z#Llr8d0sXGe9i$#1F^Le;n2IP;zoWidpH4p2c~>>aXppfKr>n!qxWf3mU@xVIE=?^ z%nb8!kvP}?5npMPebsNS5fJE}smmeklU#87QcHfV$x#T5vCSdP6+c0D&HORv&~kU&j$D`tjt%nu=T5}8{UhdouPD zq4z~|i+oB>?I9E}y_Oocs3DU@rGO8GD+rB1W zrcEwaPd3Q)DUw8q*LUC@85Id6JS9@Cl(pujSa52yw%@F1woi^kZ7FoePtT;hNax^r zJdGaTvTGDdY+2Dajek}|JGxrR@3=KtJ?DC}BVe8*Nuai5F;>a4@&mIS49-;d5zAR8ZbBtg?!+N~PkB$8#PsiHvZ z*VDr?F!SoxO==;iT!W3d9tD!i>G@~p)o0wD54gy-F&1ITqx4z-Ho)^X1&Gmv?~-Me zZ{;gaq&qg-g7Q$secCky3j+pGY-JsYK~XJA!jHy##RkX6D^Mm6-6v@2e;|1jFOYEO zXAGld-!6M=>s0A7E~@|nwkQ>ViBTy_uAB(9&(3+7a`hTRzMa8z$1M!a>pp4MOV94r zJhzAlwImY&D|KIeD;YGMoV`=`BCU(CW$=(%Ak*rsmoF$5N9fT=#{gxj>y7w5Fi%HA zEb1ow)$z$@l-)zU?A`*f&G3;>oRRxLvSbW_M>gBwLlzxz+6jG-8a^@i$uPrr%i!;r zr)g)!DNoeSnPLKvHAjR%*8`TvqUR#FM&6j)VbT`Oj>?syoRDjw89>~t2(ijJ{q)A{ zZ{WhNSew-Lb7Eghg0TMK(fN*`{p?K_G9*r#(~(ZvkkCCIKG9=8UEa1@y@qz19jt|n z;B6um-~$B;)f=$K!;khRl*IkUFENziqtSa8=^1&wT?gRpHdWnCyr)OHmV?K{8If4w z63>piPF+ZIRbiK+Ug^rS_c+z5Tk8)e-HBJm7bsxoHo{ko65+NVnFfp&6%Wr>y=v|6 zbhNP}Hq_S|B%H&f2-m5GuF#+(!j1g}S)dbN%){<|l;@)(=cCW#xhb&yBcjMwyaPd` z##qZf7ct&nhe$cAFn#Gu5T&(BR~W)S(EYs3qbHag!ER*G4Aqo{S9t&y=KC7!o?8Kf z6)%;9jq!s>t2IOpr<&7i`b`UU<1X9YBbq$hiMu6XDt^Cgi7gF#Q@hf-9FiCt1xg3MnXAnQF34-71Z*g7v|dP$HNw1uv4em`H! z@NF;ZPempOL?Le6+#b26Y*nQ!6r{PTvc@m?D% za#Gb1J~^({t+b@QrHy;8ihQXM9mlwkNo z)e=YI;k~nrS18I?2t~?I3SYkXV*R&8Ym`(*!3iL_IX!}z076Ar>X{OGs9I_K>)N0% zJ9ysX0Ws;XzcW+(V0<8G){2a3%}s4mu4c-!3Q?=(MS`OxpTl;T%vnR zz#W)oZ2VAojSTm<%jIgC81o7n`ts5EO-47>v#OwZh0xIS%+)Yg*&CkwkjTWR_`E)N z*^4l3&>;u3l`G{XxoK5A^}rW)v(^*!bu#YIJl;PYk!53t!ZH&3)Vem*a^ml&@kyJ) z7_B+C;zlUams*f`MXIXLz>0`HAyaBatmkSN9HN9fsRGv};!PyB+icF+O|A1S*{y@U z2TCfJ#2L2HhINh(G-_d})hMN8%nlwTli8M$K92b+BJ{l^VqSlYvIq2KLUVl-u)5&t&U9E0sn8*QJ&cSqTDBjHV?HJ5*y|Ep*-jyZbcPsU{=X!5^U-@)%Qqa~I z!YAErDL(Nost+7Q9xy+XBzmMBWdo5 zJ_|ufJ<-gycgB%yuxhoNF@kr1vd+~+s3se~dYB^VSnnRly*w90Pd_`LZ0COU(+NP% zVa948e+tOzxu$BWvZ#Ac7x{Cg{NvybWU@DpcV~SWAOKzCX*7{c6ntE`>umr3Fm_f^ zZM6&6MT&cIhf>_#y@eKv6nJr`xQ0M*EAFKfcPLi8xI3XpLW=~q6nBE_pL2Q6`(6CE zjKNjLPWH2&x#ki)To6KYu8*TMHyy^F)k{4{i;EV{VslSuFtWW{X_=0PxYl&sfaRa% zOas=X9!`4iB?Ir%rf(9T_ZioT(G>#OJs&m5|2w(((VFT~@n6sr^2$eHJms0HL~jl6Oa0j*16 z0LZt`^f2Qx7lq!Ru)eXPq}<-#2*7a3nvb%pimy*CUc%MBQ1_tn_oACI;q`C*U34ad zG8r^UrL$LZa>qeFjz*yJKEbXr5hPBGowcW(ym%6E*fpWh&VFr#KlW!FBqKTJ=B>>` z@#2SQS0x~18z`xr1BNM4Z~(|PH^>4}3wXigg~(B*2$}3>zdu66!WQG99O%6{;_fMQ ze7wBxpKyao!j%WH!?B&6Md7>Z4IfC+m4r^HA~|t~U&f^gEq_+G!I?U2A{nwu7q9B| z$EV8aj5QfiY^KFDG<2c#v_rlo}9tS#Hk2GPsLBfy?9M-uA_S>z`ct5QcKCf{~dS zvYr&8};7iME9b4G^-0;q=AAkl=duh*WD-FH*Hh!gp`pL}5hy)D8 z%%pzWqAAGDB`Zv`dIyqkp}&7cEOcT*xO5(G-MtV z{O%ymTR1Qd9frHM;fpC?_aVRE_Bco4TBSk4$p_uP0l~K=yc^}v?1x`;k#j$~E^2_) zxUy|)S}+)6ezCeYB|E9Gxa-Ao$1obdaTA*C{o(%OC%XKa1YW3>p4DXI9U072U8ZuF zUbawdu}FiTawQYh7oCj#WAtY-tDx>AWq!u89$hnEk%Az~@Njays(-#!j!?%z6lY|T z1AG6z))Xt;c`ZsSAoQ+-QLUHEACi38d$+VjW4Q}!Gh>&X7wu&up>(|`@b;~|>3J}( zHKC9mR^5C(`9`Aot>`}`Px9V}qPddt@eSu3!B3BjVX`)GpC@ogEJCY5wPVz|Fdm{7 zF__#HC4X1mn>-7zx@quvpn7FIm`>{&czqk|7-0st}hevS3m`GY+g!E6XsW;<5suOKyJTHS+RCBv5+NRbJ}6Mfz=;Z z#fl}(Hr5t=LMG>Rje1C5XYFl8*n98(EE*LRFTys><(>@KT6w@%oA6lG7QT%?sMF(m z|G`&wb5Z0=m-lTml5qa7NWwo}jsE|NEwhojNk|RM*+I;tLZ92SYh@3cNkUGJuXY$e z#cg!`i{o~+BRl5<{#~_r_HL62b}=!D^yYw~OTatN&@YNYS1xw!mg7-cF#NW+-&`2= z%j3WFA1aY(mPYX1(+EqvbCsNF*;(B6psi%(n-c8A4iL*r-lfJ7(;&1#w7lOpi!pZ1 z8fl86*IoJ&L4td+GcX~t%rU$^71|dly6>EkmrEUk2=cN4v4SdGVr>+YUEvIwxpBLDY#<6jkY&U=uGBL+4MB^h+%_lV$ys zv#8Pa!Q94A6*+PN7>tCnIrK&Cy=bLUhaxAFKS*3GqLo0^dOmR?6ALqXBO~rxBN^{lkod9AG5(vZMOsJk`3}|~`<*$F zGJxb4!C?Rg-;60ygV(o>o6n(h_zt)dzaWET>rQFbEFk!ehlRYfiFrsPmLxmEC3<)qNA0_0*Z== zL}U=r7lU5LR_66XB_9$Gjc#|A|1qN*s4QR^9L!u637am5#+cm5UN47t-(Tx2^M*N< zLiMoRDsjOj^Q{XuD_?gSs#+B8^si)ka_N_cS!F_cc;8wYy@y^(w6--L1v^Mi_Sr`L zNNM-TX`uQdPwtz$;bj66V*-Y)hUO287`c3Jyskca;NQ@w)PB{;;Wk(Aux&z*>PioC zy^c5h(d-degh~dkcMQZAo6JHRj+dUD4E16#AS4y`7z59zh(pfZMyS>0{Y`}U0NY1( zS>oSt(WfDprjrNRGSHs^PzbY6ZBg|htDHOAF?Vam;L;zJ76|J|`BJs;g}*BZGgj^+9(JSE|ud0_d*HVU`=gp3lD%)X8B_!O7c}F^l4nO##S2cS#ITM&R`rv{OxHDbF zJr2Uq9cpSe{w~XazTwX6@oX%>cN#REbw2%JuVb9iZC^=jP&+6hZ9Rh=T-*3rQ8!xn zVp=>PE8N=o+BxcrCf?1N`unCRqs^N!k1KCQo@3!9%Z!%|3F=v0AI#|X@8ycCvE;7 zZ~aTzyB_z2naDl?;IpR7VD7K(0TW{np&x%D6Mq;)fb&NbKT8mxu0{UC^~(Fzcg0uw zE$N|^7cZktGmft@v=eH`QgZr0^P<`GYPzO||CzNy8^PkDs?*!sr>X)rI#kcpyL!AF zR>C=$q@c*iATASAavCgcU|YdY`lOwLYOyaW9Zm80YPk4yZD)xMTmrbw6Su|16&{YJ zO`&ER)_`Et3VyD<6n(X(wa7Vj97)Vf^zv7~v|fdQ z_S%szzf<-l3lZ=;uT{}^7GLf7AS3N!r3S6jBkN^=!?oVEw&g#Kl$4Z!s|%y1CB^OS z$>i^1EMwtEzveM9RbQ~VWAIkD9_&WY3(RI4gNTaBW9EejhsCz05J6-zM9flGz3EYoCe&N$2H{j{8h#1JIB;$(BSI#dV zA!Cm}s}p!8%_U!tUrrPPgBe>3r16FIa-di}Nh<{~4%ORS%k?~)j(P$9OOKRBFUA*n z;Vpr3SU$H@V8`h)jSCq@uvY#^*%@aSVl_ZOKt=(6y(?)3FluoO_ATY=vL<&NB*9+b zmmrhR2AV|;YnWgi)_9+z-N?jZ?zryErv%O75eVU4u~ zX?xQs&9?@(v%H96lpb&+2dr6 zACk=jdg&gBX7xdl`@yog@Q~NSL9vtHy$50bfx7M@a@`3^H zfNMCUJD|bV1?%Z5S}qoB9?Y=zFo$qR&kz(a@58!YiwLM|r%%mkc~yv9kncEWx41*w z&K8aM>1mirQ}Cu#n*to1RAO*}@!`sWfM9?vv5wXG#|bKHcD!`!iAoK_=0%p|>U(Cs zsd8u@rD2W@v;GtvmXX!VzXhN8u~rIBF$uZ8g(4VOB?5zK7|i4qq1{{CIj8Jefp{3O z0bXTgq!E{f-GSkGpawSJ2oh**iedxL9_k2)IvTy})xynoFQ`$y8Pl!Xhzbg50D8S- z41m>01Yf*(x;e$*5rtW{q@RwRy#KDlt@Lc{e7VJqRVe9h>ihsaiK2DuQznv~314WJ zJ73iPuV4B98%Ie0hEhpgzd)oA4`!#gZ~K~;L_KqfVcE?8S(ZS~PC=PFd_F1)>mx^7 zy;)QyX~c@yBVIP|jIFh;W&u7Au0WQZN^V1+_uGPFb^JqWADcJ&34GG&{uf#vj;~Mi zU4{Qppyekr?e&$1 zg+yo}tu?^!A5%XTZbc>N-fL7H@Jk=-s1FP8Gs3KC@HYhpZigMie7I-%x}zP0s`us% zO-#ULu{iTd>}Mv1rb@ZrIsW0MDp*EV7;5yvfB(KvEBz^fmzR ztgfvqo^>msRYBA3$d6WPcqh6s-3VuBJ2d1|8-AI@ARlF)?`Ol@ed)8pK4~Y1E_82qy`*%&l9{=k?LWu4TeCv!4bu*Kmk9oL z5}@}%CMGTv9P!+}0@Iy^cP!+N-!Ut#;)#sl{D{B3#nZ7DUkPM!>xF}|Uy3a%;*l?< zw0S-X^PK3o3QUb9O|U{K3wyK$u(@^$#eEu+1t%$Mg-z~k=G~z_pIA_Jp9@;?703Go+3cdREQ4iqx|q|q!aITxap%*; z0nOob)YN<6?-RF>J-nc1tJTT)gEwNh-)RYZCI4n$^#9qMUqQ@_upTh`UvGOug2XQ^ zxxhY1aR7bnd-lY$fn|~4>y5f%sV86?z!D#RM!een%wTGf*|*-~%8@Yb??k@NMt1XM z9~qwZK26K_k5k$aPc<6PtTmjy&+fII7ul&6p|A%WGY)Rnw&hLx|KsVg;|jC#v%+j$ z_GO9UJhpfogL@e6isKHRhu->P*tpG(%~V3bLg zzaN1t`<~1lQ8fhS%1U$R*tfVZ#FCanExTva3FuI3gf$OGWz>hl2XLub!T6=QIDCfi6WZk*j0gJICj&D^ zqTyiqCqP&s;;w-Yed%9upzZ$=2kz;32*13UcxmYF?cKa%JeJYCuIm5`M7v3 zL~Q+XIHu&qQB$JZ)SGWAhqFA8VcAn^F}iqqk!c2-tocfXDZ(5Y1buCy$OjTOk^vDi zrbVC7uni$6Zc(pPOr4AsH|l`|D`Cqc0S|hn7-vZ#zk4--S4>mPG)%!ULuc26a^hS8i3M3LlrV4l0HZDERAv)(o{t|mBy(#D$q3k^&{G=unrjaahdfN-$;;8#X5;;jC`6af*My%>j{(hb1u_4%e|awMdq| zqB0M<{HL;8T@fxGTAhWE5V|o6ojH`Mg@MXh>X3CcKl6~s#zAh2RB0sypoi~Qq2&`J zG!eK1iS2Aj)9stz)$Oek==JQ&UZW1Ar>9Q22JS#@d_AzQyWznuOC5>(lxRZ+-9Ho% zGFT5|D9cLJHEy3@=GouK?~ZV!SGs@u`+C8{g7SqCi2l1#&39QWGtfZ`kjA)HTyu)z zgykKuKL21iAtlQe)*SFfBw7AF)q_@~x9FKDLOCJqHa9KxcNx*Ug@Xmf4?u7W2PV#r z*0S8w5v(fy^2bYnC)Jnu1EItgL#FEFXZ z+*1{go@mC0kCxEXRjIo)x#`Vx%69@EPnlmAU_Rv(Jd)LN5*F1Pf_(wFC98Jx1pDfx zp@g$Dl?#3k3*w9;85aHwytSg)$T4W=Kqu0;!?B6up(=6~)NwsC#95cN+S48sY*cbBW<+SSq&7wekiu|$A%+RQKi>Oi7(@N)QU za;0@RQp#EUcxx{)uF{v%nEB~Er*8A^*QD=>hIIY?zVm6VbiqK?n&JMI-1|K9_H0n2 z-?uG+5KTg5gue$oQ5+W~Px=djCa4nL`QMn5+NFNRzw1rk?#$GHK4wu+Dq>f$99iPT zJQ-@4Pp9z~?NyG1p_ZD#cooh1+FXQ85n(daCh^rM!^Me} zv79apw8QllpWi-OoG|n^BW)zjSqTNQMRV4fH>r#?w((^VL~x41DhUmYSN1mXKeN^%nUp zuh5KiJ>;2Vmi@hHS;`x48?_Tm>*Wdihd0cao^0N#L&_nv`wOZoM4d;Fhr+b!vs7Ya zkr5+lDrsh3e%d_`l$=SLaK3rrSdxOW>t$3_lz@c5t5CbD*&%;d48{4P9@R|<*A$C1 zaJ)dTP!;nvHKsR@>Y|ZFRne+T3=lN2-geMeZrDMsP4j};g{IN|ev=2%d^O6FtW8S@ zI$3-xsP#RMy&&*qG!bL^%~rkD@k3ywb?{&jU8o&Z+Ci(bt0+w(!)`(R{g?2uc03V_ zj8CVw<}gp5sz+{4EO8*%I-fuHPi!mzX?3ym?vCm_?!h|=K3&=5Venew$WYrXo2VVZ zXQ9Nkoe!pF4r<{HrWl;5%`A_%sduGjT2&Z#geFQQho@aCi@gdhGJ$(K;|;mtEJMWO zfo&4??rqZGAUNC@s%*J5`qM2*UAF9z%uz0Qb#(m|z|41MT!=A#0VjKo+#SUpji`L! zm2ru*cJ^{qf6;rS_}LU%df3rrn{|k z&jp`M!KD_!E#U%Z?sl>B0%iN-wsX;tXy+;gmQx_ie{>Can!lsb2Y_sx@qnDYo~rnJ zV`6F_Bc&OKR`;oD0#I>dSrE>S4N>J<-BERPfw)@q(x1JL`K1GNE%K~U3CVG!WwB7D z`%Hd1D$lt)*&zO6vw>SJ?;e~byw9;IDEJCzYd;=VWqx1G%|eRPl=CO3Xz7Q4rQ+nQ zviP*G&ToFbN{YSo%!S3XHqXTpU_ah%;`)xb)6fpgqzlU24tyM%$q1;E423!JW|ww6 z@3PA!t%#dP*9C2SL>?P;9b|Hy^GnK(597C&w@h0mr!^9x)uLy&ss+P;Jut-G+EG`| zC1y8&N~|D0Luj2{&Ww_WFD2K$q$(fZ(hTy~_U5?vP=}nHJjyO)T8X9jW<>7&>xAF& zf1L1VPE51IE$|C4R%#WjFmsnZdOlG5+u|7+W(C+078%J>{u31jR#g!c(Nm;q`dC#{ zOz*WJWXYz|?Zt~8Dfq&HLfizbG;k^`Ta^!_DA$SUW9{Xvp%=~lu2>L0DCZ zQrT89?=aPZehRZ&MbYun0?2T4545EZn!Qn|z&X-!FWSS~Z;hIEWm*TB=+U<^zc;;j0YP2nj->}spJ={{GoZp-HZGjUC%`W> zI|$7VH7EpexaV8+{XOMyIuZ?g6_3Ga1Gf=LC!6%gd;bn#cf&8#{%OkH*Wtx%*pD`y zR$ABUY91X3x|F+uSNzvuY!USI%^|hNtKFi_mlK3t?=Y~vWx#Fr@xWT{+UdhL+=)}7 zhYyFkxp;uNW@hvVsmqY!y=Dsj?vC3l z^kQYt%OAz-7&+hu-5FjG2g>d|)KAxAG$K3F;R&^Z9!4B+-bPYd&su6*TWEw zWC+q~wb3Vv^~JMO&teOAn4`Ip=+Ncx^i!b(1?R{iGCg$2f|SoNq8FFR{>`#+VD?dl z_!{>x1Qj-h&LY*zm$Y5JPmlyo1cEKX5h=kA(+GAAb1Z%M8%UPZe&R<)^jCAKj38M5 z4K&dYGQ_ENv9BUhPEVxHvA%9iT7@pXLEzdywM+24Deu~)2Ekr{j(YE%!D& z_8{0;1_;Z8=EgwQbC56l2M2X@Lkp2_m)jh<$C_H`GbQ8ECQApW*^%5lPOh=ZoA}yz zS!wh+(rj^zY+bpUC3-;hPII$HfuCj_@8m!$*Uecp!@$myC#tGNNY)+YX2Xe0oRe+t zc6qXA7L2*0KuW*no7LIPbl0L-vDfdE8GrbwFAL6P&GuEaceR$1xr9#ImBNPsY6X(1WVC#%ybz0WAbb0N9(>|^HyJU>^hPzxyGpI z=(7!?5GtP)?~H2NF1xK>W@eu_1LFTJ8U9}~qiRo-Y*xal8qju}Q=wcG3iaJ0O6hO( z{7KO)77?BsAw@b|mf$k_idp?B$7TUBp@{vd7zy!=$DeJus+vq+zUK{bl!|>$!`#-& zeY?PI{aqr)vl17^X$^42KvY%j_LT(;(HJZjU<7~4%*YE|Fw5wdhP9!!aEdP`danjgt z&F$k%ETL{xvGDBAjfop|kkYLVqVNI4&e4KlK(;mFZ>rX>gR7+zg_7UjfrOBPf{~JB zxw!w>J7j>cW$sF>!XBK5iRN(y`S8TRAVTS#YhmI%M&YU6NVoE(a7*f&zlifJ#J0=L zhPkF>wNZ_JgLxG^J;%K`vlGd^5B~kj7PyYi_|15?rIRfBRboT9h{zOh=u3l1s7ZB< zfvW<*=Z|6N9ES+S4%+vnk)!2f!HN+L2@Q>&1nH(Q9XyP26o_MXyRv)R?Ka`~;mb+U zUw$tsoyXfw^y!lfJ_xD_WjOqhUQ3t2PEaElmPF+RM(53-zo}&`kIOkYlaLsrPoTT2!on;4cK z%gE^{cD{*3at7ark`I)6Npi_(tTJ)RO{72m>8RyAK;{2Jh-HdmZ4Oicvo|;|5q4cI zS~YdJW@l(D`yMs7`NKFE?)U8hFkTouT>g={nxB93`Oe}=OXln*l_OmZ83YaH-=lRS zXbft(tM?X&#trntb`;b!Vdkoa~61>mze ztmgpLKR>=MC|8-=)N`hfN|5}AwGnY@6?X~`FVPxi1=7#I-RF7 z22&ooA-69d#Lw)Z-AU2VM+*tRB1OcnU$w>UZ?wF>NLG|%25+|yA#zsoT3r@C3)@3R zjsGLambsWx{uAxl&a*@P1#(~f?{dNGf5U&0m&&G;k_wGBjR#ju+EKk!DwAP-4|jT| z3W?mcM-n5qVz?>u6e?g9K|z@qi9_okTLMsS;UNX_>&iaAiJyW@&VIELwCIGdI?8^> zPSq@O7&AO@45yF1HjdZG_1i$M>gggktQB|DNdzJeC`i~)J1)InP$-WQs=_+S7stC!KI z%os8g{Z0p}WK1;1Dz(NiX>n016;c2Y)$5xmC!Uk(gvdbC0k3c{R&7URJ{fp?TmOSt zRODD=R{?9U=nRZ4;?C*AIO;Sc@Rjaa=f4>+xPPOH`K>l_Bc`E&9H%SjFG&u@6JPOa zRET6@88kGp7|iHBCr7T5bzrdqKS*dk-2RawzmXJ09otw|rw<0kh!);2gyjso?hw}A z-aZ(0?|kIp|7@3XWjP_j9>d#JIggVUc9AFTvpIhTM;q(CemogO-p{E5Ilo^ik*UcQ zgLu|CrRCx?#D<}@dX+)&rhJ39Ur`&E*)pn6CpULp`T_Lt;R){nVxWuV>{IYIEyWOX z_C~uKALeAUY*x3XuQqfpC&@yFQtby`XLnQntqpR zg474wz7Vdz%}%!04wY`FL{CknqGjwKJw@Sqo3Ut>nYB{Kxh?YSd?1R>tShaWMAwhU z-=)yp&ZpV`vf#p+OFXGf{)%=vb8X_b-GFhy-hAiW2LCgHu8k~VTVlMqGR(uTyYcG{ zL8xe`c%)6uMEjyl-_Rt=?{o=rXmvFcuT+dW!@gC5KGkn2Y-3oU*PeA$A&YdC_ZXw{ zSV=fK6)nwBWTYf=`T0SlR?36?Y31aB?+3bVlZp3o&`I=Zp1XXUs|j1ZZFOi$|K!bj z^lE&LyX&82Dsb;(Ye-zpQWVmmp%-06RkkeRL;$!DliltC@WUAnTn_=eh{r!qYM8S`+5PGCHQl)bxoq+c6-6WI9b$kfJ6)fTZ>mbh; zSpaNE<&b{5l6Iqre~a3;De>!A)Y4SbKOzr(S9~1i!PTq7R-^qjg>p+je$#Sm3Po z#IbsXO;6PM^J?u%Cz!b!3#FMSMS{q8_g8UB5o9JA zsOx03ngfp8>AZ!&*R@lAg!q90Z&_^il6MSuTPmJ~@i_j$t(Z2XZI+t;?VwACrH0{> zo!K{~jj9Z{P0-ZCcxeyYKKq5!F)=bGv?Wj+{5q+L8W}q}qh=?HEh3ev$+_x#jjl{b zPR$>#UmIGOgr`ZKrmMf5fjLW%Mo=-(r2AV5Oo8a745EfZ1z{03rbF#wJa-dJ8~x!} zXA{C&Hb@^4N1ECNSFwK8#<<0kP!gjwn}Sz}DV5=Ioi04r_1dYeAiA6G1vlRmzTUL2 zw7%*{y0ij95_9Gr)gg)&qb8(FN)3vY@vZ*3d!Sk#|2Gh8B<;%h5UE>eZ>}cEU)D=8H(ky4`SsUUvXuE0z zgEK!;6QhCIuW1Q?Z$_%G!M`0WDgCzfo!=Wys!J9YcK|t*dwjBN<935~VS32Mpf;t! zyY%aVx%I@!+25{ISyPbuI)qNq3jJHnx{>oXmm&5v zz)tf`xI#ThU)hlnk6u_o6EOn6@o}gwl6-Ggw1|-n052m)&n`I_njjLDd#TVfoC&tD zXvQEic?5DAy3E9FguAXMp*`zu;1y8_k^H^5e9V4*xLpvrW<#i~$g_((eI30Kc<3+L zY~W{9X7@#!~MfWD#7;LeA@lpNIe!H97Be!~ES;2pQt1=F&^V1*B8urpk_97bp;P)IS8xX}ci7O1L zPiA3++F>0C9HLgCVcIkC$cJ0x+DA>dYs9(|*4p>k$I-nKd&hyrEj_DC=yWcMku8jL z+V@{vG$kDL_$;$)JBj0e=aYC$NMzIFM)E%qJQPqe{?^whj$dgqZ1)b{)*oRmbGvnU zx(!CcLi4s+>ud8+AE+@Vk32bX*LlN7L+vtwBM>8%*P`q@zNQcC@p$Nq_&vB}{I%0$ z_SwCfG`|A(zB8Dc5P693l*qObkg_ZZNC|zeEK*LXvJ8DjVF!P`p}{Sq^gO zwF#!}VWQF^TwAX?j?xu6#8-VwE};BV*)zx1E=>`Q_L}f4aKS+LNe6k=coo%B4?4Re z-_zM%+;A{MV;aN=j+5?W|d3UWVm>^TMnKe?#_qC2|3l zynaS$G19I=%NTI-XlU3SikJqr`9Gs35D{%4f31;Zqj_}08IH{JK)LOcBz348oFMrq zz@KZpvfB2D*y0nm|B_vtE0QBIwPwzPuSpTx6D~QnXef9V)L_u*u0}M3JO_7Ocb^Qe z_^k2fh@Q7kIAggdtY0R>3IYlKo32CNyYu~97L=~j+vwOkUUtxiRu#3v5st%)TG+Y5 zC$%EBA~@e^kZUU}kWwVMc18TAs7;WFkkHE40cFyd{?eF0Iec(lL@u^jpB6c0y8Dy6 zJhgoWciBFcz{$F_B9eZh)j(hqzxXc=N7H)y{F_IbxDGT<737EzeW5nbATJS)-Vs^g z3oE5Zd=j02s*CDHe}WYO72@)D4s@}@^=#|Y5;b0ficLHAlL8#o_zCJ5+qQjtWxbW) zQyuPbP2I3IdjD2o;#r^zd}!RV4E~AOdl`L)3O2q0;0}*S^rU+HnYFLj8YorN5E0vu z+>BvfCRXw$H=8^BpANA?U!VG)bA|PO^Bt~~ko+>=)+GQX$)g^3 zH36l=-B{B2d(6w6me4OFHo_;oQ{@2p?HhrKd^Su_+95Wj+hy1kCJjiq^?)PcyIewm zZ$j*Q5S#(Izb+STT^Zri1E-&FvE??S9F;YFjV24mcl1;ix&H3X&~c2ipOQ#|xwqGr zmni**wu*9VQx=k)cdyUSv#9r)V39wzZ*{;GX?mibko@ zFl^^mM|Gtw{x$X#VTS~=(@MzTPt8vk_|X(3KF1{t;x6wj?UWpKat-~}_7j1iwNI-| z`JT+F)o6($z<8FsMcOb^iE@%P=}nkNih+F9adT$@|?sYC1V- z_5&WMmYAn_?f!^HHYI&LY$H>=`1PhCbh97;W)UJAj1UjeMR9ma#Ckv^oouv^oeI|} zcGU{vKX$tqfBav>M(zK2)W3SClvJlkz$kDx9hM!vF)pm5SHr?k0Yv5&CX}l7=!I9E zzst~Y(#J4fr?l6RzQ{B&jk52oNJtpuO%n*(K%=Q)8xRTGT11qZwW#nA792Igx6U3pjn#| zDK6uVv?H7yjDs6WHe--~4Xu zv0ycCb&=ip+Qa%4yy!#>%!UN*Xdn{gzEh-F*0jx+6sF}dMJa+!Qm7p>Wd>Z>uW6!5 z=s?xHHy-hC(Vy8GBo-D0v}ItB-z9bMSoZt~xD*v5W9j_IATMLzs|96Wq&;EEWo8H? z3!J%W{YBKr>d43SSiW}yvScxOyEGyyd4+!_W835{!Yu>)S_J;1YZd8^Pt& z#JNMV9{=EjfH`YC`t9)|JFoseV(T%!9ZO@pCrf>`qI#(b{HZQFU1SgbW|3Trkiv<( zSUnv#^zqqz<3k)hr$6<*AyIop4~!`&qoe3p7-8-GWmD~ljjtmwO2(&op|4+=f1?4+ zFo+}}kjoEYEvzbzbI_r_gL#xR+nxsLj>uKEBy$|o{@ZqoTi==qMH8V(c%1ISdEXQ; zTWIt|jTcWeKl8^+6e&cBn6}sUr(|yB1|4g^3sr9Je}jb zIU?>oMV4NB?~de2vFY_cihc2-CTziz#0_bv9#1GCim+2yKx%BwOV+NRuKT&t1RLxb zuOJqMekYrI;QW4WD8rEGy$^hmwr(WkdF{!@Wub)tM&CxLC5ND!Gcc(4bHb%xby1X8 zxAyPzxAx?tRo^8&iQULDy>ty+haA-VfHjfK?2QZr!hd#55Nk0yy&@#9TRpL8@lfn{ zjfX6*s){AG>6M5td!_b4PlB&IZO!aQ7ra;U#{&tKLR$U*^<)0yXVL%st$X+zx;cJ^ z(6?V>^!z}q51>JM8k%(HogK;{@0n$Ly1?6lk+1kj9#l)NksAqR!(uV`v$ zJaWE|Tl|`Wa$TqW^%uIqbc*nt{P@fQ@B^FZQp?L~ka4Dw(a_=ktMqK|YVjq9CiJEO z;v>FpEVCJD*J?2uL!|U`V2p;Zy4I>)UpXtImxUYT+tu79y z86zA^xzIYQX<<_2*M>ZIXa0uN$Qh3+(;H09TdMoxkxD;G%71PrEbG1fDg;t(Hv-;g zhNkOpM~$5_qvzn~igSu_S5-N*NTG2d5cA!BcK)c$;bTem5<2bRo93U5e1l6#J25N( zs_4zQeM(#*1~=*AH?SOySeT`^_UOVF`mcK{h#vSOA_u{uI;Qa4<^lbKDwCg*G^M*mS@b@RSK}3qo z+;@41?gt3551A^mna9Hr6(%79_DTq|M-QiEGVQrmhT#{S=e|#@5+6JUZBtCbS7n-U zz)6hJ2b8C^K0Tz;KF30ZevEI%B;QbQ*Y>>@A)F^1Zq)wKXGQ6mO}yfU*;z*0JkTnc z8-O`z6MyGkG^;jsbo$Wf4;${j#}{xhYQye1VcrR<{lgQbfhSjk#Q^MWk-!4x`Q~G+ zl>&4IKW1!FzP@nDu3L^CVA__VFwZj^G&WWO_@XJ>z^TebExkTbr8yay{t9x+ghRB? zklwgD`8#a6`(gg|-3EBYZ=c57KFD@F>pF0atc?l{tYXR+3Fm)&uoL~W795w@Omdzd z)bj@I>$TnMQ&|lw7nC7Nf>L)LC|B@GL*Pdq3!LRv%2TW-u}qpx66Jz zc{g*dJ&_9wlFNW=St_~$C3ZAB$~n$CtwR0e`W}^oZMDR6ApK3k*lJAd}f3W&C zzO@SNkGHAr`(e9wWW8PRB}PBbhPFT@9NNUN3j+)P)p%n zgJ<7RjX{lxLgE{RB!jME>*2?x_hz7;$v(X(QWk%EcE>31g?2G3h16p$dVBO&5dhaT z=p5^*Rqs#WRaA{5L(N3RZ1E2D&AeJ7cf-wTnPjzteZvv3WYXG2*w~d?@AM~8&sQFV z>8Il#kJe0i!&6e7TEat}Kf8FoG@g*Zmg{j}t|XYpL)DUW=e$fqjKfYP;=WNr{2KKs z!Jh<4%eq8OG<*;8Q3mlzguWQp>aBO?1DNeHX7Nk6o&>O=OpmEraTy=sr~;h z5*IFyh3aEg0}JQW_;ICSNb`b$2Ln4yBstIK*VL9b%#1w;gG%K;Q-KVD&;ac+KN8`< zJ;V01n_(OXv#rw-z!We|!EW}$R_xf0{qN9XXuj)S(_#scpQuk>`9b$95N`RWi680nk#_vY7 z-Q)pTYIX?QW?V5Rs6V-DL#S?S`~H+%=l*IDa`?6cr5~~gI1&v+eoTEWt|kqBPHHGS z0@#4lb;Ym(dpmn#PqV2+Qy`#V(9JwK<1=(k{n7jfvfp*TT+$8#+FSSyW0q;L#tJ5K zri5Hf7L5}WtWAjKT8{=1ZgK#V3AYwy13)&Oo;rO{vB{$UkS3ki$69cMwmW;|7?pjKl`aB>C;AaFi(849fHyn43;7D0GHZcvW=ZHw z4mNkK30&(?d_1s+!>n$GasShH4Mt^{eFe^9ek5hrT#vVSLb5NL(iNinB5SA*ex;F_WMBXQ46*&_`7mpRVW zjvP*e)=t$d7t&LJ4BesYNYxfc@RmU^MxqMowfrgH!(VU+O^ZRX_a`3K7{&(m|TvO<}p_8xte4@&EgLvIV!XNLJ9H%10rJ2$G63h^FS3&cv zv;oh+g`l?SBeaaryhma$`T50@F@Mg}J2RGfqQ)|#)#0YOP%yrc+WOF}^f+gCXL_Eb zkn;NZt65+_0z+q;>a>kIw`)7w*<*TA96r$w%*@IO*Rpv&jh6q+nXQ$J4g=hcWJzvc z@nrGFcD}tusIe}*>!U@-e;6nBugk&g($WD$CRu~>lZGp$3&PJK-8L~fMUK&nEt1b8 z8W}n&kImX#zpEUkeR;?NrFI%iM3IjItyX}ky6LpYbuOBBBxF$7ZAMe+r~$9Zthikw zLLcywA6jcI_x;wGI#dI(!1vGk-CLSgEm->u+brJjy3Fv7I`R?A4>^;Zp=qA*mS!~l zBRoh^bs)0DWNFs%v2TJKeH~74>A^bR{*2f{fr%WMka~ge|62$hQuSOpNxb|o7mcO~@~F?a2tGY-v{y?U$w5g&FcEZ>{2}>w>=lnFbD3wGf9lmwh0@yim1--Xj&GsHlhnw0QdOnk6XNa%( zqU-DH2OQ8st)ALPpoL2Dx0v8;x63Sn92M$XxS;Uc(fZ2e-&ihJ z%ozq=Z zgU!2#BX{Jxk_vtP!LM$7I-v)iv$CDn)T8Z!+XVB%K5N|P66pW0v-Z~ z`|hA$50?84eqjgmbytvb`im+B9Y&$zYU*owam6UTk&(BFYtEpVUpWTC-}#3Nf+gAT zhuQ3u!%w6~E~OQT?kK_5REBGzGdPDbfoOiTmfti&2U`HrS~YwFbbU!WzGN9cz?%Dn zXq^#LjQx1R-gF&PiUKX@=Iv_W%BuU6cvL~l#uR@ap;%sjkv&vReb{fPy9 zgE`N=_kaI>$I;}8A-g2(5QJBlSAM!YUMti)vWvcti9dVbGQqa{kPg*7&LZ_nVWJXT zCgGOU{Ay$I4a-7*(0+15lwrVR1yox3J^(xCsezC7nkMV)E~gKl%DXBpR!yPKI7#+4R=b? z&ym8k)A_T)kC3jz3eVlpTeW6ryBnzL32V~FUDYZZEXO=A$ zhIsRqi`agMq943jBQ2e~9g~|Zk3dx&iJn{xdCiIhSjhNoGxo$G)BHn$yu-y7s{B98 zSkiyX21RGUy*CnIzN(mQe)?_ZWc8G!Rzt(NV_*Q3l&n|gIH^hYh($?Wv+5w zekb5U{Rc1nN?R}GTJR@W{nANblzH10tDc4tG(Ej;qHqF;XY;_yUKm=ANbAD6^+%zg zbyz9UMy_z3GU!kd*%gBxVzurOZ}Up+!aN_+gQmZiRQ9$c>cL$cNYd(nCUo^)|7e(M zvZjWuCBv>IejvXcJQ(sl#k5Q~svbJ%$6!krt5&VMi{zXWaVwvp!e|?%7jwn`*0z06 zB&WK4PwB$TX%_~GtP#+Hq2%O{p*fJRNA;Z4JkHio)Mr_w>(kC&UUQtCa&8iDP{~|; z$%Zy(M62xb2v-|pX=XRWb;+X#0*zDxHpKF8b+*L9XY4&hp_dAmCxBGc7L|F!ZsO2* zy*0S(X2%ETFQlVvKNKsW>nx-vMz5NGfAOm}Y#s#JT(C6GT$zQK87<&hyQkl~)gO%t z)+O<_UWNU9Wn6U5S;984>@(m9p1e!iYYEF)YWR8F1{ zr|YzQ_rzEt%*QzPXSaS+Nxng`i+Z&Fgh2_3Jo*L0n;Z49Y=S#M38!~T{wDa#!Y;rt ztPD(bf*66FMfq5n@|O*8GS=EPxQ(K~G?T}&qaYjjqOzb~5`clTdWjyR01*{Tx{ zYCd5|cA^=9t-b&fP>;nt7$5St_$uCkAYKGtaSgH4yh&^~BxK*rlb{ z3MVCAmi;Tm0G}pY^_S&jlG2A+3QEemp-JEhdF86ZWwipqiR^zrQHuZe8a@~|d`XCB zf)Cx63T<&-B=k#?!4pPb=vsd-3= z*tzx%scpa-BAfJZi>2Q>=x}B(R>c>A?PUBgST}opB;~JWZ2=^&x`^vsU^@qdt0;u@ z4VT|mArDhO)7A}rI7o@T-ZyNQdY%$FSq$;E`4DIP*_qT?VmFl{qDmxCI79d~cVS|M zipMoYR}Z0XmFkB)qCF)en&g+7pGBjJLqsE@{VKBZvk!!7HU->#ca(A5b4H+gC>VY|QRt z12J|*h*CkIF8?nM<>cc$@M*2;t>XwWTAo02KUOq^!)1 zxb=7ko=LI1*y5$2fTm{sla!sXUN(!#x631xgG*z5A)V7~q4LZglY z{$jfF02<~naG%O8!*hLiE#;|T``W4X#dLCO#CZuiXoQ$Df5LoSQqynaR_Uk$g?w7) z?8=0Yp&Qz{*0St6RJ|O>9v7Hh`z5m{muU zq=|j&Q8#F0Z1cEdKl<_^sf~?W4iSQlH{D7F(J9JF>O-Pz$Tkcj{^ER@E~f}SyR%ss zXUcfOmPeC!d{Rl|H!IQ~m<_Qy1B*+3%teyJNEAE;bG3?nX>>Wv?d*s$bV-*#k|rB# zqiG}!o!zHoQ)Vo8(~O_759S4ax9n6U87L_#?t}yc9l+d{e>#`+w~w*MS1lcXH$mW~ z{|pe4gH%Gf*;cGohCMhTJ1eZ|_2xx^s{0R>*M)bg9xhh8~{8_JE35{_FL|H(s%N-8`ondH#& zEML}dOhQv`-Llgy89kE;o{7Xa&BWe3 z@m3VuTkDh5d%A_w&!(MuRzZus2j&V_2NGb9%`Us_N5w{37WMzFjhKjjEbP&ttH|78q& ztm2WRE%f6XN3GjVGv%;@q^NRjU`h%Ri>}UF?~EV5jy(6e+bsHyA>5 z%{$ADi3Op@BTA;Gq_(&K$uM8QfSyd#bdlHasdR8I6uSE7#^hVd8dH^W?XJZ(qSQkv z;#F1(tH{Q;yY>$eL`#oqV-iMgLzEBKT0}wg6citezV@@;`g!a4weQR7w_U!?&orKt zad5*kDndf4MW~O;p|!um+FwvhDxm~fWvnRd$aS@ld*4uy^wdELq>VO*Asweuw5!qu z&2QWhux<)lv4TN&0DSyy1;W877(?!pZx=kT-`nKg-TaN!OUcH%}-?g7L*|bGS5TE4f9Q0ZiwQmtN+tqf}@$!|W zB>aneXNJ7Rx8(9kGT{u%FQ%82ZWIZOx3leC8bDU)@Slv8{Tp_SaLm0b=EgqX?RXqc$Ztg)>XBB6+3pyHc&hk(xq9xwAv{4 z@)eJLqid1!%fn#Q>9Im7hBgBp`yIi5XqsO)#0p!95m4ZUdfAl>>hx$mzq*cy^J+bR zq>dW0v>0WItPA#Kj5C%hm~4T=;wC*;--(w6YtyucWeZX6!Y8 zpWTOySxaa7Vqe$8_kS|l-xR>~|Cbzxj`CPC+}2xol>!g~bvW0~M~1cT(Eyj2)ITTI zYQ&_aKQ{XvWA=KuS&~;9cUmZ;qFxCkN0I1@e`U2LLLdXnb`H}&1qWt?l9Hy2**RNh z<=3@zyA$rl7pZ)fo%+pGIcLdPJc~T`OQ>#to>!A7m2&jl!87+zh)y+Fb=RL|OOw$g zJKUv_{76w~WNK)Z0_f;zyGjoKtQ8fL6`p**MMT_atM`B;@(gyPL{EkFuAL;? zh5KJve>t4!$)CQWLPor7b&0<*_tPO)TE(~wO`5;9nq`$`*7bk57rYjID7IG|Y0|&R zqCd=&HRU$_ITnv`_6VR^fNi3|2Y+r8>-IeBAwv}F3X=SW;|&+Os70E?k-1N?7XxlT z|H`z(#Xr+8>-H89AKGtn1>g%>kssqm63js~G1}XLQio`W&f6B@6%1>N_j{K1k#q^-fveP@_uFUy^#7W3#^?Gp!)~^sAyZ z%HKXNWoc(RT1rCIXSJ|3TOc_HNtX=>b(p&`86aq%rQW=Ee{}hCoat4JDS$+|WTVYU ze-6AyAC7PGlkM{ zoez1Dm85-RRjXy^DRbJKsiwx`@&6>M!!^?K%6#xEh<*uDZ^fE1aW}R`LBri8aWc-C z@Q=fFBX=l8Z+i|zB-&^Wv-M5{f7NNXjlnu9PbJm=h0%cwXK)P7!~y(ntKLlAtDlG; zY%<-tr*C3>gSCn=WcRvKUoa`v{do~6W&Dv}qIL$iHnDv)cV(S#ab3<)zE!~G~ zf1_`Rd8gG^iKlyCUikbUIOiDqR6~GZcX#(P2WfI?^Q9cZ@5rF(!^zE}qGHx$qyDx@ z=G~n9w{RY|_$1<8hx%}a;9>Wle;dDXaQB>a@m)N=1}lhmMthp$wgbK)(mDr141OKb zDx~t>m6K7Bjs$M}s9VnpTR$4QG{2j*j@hUbbhF)N7&xsJKDyJY4maZ%bl&5a8CD30 zd`v1%J&^JZmaq8$Xc;HD%r*iq07`AV-bBe9?)^jOgAy^f>WsV;zbZ(G4ZodWR>}-9 zNf@zB!$NHW_{B<>IzFuU2WMQ#TJLNO1Eq5+UH>7oO+G4n+^H>Rx?jahspr(%mUo4Q z)FV|Q%mf@>=;efL_L>ixt~W5!H))?fwI=mACOT`G0}+T$pXFh0-zi+gF@}PZCYf>AB8^{RHTwBq!1YMpNxxEB@hAX{?EKK=!Eaw9*LT zaeMOc3js}G=|PI?PDI_OscBcC#M}I+FAA|u34xA2P9GS#2ouxb=VWwzq6BJwo^NOKZ0lMyJcL!Iug&4 zZ8j|}?y?a70IlXS%RfM4E~jzU&L$J=OEomduLHSzvMQG=&@R!3XAK9H>>=WpUzgtW zaHyX{Sx zlas@m-5@N^rw-xd;_7H2oCD)Q6VRKj7jx8~#{F4<{8%zi z776|FeCypW=}5`YRK3UX!~Rrgw*psU<25FQI#{G`KCjD?rq%5%SdotZ`i9_JHJeMk z!r|uI*|z#C1>wxT(kG^~;I1Xr(%aYGe~r4)w9#@dDp46&8gPDXpme-)Exj@S*p=jD zWMZ`2=%t_IJ7uE5J{+y{xw^ev@d;?DJxYfWJQVPM8Gj}T0;R>ohL|-a7r4`(9+Ar} zuf=@W+1!{73zs+PD>nYPjL!Cat?c^uSxp}0r=f#iC-1?I7`-fuTLKp@2Ob&O0ZZNI zBXTh>E}8PBYMC_xGl`Dt)sogFLt($*^ZKi|ERD}7cVBS_&{IckoE+G4A)T6ouTD(^ zZnf_2(PdL%)d9d$rtOF4m-~lhSL=nlZH@Eft=d^qzZ{RIrjj~P|Ej{={O`h&@imXC zlWIJO81RZ!|v6r730MKB>c=NQ=0Nl#RH@1})5=&50{ z8s@KVzsjfC1+W;<oe?gP@EWntadco~rRx@SVFnr@ni}hPPb} zX~TnfX{_jzCs?~t)&P`RTS*(p5ZfrPYdDdsh@?2lc%K|NL=mE%8^-QZ2r5To$kMN` z4(v9(+8HqKuLo({hS{{lab{zCRiYGJGEFkFqLNK~uTj#q@S&z!EGe%`GDivDV&!N9 z>bAgtRR{dxgeobCh1|h5P*=s)_bCK8^w*K&wC!5&9>lSR*PI1UUllRkvv8k*a=v|e zH##;Z`#Dzg3)Qt{X#Y`0-A)jtC~%c*>`47By&-n&B@-nHx5TE=1Lhl_jNN}Sb2KP> z)h~Y&r&?I^Ea1B1T_Xs+T`m19l{0~{;P7ph6;PvF9DX}n?_Bpsjr}$*3@~F;ki~Y4 z)n(=9y)qv_Waf9T(XsF3s{ov1ytH-UujbY`3=tF#MqHa&VHgvpt=N> zDzo3br>-_Cx0K@~Gh$(T{59Llx3JXcV6M+`5^zyrLocO}#q4 z_W9w&58tQ)R6-rJvj5cAlCyR`fbyAISh>9u)4At%m+Ms_{He#&V;!N=qLPTtfW8tS zw&&vUG?#bkvx&)k zqDL_hu4fJrkfJ4ZX*sm^54wiwtN7nuj#AwIc#G_MGae?ZPxT}{{iXF zF#ETvEcAzW3YcY8K4$Wfg|S z&g|g#p^vZuhr5E_>jnARh2v#@$HGoCk|Dy&3SP|t`zic;yOZ4A7adJtc3erFu4(l7 z926TxgrpHZ2n*TD)w7GpcJ(xuMoDIgO;X$T<-NDGgZsNPAxs}U4l$j3Lep4XLSvo*#Ut_4QCJs@s5Vx)}F$sef#z? zFlE;Y+$Jxz_KPH%-t25sTSyXZET-dSO!0!zH?QFMCGyK`=lDN<)CL;J=CC}QEpGAT888mp|S~Dvd!YJ#$ce`*KqfNUKwf&?O4Ac>`19%L_x%jCR{tEXn>Ql4QeoJBKuO7Tq604uz#drsZda2(ACYV(8fJf~bM^hMwcup~0&W9tD5T)EB;=hk8 zZ>Wk?3t3Q{6FEn@v&R?}0XTO(b*R^7#ltchvZXLy4%%es7aj!} z^1q;v$!CBoCCNi1+hA6V9z2(zYxt8) zFyt{u`)Po^{fqqv%~wk1?XAK+e=cNYPx^NyTG320s1KsDVnO0aGb<~yWrv%*mRJT< z<90orp_da275KY;5;bl5)Xiik3In0WFot6*12(IGk@BXbiXFDlS!G|(piq_mJWZsj z>`gzHt{0!$q+{nxELC7iNS_tz98u}O+P|shZ&v1H+}xHRNbh9C`d?aU*cw>87BS66%KT%(&;3Q=6XH?t+|5EMLkuv*!Zdh%zoC3x@)+8z=P{%vbMN4FOPXsVjUVZZtESU zT@Ob`&BKm)%(x?;I6 zx=~om9X}b^aa?OOb#V>kYhI>QMM>_`v!P7PZe8J6f^V8T#*A z)y&5KZZDm?h}cL6DEOkp1cms|DxqQZq9 zwBNt-(r7I`=(~nsQjlx~v?4kS5@h$=j}#+!Q*dJq{5 z1m0rHNYqC?NSBEr0tiF~0IB1LoE_LprfGb=-_v-!na$xRz#R2CACE&FFXGnL?8zR* zk4FUMKx^NP&dGRjYMp)D<@FHOsI;|qJE53xY0vew9h3v5$!8N?o9Tv}s{L6~*R@y-@ElmiHDH8eO^3m=YN3vt{Lq5~>3*LzaH4T)YXLt5~#WcEn>yVPa_j(Ng_1XQ= zSA-^b^|#o)3kl|WpK)FjNIdelF(KPgvpACa_l5uT1H6> zRupv~!ZajJx;xVgjaHl8@~luYvC-=~zRRxXjQ&J7@52kc2tP_>o|3F23naf#!=QX< z?wf81I@XEGL>Z%N&5O}Lru1?r5=48svz~YiL61UMOjQc!Kbr2AmDL6M7SiUMjrt~7 zl!*iNP#jEMV>|C?=42ofPOYVMnM!nG!~%&g(@=;>UU_z8`mBY3Ve`LC!0kb~VN{(t zjCJ^=jlCU&!ktOzFj<=|4w`b-y`(VckCv$ssAJ$=km7@@1b7y#JpOfvt&BS9lRF{xcW}o<+ia+RKX`%Ds(%M< zHh<|*#TT68`r)X8b|jBZdVbbzEYOFYFu1rSxz(J{9%Eh7+MSrh>1#as`9wTAUA4Ew zx@~_}Bh4HL%t!OA0vPs6u;&4*W;$4yXLop5^d7%Vgk_cK!QZoIIky+_*f{0b`g+-_ z_d<}@gJ=nYM&VwIss(_ditI9CVJ01#e@3WA{p~Vlk4!-wdZ#-sK{yG0pPx08TzhPBz za6;76uSTNh@Nu~Sk1&cxN%tt=x6J@8=HRTb?|WUX9C3|$w3&ZfPXRzv8Qv>cVwf4E zGM4KR6%C19Sp)f};w%mMZ+N`l-}eu*==I_w!DlMKIBt-q8qeZZaD0dggTM~i7e4h% z6KFF(RqvHGRqvC2krBnD3pM&z{%OXQkB}CVm(N#_F9kbrTt4J7CLX*lGnI6co72I+ zD?*lSW}ug~;5!AUKA&w6bImDc)*`8{_J4BAN8Xd`P-lF?IYowLdS4S8pYg5CxNC<+ zLMLm)ll?v?6P^YAlPsP0+ethWTBsVi`qvPy;O&1u?pIko!bciS)lpZ!~k}rb?xGj zi5_gnrSAV8z$;Lv)dX_WascbOTXZ0 z+d|SV*q2^@j*4}4mr1it62HQCYXtG4FN4nG-0UZZaKq2TRVM{KX%7E3ukr%1I7ww z8_lIAuKgAI`6uyw%HSVDs2}Mb?`f)T3D&I-IYPD&#omMy5SQ;FXIaAr@G>=hl9NXD z$vEFJHe=PowEAO2`}JRMH(rlDD;)iVI~VvW2(W>AW*j&rDo5=`-lN{uw;dLQka`)1 zm1`LO%n$~meC9^qOc?X`cz{5nf0uEU)Z*h*~=B(I*DBhUkpCQ-)A+wy~QkUol{?WO#0_b<^3pIfuL_Q&dzG#z8?r z;S=Hf*qWYS9@e<;6A-`|M%Vh0AK}X&Au(zv>45P}3l2$cz^r7png&h}ksR`pW1L$Cntx9*Bk7$f7yJ>G3i-PMERSTE>Wro7cOmjm6V7@AE9#!w(xNzK z9vZUop@@D3A%q%wJXU=t>pnu|Lp~<9kE}E@;)s-7--j=q94H9hphA}iG_(>$5j)7a z_S~1-(D+eI1(AQe9wu;;vn%mVQ7=Hz4rZpT+dR`e)&hY~Vlo>zcUp1p!NoVzGgI2@ zL*$regohq_md`{a_>ReV>UyrG%n1v-H>`?cGk!F`Yl}v;ttmUO)t=F?tV*W&VbV1` z+OHsiaW|u$rl=0u=s5gXWmyE#yfRsj39Vzf;^|Xcy{Y(c0)4`Nh1uQOA_bGrVf zI`{M+1pV>oSM+MjmO9(*&)>c9isi{_@ex^a%2EJ^HiLl!;P1U8^&0E+OygwE3{Jcx!qols=WkRa zZ)LC{KKkq5VBNRr`N~<3|ER{il8jkMVmE>W3HuUXf163FR5n5O{t-#-Y8zf);;o)0 zQzyo9th~7M?nTFWNr&Hd9ffXVEE0LQ>M`Z02W2T@!z?=`tKCdWv5>J#+>0@^ehn6C z6yOQ1tOs4<`Q3=CC50NLmXT+xN%#AbWg<+=_S0KMn0sBxp7^ zmp?cdAM!%0nE7`I4Lp?EMY^^ICXP$ri6ocj%T$n1^GLuPrFmlHMVdmPOl|iR1;4Yt zUVTkHL&fe`S0?&OR+IkOjl;W-Ii57f5Yx*0LkZd~bLMn9@--j7$Km;H%X~dwSRh0} zT3EL!Xi5oT`D#+c;#lh3&D@$;r1j*EcYWSCi7j&yWMxybWJWR~K;E+*JIMB8p&|7( zDj@Gj5$h*UI?LwoxyI;PAS)}QCg#2?qjY8omTnGQdmrrZ5tBH4^9?>?%=>Gq#%+)+ zZsVqF-4czwPm`?Psq5_%lOKWqZfGjA{Kx%q7WZ8+2nxR`kVsr1Fyj_zL+@l^!O?=V zbatk}bwK|qeq0_cT%u1ZmCq&ow;WA7flh0*vAoxHqTYCee00!89p+d6u#h~pM7ZDI z-WwbW-r8oXzkk0_*U)%@20R=;-}bq~BTSSydJ=K_jTw)!WOvh5yV-g0i9Nouf#tXg zbmNue5-=fGU^W+QE7eTZw#Zq?zegs`aGc5&zqhZxIP8oMZm^ zd12ba)5n?(2$}sHU&jRY5f$KAL=_|hEeiOytGJfGa%QpZ1HgQD)b&_S<s9MQ$=2TnRdG0YsB zXocuFMK>aD-`!_?j#2ssfJD@R(N=hDcmGs6_Ti^*mVv2U&Jy6k60`#IEy}Pg^#w}7 zG38t5Z+M^Z4?N$Yi9V`msB#fbuIw+SA!I^T>fz4x$riuT{4I&G*d~|#!Y9yT^j4qj zr2>%4td`X3`P@H{e`#x6K2*Ru@sYJuq^KVr?Z)tGTkm`?G5aK!`B!`Jj@bjycxcSBE_=RarkZ>I;FPpzq*&x=JEm;nMgH22#6ZbDOlF6{d@RVoed@rBTz92QW;}LPS7gt>sF{IXRIhCv`+4 z)$eHp1qFdn&qF}y;sN=4Q)a$}CAc8>%?;t|cYxgD(@@3-4Q?3~q~N3A`i&iA&)=Q- z)2NKxk;Zwjnkv!At4yeU8UeE*BuJUfVRXfkgpA;3(R2J`#SOt#jkhjv07P1!tEc5xOBICgt3)C_#xtaJApS$OkO4m z4_wJl{N%N6ODzfiGIc=e%4yKtD)Bt+YKr2eHNEM)F-ziyryQB}d90etD38bmQo5#B zD(|Sg*Ln|Ypf3<`f!H!1Zk-@)iGj;I50!4lOMe{H9K$$KlbJ_1x$7L^svkl{Cc{Xb zzL~j>(RY>(l=K;ytJy-X$t3R&cZl9BOUq+5woNYv!g=%~wv@uEITubflhqcG(`C8M zXK%SPgg1XgMVfYOa~ck3U5=-X>ZiUKcGAAepkKYmD#GG+RV=@I=~se0)N>7cXV`T% z!ysHV4)OOSR~r(>j@J|6*i{sn87+d$$h$qgn_p;WanZ@$&kMaCen0u*Vp%ll*tnArm6jGq;PLVcrOoE@f}P|HxqHjeOSQzeaIU59ry>PpNcYG=S!1+7R9*pG`64JI}0px zj%W*xweI%{^#aP`p<$tA06W14L5v~{w97vRu(rmLrVKfwYl|1t)ZbDv+D9hiEId7# zt9^~r8)%$pf~qqHjMLhCWxNV6wVvxzDCAE(1wZ75+N)zO)qTt+_Z)IRe#8OJChb{s zy`Qo0wPd9b<3_JHK&@BctXoQsbYZlJz+JOhlqpR*q&owJ=7mL#%1j1z#-iMZJRZ7t zJ|WWL4}PuM$A(9w+%K(Sw%uMV`%uS40zu2Vy~L3Z%S=qX)u}lR+mg(_6>cTGVsWDM z+?h`r8B6TAaD%E*6%`|(2{SV{D|?O$3VK8AHRmrhP8YMJ&8ddlaBI3V@3Fu<>(fa) ztBDJJ$(1ye+5N$pTq4R5tW^1tMN1g7tIp0p@n($imgR&Nh1ayt%y;h-1(=-DrtFm1 zn`ck32E`+IvFV+C?eA+d+%dWnUGa4Dp1WcE^NgF{nw=8jEV5N&7%DqH2t{y0=`QyUHSRD3Z zq;g75Iyp+)*`IacqFd&M+U=DU;jC$p zs9eRh;N{z0j@6*5njhQb>$P16^{zYD4#%^lVjJ4c2%LI5Z9_sj-UkJGWp5lY1$wC4 z7}jYU5J)9otaddewFVnCF%ib)O9cekKWp4MeP8tNV}-EDe=}a}%R{}GkB(@P(Tbdv z)zxr++hg<4KGsm!UI>fZmdE6hBAt@J_7MCi86E@>68fTAccv@ZEpseMHbOz8(@Ayt z6B{&3l5F9^%u%ejZZRd>hC(#BRZua0!hcw9$FdlPncUF68KzS&(%QCOoe`&_03D6l zVUKseQ}R98Sw^FD*DN+UO;+WYQ)57)P6KP<yLcT@Yj93#u>%clUDeHS>wU+Y3ucN3i) zWA+K8f`TcqZHc+n2bQ=G!V$cEPap2qSf*)E#e((G)3XCB&rPqr4{+}6n{_L&v0^YI ztzluA-;{r`AzIQs<`-zH(lnNpA zn~1)@&*Ps&{1A9fcKdeu4LPqI=tBCX`yX0IpXP=48|nC~0M}rmYucNa^DFhZ^nfE1 zzhy&@(@n~o06W{(k9{cf%OQmul8kQNi(PY`SmCTsibL)LXa|mxFMlKF{)N-Ybqn)n znYzx~`BOg1sNGV;7>TvD_4YuUqi&70aB)tOcLdWG>GV)?t8=JbwU~5d;)Z1|xQZu^ z;wIk0co`24hsxI;UB7y%>#6jO2fKW%R-!kN;rQ7x>E0@D7#((IsAiz~AnZ0{(4zE;x`sC*dAO=Lp@+cH(Y zlNFHWbW|)N@6zrw%brn=KSUi8erv(Rhj7X-xcp+C<&4-#-unN)tiBuwu_*OJr z(6aQtBZRr;n&@4PVUp)+{gUP*zvrpY^73_VOA14o@^OWFB3Mi=bZUOC-A*B>IBhud zUf?GZH-ZIX2h?lC zxH1(yNUoOhm4(cd&eM669_fCuJ>P7C|2T|!DqMMJL%Vo>Lm;|{yo9ENJT|yWEQ-H- z!_NBNi4esqRfl{nT=&+KGG^jc=|@srDqi;+5vXM|A{p^aeQ(5}HqAsl9ThlP)KPHt zcR`CX_0{hr*rglIc|Ar>OhWH+7TZiSd%UPB;4gbP|I{g?`ICWSXCe@)sBZ{!ZF0LA z*U}K2j~WLnlid518Bm_l6nz!uIWamEGnG=Rvy6s$Q#qZWx7FFdwrmr`^%Lx6t~#eO zaRJpuaa+Ngc8^q(94{O*`>)h;x2r7>cxp@=J|?41o9iE3 zOz9hpLZj-b@xto7T#G-h(>JlA2PuO$jpI2ODu`^(!E`>ij_YWEZLadiP(hCWE+~&a=Ur2 zW{2&z95-uq2dnr_^#iuI|M4&k#(XEL`X4`uS)uu2P3=PTRbtE|ijhAS+dGm249rP9 zBRM5Kf`7Y5{sT#|HoyR8nLKG`HG=F8{OWuc$n7&~DHE7l_F`NYQ*04{#X{-2YGeKt z;b1Mz4LHF&xwx2FTfbWz?Jz~}=(Q9*cpTKe_}fwalVl~8KWyU@v?z*bP28>Fz&ZtD ztTOVf#PDmCJAq@W5ynY%)7C~=`@XX%HKV)uq41<7G0e8SnOMPVL|pLM=FoTNTWiM| zBdj^i#+zGPOY7B+b-v5jg-z?iw~B>ir89;urUoxgb;`|beS$vU(?dsCr+M`TOmmuE zvhF0LXe;l3jHo7fWd_Z=65acBpZ*s-@#%>zdZ{l`1@-Cn=}=Kg@sg$T5oFeVo$20} zlmpjMX&7zNo9`E1^fhs%91A|`5B*Fm?j&2z8Qkb7sj0RYbw&r4+2nF3PlU>*rhrYp z(Z#bCI`b@l(a_H__(HVjHd$4}0v-LH;_)W#SKsi)yPp)Qt+8RfzJ(u1#gyaJ+zhsN zbiO}06VIHDCM-`nj;oFin57xRJ;Qu=ZXId93f2-DD_G5w%)*x|T0EPU`NE2mDzB=r z!F2aVMs(MQ*j(f(J0@>xdb%CJ zcE@-%VL3_8oA>()|H%#S&O%Wb07Utn$U$=N=$sTpsd{kMEK6ma6cRX03XkR~{QW&e z>SK>VRwC*1baK4iPlcpFWya{7SA2yL{CyriTxMg>@8Y=(s(AVRpqRsfeZqyu3dR4& z*joiO!M<_BbV!LPEnomD-OW%GRKgaJ7#)(*F*Zt2DHV|}v86jVx?z-z9=Xv224iFO z?s<>C|MR>D-{E$;-S>T6zYO)Oc#f&>m%=~wVb%di*IWYe<5h0un$vZ>NSY3^E-tm6 zoDh26*Ib`0%@P&kF1VuIDd$I4R{Mbr0#5_4-jjF(ox3 z8GlWOpp3{L6Ek%N$1;ByhisDbS`+stCg3w8FhnShkf<3@+C$JwlpcAZ3t*7P-|B^) zt5tY#d$lO066)hq_4%~!xlx$r#-(!qo#7Xzq{?{=PdaDrP4xDxWwWn*i&z7o-f#7| zntJo?w`%qbqWZ+OkyTJ(6uH0AaNE!7)3T~a)C#yfVd`YA42gM()S@9RjyCzGQs4s7 z-PrMRo}0wV>gPQb5{~*Wx6M#??Q&;zNd^-~K5A{i< zFrtvnw9g8NZM}iB{hC~dn{@Tq8Rd&&BtN+#^sQNEGWcQkts!j95sVrs-j+%lOUeA#=KQOOz(-CeDM) z{=YNRKj)-Om)+(OZ}6OryqGrqe0{?rnsyw*`E-Ev1QA zreo6jVZ!df(iXru>$`n6{SWA5{_OR^R$%G$Ai#uU%B{}L6`q-N{1dhd2+ z0>kfK?K4a{@Ig$rktuv9&G~-mp=@r>`Zy%cT_n56*1gj29YeaYV#KeTWmTNo5cS(E z_d(3iAX>6*@nT7DG8DA^`R%j$^X?A^GzIE#tB;G_-6*c%q4s*Qv=z{fW7%@eM zXL-N$$H7r!K;(CT`b?Hec9)Sy0+pmRE={_lku9>nwK6*u`B^=Ltl zaC~dvPOm1%)ZWpO(PufKB^COD0Q#lmn#u!g0*v4h4a4RD+on@z2m-*aYB=m&0e+cy z@PsF~4NdKNI@_=+qq)5A%n=qDYSeWf_-RLlK96fEAqPs0v^}Jk6$dR}B>7go!oS)i zs69VDJ$grn>8yc*r&1x(p5I;E)-Ft?z9(8%taff-r1(T_n1&+ zkjI8V^TSX=UcSD|E3XqZ!?r%Px2tWy(@P{plI_FGCzTe)8!6~_&onyXqT`QtFX>9l*_`cT z|8!*={X(G7<96ow)lsFk9*}_kjBJzVTNgV)W4^O;h0ZJ)-P>!X%~3^$%%~HA`Ee%4 zu?9Mys=c`LcX0D}ihfh)t1y?$k}o6V>E@XVlJDF*tIAX75mS|%I2tf9?C?@#?wr9x zzEp)JG(>e?IKY4)!g*HiHw-+$s0^q|R;4_gBu}k-g z-oh-@LZoi=72E34<0RgQ43Cm(kFiL&XQh8Y3wh~j>tmEo`LHtIvQ3;44t>@|K}CAi z(}O`EATTb1904)#OP`P!lH^+sml=H}`F!C|8n@1bP+9T|8zF0(xmVM3)jy@drNJzB z%=d%h?T|&X*BSLP#w5-GD`LtL)qResY6#FHj|JF(U3)I%>tT1kBN-4`6ot9mFt4DEm z$?WkcT-;m{n{eJM7m#7hMOg$;8BW3a;==SgdEy=^S zB)B)sS<_cpNq10e6#@h8UFJSke@{sH6rSju^LbYo9*Gq?PcCTV0WUFH>a^TIv>qKM zy3^F7$w6|U85#1k^D_p8TdfCdZ;B=2BLHyEY*e*<9*UChaNsxKrP@BX*TN+p3MYpi z&xe^BKbdLH9YpVPu|;Mo(K|{T7ObxIr-h<*kk=svqg}&6z8PXK|1+B*QW-}7cW24g zFlTC04&PjBUW*= zwK0oM6{$kM&M==*0X0uzbfgkX@JA69!u)x2xCA()OZ%ffVcR!cKmH40qfAAO75hbJJ54r0d-8#oHx z#9qPe>3FcZiHzG!uE`y%ZKNwFfHO|nL4|`kFy3%++KnuX)Tjj-uTD8kp#%x=jNq3x z3x8fYG=KAm25@-kK!%kwl&iT$Bq3t$0N9h$Q(r8JoIhg^@Q%tV?;Sc< zG3;!69`vS7e7lwwb$%hHHcQD-Kv$ zlVU;av&f(!yQwPGV9s5JZRCiG)Dq|0a|4t8oTf_c%veq(x(pys6EeA#n z8^0p;J@T?`ekV@F^Vm0q2?Fsz>LtybuVE%k4Li&r%d1wKkzsIM3eyM-&oH7(y}UJc z)l0POz-fBNZRvTf zqiE3Q;(#hDYr`BnJ{S9nDj)sA3fuZQ#axq2Pq8RvTx4z zbN$S5ZuXqr9>{8{u$0_7m-Rl6fq^JMzfkA#;VrI#@6BU7rb2K0h<4)Y)SajD*$*n2 zlO`6#=0xF;yKjbymC_}Gb2VcO6to?Udg)5 zKi?Kat5YyWaotue0%P5|PLfm8Z0tr)yrKYkm?sH6Cw~m!{ad2e8ARxa%C8!#UevmL z;K!t*ypuay@c^ke58G%Lk&Aw?R2w|kM0J~=5@40?$b@QHvEjQ?L-nR1sP&5=8Snud zBFqED?*9Dog#J6@C9ea$x3NjDPz-plFRgVdjZ4E*x9PNHNFbh7qo&MG_f?WP|Ka`_ z7T^e@NN-{cQG>^N5iaQ`uKL9|0|X9QcpXEpB%wd zX~F3Ki{tu;{&ztc^dnYspe_lhbs;1pRM-?Ocb27^BjYMo-OG!xPj1N zQnU9IfKXa_>d&}Bg1KcfCeDJ{S&}IaeeIO7=FTRN%e5AkFCkI1UA6C`UQuJp_YSwt z=i@UcHaewP$ICWyhHioR%_9R3V8(auA>dc)#{iA}I9K|cy~tZOHMfkL-Rn3Fa@+%h zquZ>%|6<>+Ri7=04SE`7QrRGHon#hgl|etqdF%C_%ELK2Ww(RpcO~qlFme|Wl$P(i zql80|7kN6;IQv^4ZhO1gDjIRb+`TrJ<<5okt*JYs+gkBLVF*##%4z_2xw04XQN%9f zN`Bx>#l=5Y)E$5!FS`F!brm9wVZTRDus?pDfs~vZ%~u%dp7ViSq>cgc9HIMpJnUCJ z2SD6kA@Ivw6=V0slDf=M2{)yAM4gpc*{bS>X^HT6+A(a~v2-=2m9ia3DZw6ez_pa3 z{I*g7}Vpc&K7y2#4%zF=gJ7p}tbFv`J<>?Y>ipj#5U!Y#rsJ3%)$bQ^NRX4k{ znf>8uIr)s`rd!`HU-kaG7rjQMsvpz4#Rkm}v}lZt$j7OgIxtUvCp0c=S=p1d$|41Z z;IFCv&ef?-y{Ux<>w373JGnEsfO&CWmD!Dpzjd*z)_tHo&`jJHnXn~^%;|~!S+)ka zMcoYGYSDHXK){Dd8zx4g#X6$#OKi(?t*v@9X#7XyJ9B$?Fv?fflcoigo=c;}!|6(; zRm$RA&PG5{rd{>P?iC--`rqo5fdXE@ka~w}gYL$BLt2Sp}9dILBAwE7o34 zE`}uzT9jSB;=EI?K;pF8zSyHz4|tQ_uB1yIKvYhs`70P6YlzSGdHF5z*|-tdcrML@ zh-MAj8<%v6(;5d`S%Y}i0>Z_e0Vw|x2Cv&ovbs?3+=+VN+f4OlFsLpdgnTsX0o4Nj zMRuqnmH!_E2)nvfwev^~FG`K!^H^CJ(Yt=R8(knUorFWLTT~Sssu)VVmwGx|$PV!J z@?ujpWBlFbkzzE>*{I^k#}$t@GH_dbCfiME%`qDbmTRi}fMvU_L$#vYVAGzJXBZt} zxi&c~>ACmu*|VeO1u$SonbpJe@wkybE}&K$Y_E$VH)Wlox1?S6PeA5r0J|R1Bf7M*d-B1@DP*ylR*!9TX;$ad z*KS-*XZHes#>tDF;iwOgSKu0o+V)vg5|;sZRocSa8^m^bXt5;e$^7eM$tXE-^Y_$` zd75o=j19-?BKBI?K>0OGT^-P0w~Zy zVJb7J{G1T^rEi>({WBNu5;4379X3P}=ksbI<)cpP8OOe}8VK%1Gv%@0=COFLD3+=u z%GpI~X>OL0a^HIm`WY92>NdIf9b8~-=P8va6}DZ5Ei~ZAu|c>IRP%|I4`_JtmYXJM zOCEGQW5H~np>}CuLdx5}f)pE%appO38D+t2cQG1USolpGh%Msko@l;0yt>zl;V1n; z(t;XJ>nSXzi&&RaEvp7aacypo=qs_Sp@ZH$g)o06WGKaD=bnauV>T*({qUh@O2Fp! z(^ujrC#{=ZuSh!ur`N!dhtysH@ko{kJ{SUi3Eka!Ra5i3FCeJj;NGLOZxs#G6ZN74}Infa8?dhX!MyGmH|5g;O z=|k5@F}Z*s(hgt}uD%t5Ju)Swf}1*!HUMh$KQ(JQ__ z?GHFoMO!BVtW}18GMTdfKIdE)h97!0$hJAt_m-GOyHR!U)c4V3u3RBp}R%jR?A=3MqV=aUH>qX)eIOITKg(i9c zXNB;p2)Iv6HETiZgDppNl+ItV?l?%M!}g>)y8h_f()%db;%h5mg#Ym zu5(46Kl(C|W3>6X6vwA^vRY~9(y-Y_7qS2r9v)hr$$+o!{Cz|R!paWWGL(V*zFbM# z?b6Nr?ja_Ad1-vtRQh;^b+89%SFH|YjiH!#gk>1m#J^X8C8p2ApNI^1<(5|gPX!z( zG`U2dSlC_%A@YQ~0-Do_8fWqGQKv)pAl+_Zbx#ClenhM2@PSYvRJposMIY7nfUlo5 z4EHYNnm#`@pIl{F&PXpXF-s&O!==F#x5Z>MFpR0SU5r~Cka7@+fb&Y1zZ^BTPW{Ho zFx=Xb8sXb0-uqx-8RNFU!J+U)-N7f@)JR*`ewhlp-uXKr1o=wmG)+h>3>T(*GK6jL zcS%YmPxCBS3cwxLs44Fc6fa{lD0fqf;`HySyI+CO^nr}x?hmJwjG7*tpHD>9 zZvFetCl>#g=u4-gm)BSMV10h6aqGknofop&@j8(qd(83vMppbpyS+r46$2H2`F+o} zpa@qNh&P_!zW!SGjtNVKpS?Vt1HWEB`-KbX9&y6SbS2ToBTQw1JtOU!^gkGX2+W=} zspx^$Rqmbc!uH_oQP3ibsUe|G1nFF~A;Wq=wHJSz9p9z#K*M@!Ff@{TMp-m3(?wv< z&1NJ0do=^+Ii?@KvfsN$ManVs2oI{ZyXW;x$~9N;ks6ua{#ezB$>7QMXSS>f2K0q7 z?knCLNkky*wIW2 zGTZd^@NIc*#<_8FK8`cetkWl^;H~R;lN?>8CuoihoBC)dSm}0R6;=V*J^moa!;N8d zu+d;U;4e0<`BqwbCSeeVdug*+R;l69y=bZkJ?6S-79~VK-BX~l>*rD4{a2|%=h)@c zNd;oJD{;1Jx^wjjW6O7Shn{_h*x!8*5A2kGl)!rIx5|ZRfk_}q86Zqo`1KtA=glV? z%JIzN-}eiMR|B^BBlCM%Wk``fl+OyOTSPSMe(ux^20fd?Y82?iVl8k?q1^8SHR0Lw zkiGYOb^_O?1}5koD6e0kyw4nX>s+-Wf>^Us!z^DRtb08u(h-U$#Yg$L>e#5Gn{Dd5 zL7O*u_L42iY;IE5u1DuKqIoCRgXUENAlxuy;DlL&|L*Txg*xv=zG#Negc!Ou);Ide z4=h!l2-h&*3JE3csObjzcybpHWwF)iC1Kx{&gsL%xr}yHlwi8r9dn*fb%^_i?PqXue)^d*y|0i~O+3)MxU?pY-mX z31JxXEjvD7#Dod>00NnicnF!NA>2iTx>G>qr%X`s8Ij_mz3XncHL=4JE~5T8a$ZKu z_uXZlfDRDkQd+@z_B7TG)6 z&w}XX7v1o+zvAe~t@701E8STAA3L6xLCajbqgk2XJSyts=K)R^%>*RYwxg$i&B|L9 zG42bF8EZc)=GCMt2Svy1MR1;-WpTE4)gRcGij5FS_fo(LyCE742=jXr~o#tkDQZ21t0iOsv z9Ha$~QLehC{i3V>)!0tA!>>v`xiQWKR!6lS==UMt`4#f!y8q_QLU7w}(0Ki#{>C7oGBep8mBfX633;y) zO5gIWs$B9w4+c8&q#Ah%L^;SvRCg!s2bF(H;BD$55QBbN1LD_ewB+jy9`Wjz-*q8w zM*=m#@h*UShCMd;Mt)J&b5uYWo z$$&b}NN2?#tUMABcFkEaQ{)Sg5+ESXbPu&c%hnWmpm#j^h10-he&(@R^O0Xn#aPh- zQ=Sf=*W$#~>&Dm2Bt3}#O8A2pU3XLQ^ul;<&?dp+06>9IFHxGQ6hN1?c!NL)bX}G* z4m{N-b-<_@`$uVR4{;Wxel;6Ij-a<8$YZgV?V^y*V`UJzviD$MgqgmAISN&f%b1Q7 z5nfvPCPzwykbMsTWF+;>;>o@tK2HI3F;y3*N|{0u)p8p{FYt-XrqKm|b}$vg1p?x2 z=LQaFlz>u80d31~!tKTKIQ(lTR?S8j(Yyak_mOki+~8iK-B@QgR^WgDr;5=i{xG%b(ywvHD+eM`nJW{un>*byaRWf4E}2)e5c3o#&h84uFJZ`tN(1>n9dX+Py-YAqnVsn!}eSH5) zdD#-+$;sG=a*{OV^mAMF1(~CfX~!)NclQMD%l`D~F3+3DDyg4$=Mj~cH|B?&Ti1CT z)-Sq~)M_&8)uc)$1+Q7egf(geKw0STqMjCh%doHdMQSZ=x63HPY&Qi@E}^V>9LS?A zb;eFMb7AaoVE$4%#}lBh{gSV<2^*W|{h|UgX!G(8M`br(j8C%>?(>PKmpi;!_{RT zRbTEsSncf=TsT$wDz5J)ev13CB~=l&nD5noJl9>#)h-A zQK2{2?F%7PXEs=sJdnnwIc5H3+3q|u^R@k-o`7;{8hj}YfK}2xn4D;+Q0XqGj-Va8 z$6JmNY{OzZb6?ReKUMk~V7uPF-O~D$R|Mci3HuTKb-NBqdcyVdQUAt5p)UXVuH|4g zx6OrVz_nT^UdGT=>Ivxh53utj5_!U2Q(s>%eO7q6+L=M>+Fk7R=-OC=qPK-2r7-Wq zbInn5&F+-p%S$4hwS)V1QQl@g6{)W3%RG&*tBFFrU1xFvXOG$YbO(LeuENBGTB7R8 zGol0+-}I`K2c!>nM#e6L5xj)IVz;w%nfG^OeMB_?0dM7dZ+4x|4Od+X5z;q}cZ2fT z=|`T{7z`uIZ2A21170rPaAH{VVKCJbORa2dydLbmdmpHr9=^j-Tt2AL%T!V z0Iw4uORS2*sTECidl*F99(p(SeYC#wry9o#O%a)&6IqPaIXG`5+9AGt zv71Gz$S6-z^lMBr7=gw>-07HF=F6yG1sUoXhjGi6P}CVa%_pn(SE)q{89`Xrw+p|z zQxq7P=O)=FJ)O%~J=GDl>9xC;H(;=WnR=V{XLS{9Zv83pFnKsZBnd%39Y!GAbLWQ? zo6_*xJPu?T7^Z+s0CS1zgDl19mM8;i{>0t-L7ht*%(}jIDg`(_m|q2j2H<4D znLpIs5m8qlWqK`RNU_Kc>pi`g5a>2*MRRq0BHBNTuez@1KG+iF{~-fEO{AO$Zx{mJ zeX&>>a}#UN0FvH`S5FmabEzcpsI4u#PAmwilOuLTj;&!QcjmE67x5dtfFu|-mC7u& zYJ{pzdik}gb5)+Sa>7GRHV9qcw!p~%%R`oF?QYyu83qa_@5qnqBr}#J{C_XwWTOD` zHM)PTsbQOQoA z`?Z3uVmf%4p({h6+-IAs)fF84b5HGRM4Po{a<_(vqF3pzh~}EJrO$+zO@7-0xFOkz z=D84Cy7+_oZxKIq6iy{-H42AOYA5#_eqBi1qs+~HYYM?mnBfigbf}=SxvKr#+n)z! z&~!Z^*)Gi?G3#2asu@|BxP2lVz8K=mSJLOWA=$FsTe>>k z9X!!u^WdeaHsFLcK~Y8|Iuj053Z)Fm%iGTN?1@D?Cca$a=;sRl0ri~*Kyw$Y68k3| zZ9;yEz;J9ivz%PaGh-=U)v5}sc)qJC{I8@lJwkc?hsi;mvL#{uM=doNswAkmV0rrG z+v06zb&E44$I-*jV$-9$-}$b+->+1tmz9A)gqtF>tX{uj+zHZp3E&yA6i)3~o>gA*V3RMx2gC}tagJ?6tUrjt+m`>`fGT5t9=wUP`IH8b9c z`peTJoy|U{yL`mrd*1KdjKUTECDl54*$FhQw_Hp%QVt<}A>^Z0#v$LySyE&!SovW+ z&0%;(LJx=LyQ*QrBfS$_z?TJuNhKna3}??AN&-pk7Rtr}(u=dgs~QmW$w4e2l*b%d z6Ajrvq44juqA!tti9T2wFaJyw z$u_n@AAb^;7#ya*zfTblg&m0y-$w$^FH@hT;Y9x*z@zqm$L{m5Xs)+SPTxkf&p%zH zL3*8uwDj-Jgw!{~pXg+Y{<;Xh_Y;nWK4poXw(nwYQV(+%*s!})a7N4gr<#CBU`6bi zqfD5ZHQS#oQJDWl$X8fRg9U37}{u=GR%apVt3dA5x%JqTiyqRe}EzX}Usz1Y5+ z)&6z2=-X~Kl~Acb&I};EM1C%@=)gB&Yn!HeotdttYlyp7f28*#2`Ub!OaU-da124*yUe z5JG`n`J>meYU_J^tQo(sD-0wbnuOvuRVujj#w5j>h_?9AlSol<;?jeasbYfFaM!umw!0l50DLfv=?g$%LOMdPK zQ}7cN0cw;Tuwo!*F(9JO*W5BLSAwe1AeDpZe-#VD$*AN1qAw2u?VRLV#WYbdkJ5`? zRo}QT3IL0SEJO;3MI0%Wl$QVV*Xnr7CtU%B%I}K}N@mt^_B6R4U7%~)zTB16*f-fo zcY$Nt^U1T&U53*uoI?}Zd^>%vzfz=>2D|obq%ec>==~huL=u!PCND=PtJ^4VBnSCK z5$duvDmU`>wNB1b*E)ss=cNPhp5l4nyEkxqN%}$k292HPbb;j<^fYFmD1>`@3q;PX=c-ZWpuv}UbX)}{4h;-0< zK*GWYMIAK-I_4$VY>IA}l$4e^xCH%$7pu`1vyv}8kN-0+%~V+K?dzav)1C@iDyGev z6s_puCt+TY53HHWR*ig2-rRL-I!jlQRZOt9+hg(1Ai9r!jZ9mXCIAijy&7X_nhs zhtEdXOmRV|8107oO>G?zJ&oXx{W$8mH@A*x*?jmsiw>y*$@BdQXg)hSOVGx}U+{`t+ z<`M`kt{*6f2(Dv_`7W3JJrJxPu;(>xJp_8)F$|E~${5}j7}-Gam<;8_1iFunZ;PzB zLCdJ50tj=+B3Kv`PPE>iH%VRL0EW{~J@{0=7%omZgdAG<5jR<*nm+2ljoLb%ai?S44(!mjm`zYJ*uzEIwkGndLxci>)-lQ z(#`*#Aue$~woWbg=bt2%Ggc!p5CCOY02#Osd0V;Bk0INCy{LicmM>D2KW{?8Y)0Pt` z@gE1#+it=j@Ko7OFQ5lrn>1GrmU=g2^xKp%m?!i{h8}J>%ePfp7Lh;|f*< zD*>ag2|kzUb{!v7oOq1A{xs4521v{UTG|pXJ_}F+U&R1`fE&h0Y>$ClGt9G4}XU2IS6KDT4AJ(BF*?2>g)U9 zV70f~QW_7p6;Uq%$`X%-JB5Wr z>S$Sw_V3#<*;7XE;IdX`u{X7%TTG4x+_}f-a!m`DGopH!HAig|Vy#`j0tFPBxeZ$3 zxvfvlKV`|&-Kj>dCb0pU*UveLsE97{Wr&XYf6EK$8n?1ibZTB#&S))ng&}9YH8K1B zoh7upFPd-c-0U0Qi4f?oU}xDFPUfOHE%a#3V%J9U^b-E@D7lTux5Ezr!H>(gijK;c z?V~!4Q|!HiF3|Ka*A5w~n|gXs@tJ;E5wv?wpAb;c#R|aRtn4@pdGg6XIe|~|B;f7n z-WS_neft1`4 zd9z~K$+Z3Il0WjpLJ2pJfD=ZNsNCO!B1Q(ZJi}j7!d`JKr-%q+N$IC+_hXt=Y+4$U z$4ZPAW=Ee;?7k0xh@!Yz^R1WIaYQrkXbz9q`7wqOrOTdj+KwVUE$=r6^!vvoGKyqu zMxRvgerG$Ynr%;3yR!~CL=#W_pdq%~y%KGkO#WKHyxlKq;C?o7zsP-@%Z?0R^wCt7 zy^vl0fQOQ7DLnSEJAF#Gvlpsx_a@&_`&V)bIskg8r@&$ArV^c}1qg4=pN}W5`DuPi zxZRL(cqvN97iL(pNisLEDfFyvIB^7HsH){5APo1UOx9O4S6;@QP(1;r!}5Ka6AWa*nX-~?G^eiohMBDTKa75H+L=FW-*ajsrpn= zC}jySnl2y%@b-{L&&1b)RR|0&;BeszklmwN4=?lScShB39sURL$&!U;R_R|Kk_2#1`FF;eYz9APqg z<=Zg6KzC%fk_vk^w0+SsUTSzd1I>Sg4T9K0Z@&Cts|-|#+LhFvkB>!3TobqiXL{aj zX)kPj^J(|vi>I?!{{-NbGMzJoofiBrUi@9vwKaRgyg)yhhK=-~++YOR5nD}#CD&_^ zgB8)Azt4I8kEL&(eF%< z`C!<4>}(G*)@XI)OO`3QNizXST@l!SjU?*n5;mMd*S}>vBGy$-cL1Ab;U9=ZwNeGx z*GIyc<}c`zUG{QDkshC{3Wn%hxeIgH2*Q=Y%`J&6p45eLogEUuHN^2reqE77nM?Nf z>Xz<8-&Z?+@eTmPUBptYXLCc-g#9d@C}2dlrYmjl(=D1;DGR*Y+24nOcFJVu>xdWx z(tq-R1y`EWJFmA4yXUCW58$2|!cRnC^>Hwd1jc%O-w@M{iv?#Z)ZpywHHZK zXxKaI3gGl>fv;gpcu{garp)>Q0m!~IzrPE#we3y-kQ@jR3CIgf|3*wHe*ZNWe7_s> zJS`}#{JTdNj`WX!A_PnXvK1O>?$Sh}jKC^vn*lw8jR6hgEt-sep zwLEm@vq!%z;II{#g09^AWB&+bc?G+0UXF*ME1)6coDVAYa$G^9MO6tcJ#+@(2|$D;@tohE zS8*HJX%;^jE8E^T7Ou>ko94;%{mCv5^V_^k`9ocX9bx}E5NFk0gAwprD;t$|8}kCS zlU(ycvZ6Qum#n8YG2nqg>qiepsb;LFIG_m}o1+8}&a^{2;*DEr% zSMo104@lv^TY(vGhilx}$9|f8Kly9i$^Y%DrmoaOtf73<)>c;S=`t4kGjK3JExl1B zMNIDoT6*p}y?RGP+_&l$gSnc?*D-?MHj~=Qy`Q|Tgk6SFm~ zT*DSrUypXmC`H%irqx;d+Q*CT+C7&5zLaqYrC>D=6QDIK|#eb!3h3Ev=@<#@=gj$zUjTNzecdony8 z5d!*zq^BEtgI(F9d8D@Kd{{Me;$09;_Oa@&+@)?Ff&nIh7qn#dSj)YV6!?CE4*uk5 zr7X0tbDn7lJW$Tp@l^86tI6^FlSDlo)-Rr^S9K_T7m_s}eu$%yF0Irn_}* zUWCYZRCOc~mA0LE_D_r-y5@XUO5J`It}3RT$vo|a;KGKT;IIhYXD z>C)b5{U=1CoBP6KYI;F$Rsmxbe}J=hCQz>T-Rj1iiijG%p9jYJJ#T4@N(QcUn>dlS zU?An`CUW?yC6aB`l{n+;g()Wul-p~Ho)OF!>v+WRFGIc&HqZD!!+OqTreQV=ueOPl zJHyb>KkZ&Rmn(K z#zVyHM~ebbO7N|chJg+tUk>ZbDs-FAbaXRO38`l(A2onbpax;?+w*Ba%@uD4v?>qH z4-7%Ro?rEj>tT*klM=n%rg38RvQnJo0kXSt`^!y)J^gc{g$OK>Fx_3;(vxcS7n4YC z=KS<$h^r&Fcx-Y0;r8Ww{f(Vk27chkNJg1IG8;Vcf@LN4@B%>Ih@v3({zL&ui}b62 z;329XetvbVPGRDVEIfj@%vDH;Ve~%ZK84bcBG*rLNIst(ibjmWi14AR{pKMZfH=Dp zhyHE{dGe;Xh++7fTj9uFK5jtYH!U&#zk7`qjl&viF?gF*4Ebk+?_!<<1B=( ziQke2jdM8)sP=pbFltOH4p52yHY4rAkK%7X&fT5|j+xgzA|R>V7?K!eGLX_VeUFb$%?E6W2Vr?on1vllPu zs8>IpRV@6jSHpyr+)_YxFdtP2KZkls%QjG{v2ZsoViE1Hiq^cw=+@=jm<WN@Kl677Ayi~&*nvFt1eV@X<>^Aa632G7LE%RsKcYK&gYL-<6z3OL!&7Il>%KL|H z&WM#Xd0E7PHZgLxbju0Q-IK*&L5&4}K{&7_4vP+P$@PBZ_+#^ZvwOh^!9E59G~o!Y zP!5pra$9W~>Y{jg^z)0TrjA`Ud#Ghu%%MMTz)^b%EY}xaQ8VTdGc4v{B2xrw+bbdV>n^|9Kg`F~W{M589mF_70kA>gy)-xqzY`3)q_qA%e zQK8D+DTZW}(6(#${tXt(>Zn7TtNCUO(#-ET0N+uvi8jolk_JNMwX7b-e8W8;jG}Zb z`sAGpZ(-oUe4sA<4DcKi9d0sl>0f?P9sCofX@;bU#EVmIs8N zPfaQQJY_gD0nKTetO1S!jn(u{+@`=TwfG(pr%88p7@%2^|*O- z9=pStC~?ysRjU({s4J#Y?s!q%4z$ItOX<6ENa;d|ch0D|DK0`|!j+@{=p2Ep68aP; zoWL?RQ;&VBrlrutPvwsb7f0d`hCCEgqN)T_ zR8maDOgFs}IkqQJuWK4gt+NyjA@>o%`wQ{SnEAHS5b>Ty8J*C>ZeIDlQ*E9z(P=-b zxyfRK+s-6{=q)jNe3J*++r4DOWkprD7tXVoufLa45&FFE#h*qEd@xSS7i9Yq;Q9?< zcg|FPS|M(-x+2(HX1%&qYGhOF|FCtIVNLgc8y;hnG?Q-7q0-$gAW|mXjWnaX5owWb zL@b)o-MtZ#qiZ0|=zMm+{NMNUcCWVM*!KPYKA-bCuRcLRtWS^Cy#D+fV*ByXaQLn5 zEPf8WtyNFrOUJ1kO(vO;naSMoC4A{>%saOPg_+s^_7A+=NV8%*oO>4qD{Hk7Jn;`I z#F9?-NhVu66rQ;)UHW>v&q7Kx?{A5{bRJ)~9R1z6w?T(zu;K04XPf%V&54WXgEs0s z+P#${=iH8^>k(e_;VA0AKl7M1gD*~Ah3~4(7Up#Z0P!`quDh7;DsEajp|d(eYlkhv z9j-S{(<{Ckzfb=iU;qEcE59BcI$ZW(qU;a;s2gk_H-uw>K^lC{Ue45O1G+4=#_u20 ztG=k59(ui=Ay@lnEE8VH#+HE;cgQxCN@_kgZI>VdyZ<)uPe97~Ex< zO6RJ;D)Jnf=6;9(T7mb!xKRM&T-3Rmom}rh?;wXW+#R!Mzs%cUD_}m@0=0Ue*F42D zTa=?GH$&jB+3wB|cY)Tq) zn!!4pjdmO&D^k2{TYAbrggj$k%@fkJqNl$+3h=q-39baX_IXllM%s`4iDOBZru&H( z@UWQ3(bhhe$E0_NS1O95_2@}%z$(8#dcdXvdI$96F>c>K<9TT^m46IP%@^U`if~TI zV+nWe={5ufc#;3^V`94*Im;X-pc%RPh&RjC?!_;U4fXSK@@H;P4z{(#QlQSN2}Wu! zAC4y^fpvw{KhEcgS~c|~?%h`tyWO|w(9PdZ5zk+ycGWj~5S#b~657;Bh8PX#=oI&i z>dYwHW}mG?v`e5JMa_W0I0i6UNR~Z4r3aT0^=9)xFTKJ^U^|Gz(~i0n*+76V*vCoD zSdrvuU6w4Mc5kDV8;-xDqkeeD&fUc=uPd9de#6zdeCIEN&w|dc0j5B6tz0z~S#ANE zUInjXgu@~({q24-^qbc}$UJF-W$)#3AC|MkuwQJ+;GdctsT*L z7!4^x>gL^Iq)g>ZeJ`1Y&Q(c&_#>mQsV&mZ=KrV)xj{{ySUL08ZyeIeT#Oo2`1#7OFVgv>sHE2JeY?CdjZDrE^_wvfN)#JaU+P2$h=pg3#j}WE;@TJ{XV`1tL?({<< zk;ilN2_ReWRvN5qB=OXZXprH35^EjF?eIOeA;h*Q_!ngSlcTBm7+oNVvSD4xyIh0J zFwfcW)g(!FvLl6*=J_80W#l{#Scn8L1j#58?Ec&NykCF5bBdLJ`lE}fz}u!GBjYL! zwU(l1P9#!UAiF-O5vU4v`Xc|U%UaB!N9B>oShh?9u_#+V!!De>HZD%J)?k|l%RYin z32`pkvDQ2OB^yCJ?l@#2r7p`QEagyP{hYnuwp61Ez209SP zL30fo+$f#>kYN{uZ6~N6DoHx6TaUo~hXpbq?y=qmOYBFqb9UcGJU|9Lb-M?BuB^{Z zE_{USZN{zje`=7I&Q`X-;iHPwyKs%L0ozzKHDYxGmD?ACkcy$kv^Veupm7YFyx(&n zCI)mJY7}J?IG0GjyRlPQcGdw|uMghClIjM2jZ_xerQfofwh>y}1TA%*W0`ID<{4JM z6%@@AB|r_csq&JX2+n@i3-vfaNOsIYaLNi+Q-+Hd^b zS?WdVYA4az=Ceu1)TF-PwbBQ}leG8G6S{-OClS4e1aMR*zJh{&>wTZ{vQBic*`j=( zzP3wv-{MIQ-<0~+AYO(HOd)(VqB8{>R}G)TaX&?KoyRDFy8q%C>o#HbodDoM17(8UI1qf~?d-@AG2&yw?| z@x>;6z<@)vz^*^pA8FfBl<#um>JcXK7*cS_N3ensQ@3UWt{wdodGRp0J5J-Li?d|n z#YQag)9eE6_BXYHxY}LzMN`VxTs$cKB%6*2@AKHF%5yyKk;;>4$q5c;hp78`#9G^R z-n~ff#U>}lLMX84dj9Ngj2deAtBav{yX_cSntU{(NUGxJ|FTM!S<;h;`F2YM=c?6h z;}c+fz_(>S>Oc1>y7h`Dg-@r*62bkW`DaV^^!X~BzPjN{>M$vU-P#U}^`Rgh9jb2t z^`1Q6xC(+W>(i~Oq%G04nega%kTXXc+v-ocxdPB`#J6JCcL!VAQ5nz9z{T6+B8Vm4 ze~(H0(Oz++zf*WCYOJ^{G|8ds3|6P#t0yS2LW*gcmyWD%N8J3jnbuXmP2Yvn@}$m}4rz zXyLyH%niDF+I+D(RPTGl#?yTB0N(6R)udByT2-@wT*}uXo1AVtZdv3NB(i&3oYcH$ ziqn0vasGfsKh|IgvVDy|B(ZG~G>`kZVdf_R2{iZmsGy)eu6L_PjC(|sn)OOdE4*4O z^;pR61|ZZb6aJIHta`yv>;3C135+$;i9}H9FqPG*(F1H#u5tJc<#{yULy{aCd0@D_ zIqVMfLKlQK_glqdE}5;h=vgX`Z-7BUGndU!^2~`c_VvBa3u~CNL1-V&%lb=wmXf`j zbcuo<1DOC8Ibqbpz8U;Z^%)83(WctcFIrz=80_=&CsV2GZd`*?|BmAib79qdqDJ{O zUHU7{C+AOBe^LCPBLtraTr>x5>>)7S04=;((d9NFt&74#1sfUpf(59qC{o1GIyS_A z=@<$XV;FP}4>j>WQu0Y>$@uR4Zoe+&Td82FE7K~|eWlh7=x*EQj$D&*v3qM=$vs(F z{nlQ<<|h3Mj?6KJ&(FKRTI#6Liz@fud)j=RN_jHvx5eZ2reaBYEJpML`z50CK*zieKFxTnF+8sv#~ad8|rGX_EaOg*meG{T41L%^)%j&Q`NaNU_jk8lHbE z+gH3M4f6=|g+*27nMh)k7OR)l-j_YpuKSbK^=|tNp5=xe8!>78jB%!OPj#VFl_%%= zBVl{#FKn#CG7RMWReng&d@Ciq?bS@b6d7w&H>;_X#~OJ$rSvP6#chWp=R}dpWn5Do z-U_#Re(GbGZ`$-HDsg~L&XygrE`$JPAj+x)a0A6&X+we!c4Z}%R(8B5jvja|Rfe+n z;d$R36D7#B#eC-x&Yx)9OgA?z7eKj1RJM$_uQ)5rHt4`d~aFL zZqz;Jq}0352y&~WYj6gl-(;B%9>CU>7JZ8Z7!Eb_W3AUh%EqvN`-XTQm_!Y|LZ5PV z8xn3yS=0W}cm)kDO2&~0X%Sj8IfbT@2~9r;LNWQCY`tzi8CNqOY1W`%riz&i zciS?fZfvjI?w&sQYF^;}Yk!~81j44A^0!QKfzw>4ZWWDL^)@y~^0>88iRUUZ$K><= z=#g=bUDDdLw#vLeH~*gY->m#sSE`X=HDM><$)gxF1TYRo(MIVv(FSJbE?iE)u)rg^ypdztv@%Dhm(!!R@LR{GsASFkBpIgeba zynCaiTgr|C0i+<&+BH7r**h;lLM+WX!v{@Se4t%m=#~utjyK^q*z! zUjei8F2jwPJaRLx-l@m>iQcn^H>GYavQoP7f4J{@rN8V%^sZq*7a!g5wu$3m0jHKz z?QCg3?RaM=r`3M|n~kBg9-;s;Oq7`7$S#Ax#}FC%^qTns@Vr5wyVR=<1JZdh2cCNc z(j!}|FY=%3g%ih#9H$vyKo)P?11{z*Y$`D$le2x0WN-OOaOM!>+{HjWA7MRY9qZFf zxgj1*HfR-`m$(+5H;V`AxMb0|lC<$@`A?k7nfJdG^*hykV}VQv<`$Txm)OF?bih`k z{qWn3vJ3wZuvz;{z?WM}p=3v)ubpiND}!*F5%*7V4`{S;>2HAn`Zl+x8QW|O4iP6p zNslz&*jRu`a#&U(x@x*U7tGGr2Mm!3E~l31LDPINO$gJA)$| zAAKeaNPD_1!5 z{p^6PJE>W&cN^5N6FhI~h5KV3IKGXEPEVVZM5ga+ma>XIQkJbP1o`@ zdK>S;G5cB7K>NS?y={D0%V!gK*g;4$*{*@W$AGGxF;l~6cN&mgleTXvIz#z`wg4g& ztMRA7@Q0uuwyw6qifeeTbNYW^NfU4AFHA5lIU;!SHj#p|1^)nOE`F4EWX^o3){M#W zge%5u?&5BsFa-&}E>Av+34SszhIg1A>}t08{mm!h zt7ole$28O`FjpO#cQ`La6-5YN`Kx8QWmpWme&Et>;PDBXl2o`6dIZIf?iDG4G4JNC zWS;R_y2g9H;q9!35C&{J02+{7!wsD}f5Mgf4b9r{!0z-<|Ee?jFB|@n$V>_|s$bd} zm}zWG_H!k%(#!S}k&$Sxtp^N1rQEPE)Wvs^*~u_@{q}=LR#f!{_}7m)T<1trBcc31 z)3guGwRA-!57dAaX|ZE5c~ma$!1QF*`@s~>aTOt1^a|EpAkwjpU{{?cJ@Mq9p5Xi2 z^R=Ts=DwQW)18^_`@X|xH-Y8$k(#7}&m}#n&KY<5d@ay?Jl7uQhu5vl$mFD@@8xFe z$f}(vC-fHCt=Y=7Glts=#Mx1aH(x%eUK>gulRyk~U9xKBT?lu7wj9d;k;{VDA?0DW7YoB)PUHW+Ox-QS|7S`os*!CV*L2-x0}K;^j@D78 z^Ch_OgveBVsBR?ZBL_C(?AdFGrM*JVkz00Anl^{c+ESyw3mP&ZD&FXqoHF3PkC(T5 zMcFa)lCPfco4>UlD8xvJjxhXV*r3(Vr<>*i9vu<%2mR_FnMgcCG^r$>NODYO&EFeY zi>kcujIXYN%8oHVnG;p*`K!b8?CQ?WpFDeb3F!KjkRs1))l(%I;-zXK3W<8^+mEK` zHBn2iJYG}(QNy~d#%|wk^12?N>8bZurL$s;%<#%>4%bHJzUd+;@QS*!&QD`*e&>`g zC!YW7UMjlKKI{}ZU6)$}98W9ZFf)@~xoTY=Z}ZLQ$}Be#=i1oM#tw@!+{ER`m22MP ziv7e7?Kc8wO+9Xm_sJVhThr%A+u`1K`Dk{;;3UW0xD2jxeL9VlC3w!5mlnkwJm|5iGwViJLp8@k%H~_XaE<&%qZ?4!SHJM z6GP5n1=J{A$MF2ou@TcV^Afp1;=X=s0iD}V=N8)iT-oAdwbyHmFHNj-@xX#1E*L$y znO6Q-X7SPZ0bzDtZ;yw{$6B_8{c_6Xxg~5YI!{Omwv0QgoQEl_M`Bkjik~_6Qr~8? z_Cp$h1CPHG$PMR3S|H*~me1AHKsD9VhvpHy)bfe3j4W2`(1Gq8Jq1XkJ*{0RWJxWJ z=j+mrg@pw;V(Es*s5u9?rbOH z$6GbaJ{P0(w6R~@(tCZI>(Ogl5Q}UHUz&uVz4Nh2!3;cb8;5DdW>i`CnQf@aEp@{3 zR?>TQf9B!!g9!hhyFv-X<0ON^ed43Xs`qcM_i#pS_gqad)Oh!tQH@8(rh((~!T3aHaE)QE4IHz;E4*pRcDamv~$Q z6fG25!bXHbI-K(M^fZNO=Xytof4E8I=5UpD6fy*fV!e9I@B(cI#JLn8IF6S&!ZtmlmwvTc< zH%k5^=(m`e%i4j3AbVn^ZW(y!Lzix} zyO?~_-Tj-Z)OReU6% z@o`loDfbpu_!w@i`)EWao^ii`OTfVh*)?5v=Hk@UHJNcx9vE+&FzOSfDot$9$H|Ab zAG)f-nR1fQdke~z1kW)RdsA()vaa*UEoYEB^M2^;Ww>WkQGJyOaG{=Jd_6TqCq8{~ z+fgg1msa&sfOFWL^^4ou^^e3US4~OaxEm?c;OLs}Sfj?vr7EV3JZO#_obx@Y&xHA( zKOJv(JAY8!(ZO#4f2v57DuPs+JRizmn>X1;U_Ol`wf2*!M&bn5b|5t7lA=21n^hw$ zZ-%c`m31%F(U*42XjgR_FUcqNltlTMqI#Q1hsOjwDh{T}jobxBkur9iO;IWPu~$A` zLZ|vwHZ{eda)TBVIsyfcs{*c0_Zb7-%^{=!SdxtDs5G<8#ULsOk@x1>EbB#-$DJ{C zoeBe+(2ZZaiT9MVfDK}wYZlhjkTS<6Ej}LM7=m(Nkx#O0C_cK13fzkNr{eb0hJ-xa zPD^VObs%!%&o@NY@V`G9Wwi3g{~kK;Q;CbwmU2eZwn!eJc?X4ka?NC(+}39~3zgrz zA)=khBe7S8?|KPtrmogB77D=L6$3?hq`|>rqZyw0?2%BT~cO_P^6Lz=bCTLwCd~4IC z+CZZAW}zOSYEwz93Sw*?zYgj3Nhv{=*LbXub@WVqv7PNpiu8n}J4Dls|23d`9NA(D zrTUTMQpatIiz|*$3Zn}2uM*L1`N;r@`CHsNJe8oq>X;)eOeb3mUrOL`p}~J?bC;(X z_Y*a>y37#YUw+WndhcEjj0YPc1h@P6oFGrDzwtBhswLMG=Q;vAJ_dhRz0Fj*cHXsI zjD{wqxA3dASz#m__a#5F36dL4gKJ5WdJ+OQE$g|jhd~NUy-yP_yaV^g83f1&NhmCy z^74%z#XGHBQ`u(b2kzXQw&o>^&)|*|bBB@-TE*flVCzsA5-mN*)7Cl{alG zyeE3&n-L4xDeYx}LCaydut(thGiVy4qOQ+of>jPNKw>FCeEuz!=LUWwyKb`;aX0G0 zbM*yR(t-7E?M+jV2g?5bv|!q5qAERqHjiA2B=0<|db`dFlm00gh5b&$fg}^~7}m3x zy0k4(Kk>G*>N!XeR{lypH9fs?VX)t-61=5j9H6wjYWV%9;j9Vr?CSbFB*5x!G&p_g zl247t;xeUNDl`avxztr~N;w~herl5NG*d5Z>24QwYdE+espe?%_QTvx8Mc34EeS4m zhW&2ZIi^YE;)wa)^-?kdy6R_V*1Fh|THx`oIP+%Q!~fFMja*^AMBKk7254$?J9RlL ztT>mggEb}XLjz_-Y;A-c-hQ>Gub%}e8hlj0z`{iBNcV=0-Tst_=O-MZMGQqQ@ zbCEjRCi31^#II*yxk=caKbAB>u{CgX&NJwn#%LHj+M+%cTV*;*x9?$TY)Z`SXMioc zw!MC569ZPUFac{kpOuhp45i4d)mxQyCr$DweuVj$raD3yU?o|H%Ml~Qs`vAI93>)j zQdG4qIUu7?pV|QlQ|fx;J^q|elCA;Ai3FVt4HpcXGR*w!n9V6fHSv7GOWsC0%J#O- znABXA_jhLy&*(uK4GHI^`eB|;PE&J4&K_;?TMifRoh*F}odB-(7;l?8S_x<1?2`G* zGuI_J#Shli_LU(t6`#(UiLCDNed+PiUUp`bz22VsG_|!j4v7mEL9h*TQP~EO6m04V zNmq+D@+k5mtl*>L*(yz5^}Iul{?aciKP7W=jnU)%G1A1U)>c+mHw>-zoezg(~|0P^ihEU~X)4f8Jw;5lbS`ELL{d zi&wA3x)k!&ItM{0h@T|Qo&@J?EV9_d^A(OGpljn6Qv zq_4*t?xzovkK)twcjTXvRZtFAf)mqCpE?_zRR^%-^2M6xcmVS|x#*?YPf(<3?`80e z-cndHEY|^Wa7Ur8#=)v!OIobHowGu<9C1A9FlJ#u)zD2f&9AteCBKFxf_&>@qEY9P zyy($q1p_?xp_4PryoQ>8-Y)A>UvnTzVMRx`Q|6xE$qm6^#iB4bfA6jtn35NPKZw7flI@lV)*g9s;HCIGs`-^H;=Gt*Q4Ke{=?qc~Q&x&>yIN{6n!>%h(&I zxQCoT_YovQ;qS%emx)q>S6kndrX1rxwuva$TDeY5$+`4dQh$Xmz+Up=-kWJH)Id$E z?H2_Sr{yzu8N)b}4;?Kv^?huE`2K8q{Z+3mNH*p za;0K2V<$tEim$BDX80ja><$Y9B~^ioF2mg|aoOhyqyitgbIDrfCBYR_Y{R7d5#XY; zYlRUX!}sQ?%-1p*BBdrsZPOcp1c;4Htb9M6=Mo)e1e4w)p~fQ3{HMRCd;irV5}DFA zkj+}J>TRd=Hp>&IO;RH+vr6n0ewpm{bBrg)+`%)dV}ZtZxK-_7;<-n%~6K>Y9KE>l$Pzw3&p)y#$Hyex^o z+L%6;bPFCiHu=$6@7V2qkt(OqN#p3XC{dT=8MFC-X~z1~R0Ef-e)1EYX;q%Cj5nq# z)8uTseEr;mPLTtXgforqR4C&>rZirYZula;<3wS>%2Zopt)e1Jqar|d0qB&Y7z?b! z6$4uzWzA#}sMXG{l5ILzg6z`rQJ`gI?6MfSlNWiJz5B8I@|qvxl5L zb#D%MfZ34r8UiZ9nH{Pr?Xwh?%#60OSSyNPNahRg>2tN|@j`?ABag*gk92q1t0)oI z)nX(FQ;d3h@eadWN4j4JVitVV0Hi?ipY&B-079%09>b4#?2I3J2 zyw<%7pu~Z6-@1qQV8v2Dtkl2h$^_^SiCOBI53Dt)y+nRX5Xv;LL2=*?32st-SPXK~ zNJY{taSCN(3ofPe%VQ*iiC}FnBS6S)Wnk{t6Jahv`j7xlXs{>;!{Wt(a9>ipe@871 z>`qI@vYj4G#SzYP$jxf5Fhg!BZ2tBigzvhBtrpdgBQer9-C2*d8Nf~RmkkC-esAC; zW$T}z#2Umiv!{>6#7T9sN2^aa$d@-SaFK4hS*s7zByc9G z`X91%AYo4Iox6B*tE<1<)mU1hNgo~JE!KUUyO4^9rfs&z&CFb6DP=}EIT@$64q@+R zKW4d+fLPzvwk>rjD*$p(6cm^h@`K2(0tWgxWz1TY9F3luqWarN<#GX+-0#K&vyMhk zxjA9GL!?@Uq_9QNspH_NEF^s6CjSSib3+n8b#IQJhERlLwXXG&=D2>t{oVBuRQ4PS z^!Z+N6;$pfJ%p|*F!W(a`$IxI@4uIH%pn52pzs z`-!uOdaT50WzWm1LbSX?IAtc=ar!|x*D~u*DHI!W7x-h)Yv*iMFYHwO^WX~?`lr2y za>F<*4Mct_^V%?V}1y4*i>lJpeZ z|K;gggu-MQwmz;>^nX)sw0o7S+oN&n`Yz3+Tg_ma4}JT}F}Js9i``j*|DjiLiZ85S zOf_BFnbc-zv`vICDlRb6TwhIkRwo>azF=GbP%3Y5?N+MyfjwOLF=uZ3#ycm55470e z0?SLLm3J9+koJ-(T(oO&ijR}f($#tDbm#DaO%G(ZoV{XbiM@hWXcZK#=LI$?80Hbl zNvUi-u(><2Lw@?19%)SiHK6$eBffsHS$k^bWhyBwY zaU8fL7L-MZ$1(2jB-pp6=;Bm*za1p9?i|JREakrFHC* z1{9K`a4F=t4_gFP4_kTBd+ainW|fx13HGegM3oMRzHy)M0Kt$$YSfsmRz*gtWvgvS z@zYC8m&TK!H|?l@N)W{M_V*u1HDf9fyxOi8XyVRYHiv1&%a3jmWZ8kzgzj^S-K`5a zXu)wq6p-^q_riBZEVp#Y?{YcPgW%o$Syv+|F1G*GL|$(fsjSoiVXbJ*tzSkM)W0DY zFKGH->xjEY|4%qU1WGn6{>~?u9!&GWh$MOAWRVTQs9a4DsYfW8QD`_)nOmo;6z8_X zWhkv&SA!)g6Oua{l0n3mrIB=v9&(Bu8`3gAPqE==FzM*!Q=B{qd1$9y_KKQFZmVv$o)14l#{6vV3F^wt8<%9x8Bd1Hhtncd;W-Ou*gcS^NQT_C% z=(mFq6=81HH{%${ic-S>?2~O_P7pZ-zrNYmp7eHGO?;@+n#Qu%YH^oM=Z_GIS~nYD z9B4xF?dsp$9yF@A{+(y~$@h98@fbpc)5>7XW6uBsl8rn6&bqM+eez%@Zr^Y6llHDC ziGs7IaABlvlcI~yHd0krm@e^F8YwOmjW3TU3)79iJsL5Ne!x)Io3?;^&0An88&7K@ za+q36jEmYzjrT<1OYs&jF?ecL{G`cePio-efwQ%LNZeh((yTM5BLP6cSX=7@dxd%# z_;w=)G?8<+%aL{QA|^AD*U`=kgPRN8&V~X)wG}bS5W9$dx6boX<0kmc(OPw#NaL~+q&EoY&8raw2 z4uAOMMvCW?sk;-t_BGaHvXLmtnY=YUZc-B$rxZ|MHK5#}!G?yqyAgSj*efu~P3rpm zvy7nG$jkeF34!rxk$o@^@btBaG@{Ck7B3#1Ysio>>l~yRkaZ=1c{hP_4W~O{%L03F zKR-Y7kmAdXrAzt{8->yEoW3=FZ5=?7t3LoMm}=riCaKfniaY-#_wV3*`u_% z%^kz(>Uwyr6p?vOgIU9n>fx!ti|nh|Wwh4ay4I(K%uy&6bIGL_vB-Zz!pfiiJJ!*l zxTztMJ$~_aTGz;+FaNKZ^EiI`i|)7zYgLk(td3X}zKoveF3yV(yo28BMKsC0zb*F8 zi15P+!U{=}J?;3}WZE(^RuP=5mfYY;tErWu$|rc7{f%=vruZk39XI-{p}kmV9|HX|lA)wj>mwJ2k*QEMAP_IV`Gg7WXZ`oGDpZiKF z{%zpy_hj;_TJkQzj2A()b906J3=L=aucKDT@x?oPpte$^d;75+p%cXKDq?6P5^Q{g z1iiu?2+VPVYxZKlP=6T^|6qcc!#Qk%c6?x{^bH!$J2+4T%*~l@!`acZl?-5j4}Tsi z+>f1Tk1(UKN1GAwK@+bN(TiC4S^kRqEes5(Kc226c}tC=ESPP^ci{#G)92fBw0kc; z;O5Hn!-#V_vgPCQR5-1EB$wcIA}hgAS#~_7ZKU0;PYz5&%pvoa-CK^Uz3VXCMbIzE z;pOrbMo-(6v$m|C`HdXQ3cNdd_uevN7^O)x#em1|WvwSjKll_RLDJ5@lS~-GFfGKr zeQdz7SHRdj92(cMagl=aEXarz+IG4#LhyWHpiszs&=Vpzs4+2c^)!a9gnFMj+kd>5 z>jba-+=_bahZCFnLVZ6@A5VpycwZD`9ZHpdc(M#csbx??+r>{GkM!bkBRvOj5eV^e zv za*(X($eg~GayeL-IYFULX>PY^^nxBIhNsdP#izFO0a;X?D=JHM%_EpMnZr2=q?MBe zKXXsvH0>G2(eEK<83qZJjo6pJQLa^}eK303amA+QlLsBUdAwDWGz($+NM&QgPEBLB z<4t-gSUuL`P-N7*jwLW$jp?4K!zz31@^ZP_mcSABNuu&j?7G{VO32d#g;Oa!5o*OtUa~&Z^vAT`=JmeD$^&DR7N>eUJh{X7h{_s&C)9MV^u~Vq+Cm_weOO`} zF9|&HMjabUz5J46bHNhnLAWNk>aNirtO$< zST=~%vmd*VS#A*@&uK>!{5b3D1~NJfhmRUyX*2ehg3C7C)vHwpVOaY)7ueNe#;^j# z{kEcO1VBVB+)m!*Bf z@*zHaT{aCT^kxp6rsBX?@K)E3O~xyk*tq%bOup~dpRWFP|&2snAA$|u2S(|tz~p0^uqt8HiYy#Pj!Ok`=DU*7t9*kz^lF9ft_h12+)o)9c~I_LbnS z$#kK&hO@I>F*;|5Cq1~RUT#l~XuYYvB0`pDNR^TrsHYP*k9l1k?4XKSA}mC<;>~HL zWJ6^05vtySL=F$L8JJGWs2-{0~UIhlF0P1s!M|#v(v(@(7G}x&JGVC0;qlcV7y-4)ORYP z@%$MWutU2t0b3O;b=VS;&?0OX}-&SD+N>i#)x9r8yA7IgG&(u>e*AWtW#lrz@e30-tKqs z%JoX^K}FbmdWAV9rcFVkWgfcO0HaI@%`}cDPh$nFp2`h;r4P6`995Kmk-_JweQ^e> z!bFru{OUk+w-@iFw_|!E{F+kpB>794>aA_PAYT}Af8w7~^Eqt`s^ zlnq^S0+ZyYET={SopPg|3VKsaD~{~xTiM5LZ8yh51)8ozV27MuZ{w)2cFcY1*>|%v zoaD0*ZsFXI^bzNehSe*?7h-x*CRZ56KH_E=(J$@*MzAiAh=^#g_kB{hU*h}U$r0U% z7yA{j^Z&k>DXK)$i6AsZ5R~Y>mo9N~=*($&!snbcVLM;ob~5UD5up|MkhJZ#)Rs3d z%(XN)(&~LXOxRE~BP)!6{Q;jkU}8@EPmENDk5Y3GxlEI7rUve&cWd;N>2rL6NolMs z=sN^=ck#(F?y?wTY|*h)&JDzhUR1P>LcqfZ^jPCW18I6m{Qd!-^DCtFo8lO2Q;)*z$DE){!jQs{ZgSR~Ow{eh#d zqz|-$jGFdShum-n+^xvh#WAAu57~ps+8?(^d#}ULO^P2237uYJY*feV@RqWXnGJj zDXy32XsKX87wgk0Q|So$o|b-CT!WP;f~8!eL9-Ms+p6|rHi(r?#_DxOc~m*`-YA4H z)p)ff{3c1MelGH7Vss_-DrcH2e@G06HM01_>v!;)SC!6!mBNJej$iw;H@pgr0&Q=G zOsTLA;7ipAsBWMWaF^-R^Uh;CBc2Bw?bUIGi=7ESf8HK22Gs#&3Fqe{qA13t%*BD! z*YREu?_vTVAt3|nBnVk7oTtPmv4^d#j>Jmz2>%5pitXovJ{JIAC49|^z@uh+!)gCL z)lQAQk++R_)U{qjB?FH|syQ|`dI;K}^H$WnoyRs9@;ndkMT<#l$Ewk^!e=PSm*Ycu zRu(t?x$(_QIuU3cG+hbn73u9mEZ3y-PWyY*kEqy~zEC_Dql%e$seQfMI3d}is z5|ZEZ#@UjIk0A!638t{dv<^Q@M3>erK1)1ukm%anES|S_hDEKoe8fmtR~4DB$Yic( z5vv%q@|ww1^<2|}Xdd4}v!a>!`e#RBxw>z6cXL^3#`z0$)B&hk_VlM#^tL$GI832B zZYb|x-Qt+}}AP5NVBH0yyt|z@2OV-Z++GyXg>s%@>fI2eN zFwiWtf%YhnYx;o^!?MlVfhhVm4V9B#WyiITuYY}Q`N#v!SMDMpAU zAiGL>7zP;s7~ocC1VQQ5S$29lVM%562+WgWGyA)Mpk&?+%$GTQ6*rs63|EGR$gc#L zt|bCFFvX@9=2`ipj*d>xiTc)MD$=PVf)Itn#uTUQecrSbhwbtmdvM^%pa=X#W_TR(P zv752~y5_Q;L0RHmifJ@h^H)v9x*ak8islO`iO{qWA|J`qKH^36=q(?$v3$}b-yTsh zx-A78D@?KpyCA2q({7t#GdKtv~wXsiTehrGzX*2WPN=2NkA$G_T|#%(mO)drDkx8QXbj z8vl{vESdw-C(?S(w)uB)FB+53mPXWIP{%E-D z%VfFwzbGiS%K|jIn#H-sqp()FaolRht>;!6AaB~n>EHqe)Fmr5CJ{n96P8AGju*QK zc#Qg$fx{Gx|5`=>$bNlnYs`!9W@A2OElaWP3Cq8}6gxewjZ49M5N=c)(Tu;N0(Wc@ zYgOi&0nQA4b_06G+|DxFNK0t$PJK*i&O-Dz;Q6*neT_8UjEuWY2biDP2uOTyiInpW z4WuhB*^&WtAX5Sa(Df8m zzD$`yJw8fmGIoq=^t_DqjC2@{dh9+YG1IJ7?kFV1F;Y$dv9w{^ifRy_eLhoHO`Lsd zMb&Y|Ih}6lMB*ZtJY1_QkggAG#OE`~SKUy@Vny4|36IGQTZTGi0-h3jW%JS+hn=QT z87Hfp>Hf9*CZzxeeKIQaB%kA0A_&~n+-)urQ1n@mh%waN1lhB9&V3B#1BJR*A{;<4 zb{5ST-j27m-=4feSl9a>Jrfa~s%&g!6W*lMb0*72#4meBI!&=LKLkoh0HZxa8*|K? za#wDQ!>G$^#FoThdJ%&r22|2Nwe+m8a8B3M?74d_upp9YUq!TgZAS&SuJ6R(0i{h) zV?WN$35(Z%RRDJ=428#9=@)04rD7Wl0L9_MvnWB}R|sHD#M6<_=h~hy?c%M^7{I|_ z`T_iXkj6$3*KY;6%|PC+QAA(Ll=-^J1iSze<@|O!>vEnIrWV*RJg!@PqWxE?V%oZ6 ze%_Txy@HN#bm2GK)@<}_wkaRhb`g)A^>+68@VBzJZBFXp^*Q)s+=Km%Lqf~` z>&|1ZhOb7sxZSKwUfET#Oq@~L8vzOgZNdn1EQCz-qb-S3?G#392|f1Ad8xuZ1O@YY z1-PX8!UOqvq>4;e7fnG^r)z1{<9hH~v)hnjyL)<|I#LN=dlw;8(&WNi+w^<3oFpKS z6+dUz)(2ilDOJJ@Cy00DRTw)hNyU3QLv{A`$bR%fbN8}iNKoO5i+KH)KW1O90t$_hd| z|5H6&@41&rgRml1s#pwox{blG^%g`$6B&AvD0J;f0_EE{q6nK z1Meqdtny0W{2|jkjaP2%6gcS)j)44I5Am1xegGMQu1~C!mbQiBe`X`zr?gd;`REa_ z*n;~$nr;(su9zCvngd9Bb+OR+57~2K4h@8=jzw#ugooLmARk-UhXcOdvzczK)|l%N zd*yZofzrP^h?;&Rk0&}k4OqEW&DWnAGAH4})fGGYES&BEvM8PTS#DGxz71z`YTFj_ zg;9Fl>ECOUqS=d0=@NYeL(7%oRaN(@nZHYAv`G5=HaDq|I_`;M ztyoai11g~OepK&SW~4w!mrG3qJ3X?3Pf(=lQV+S|6}VbqoQoPZ~^H?Qic=>>F!2AB^BxJfnn$#x*HUw8w8c^ zZV6#v=o-3l=x)w@=d2Uo`<`F3);#O^ckk!k``TB)ep>+KM}*H+7%$DJ)3T>t4?m+B zZeD6OdwW>(yk1TZR`e$p`}_ah6k{ha*j<>FN?z3s}oUXx&# zrJ)u_e~5TzrBz3*FEd=f(5~ezldZ*}%}QvoJu>1Bz@6Yg5r2vXZ{(+&pqx=8m0Uj~ zSNi*kh_`8MK#4Bvc#DktwxCs%F#8+=J5mg%YRsN16|3Y5;mWZb@O>q4Q6rVHGR&X$ z+s=c96AA)`NW1S6>*z$F$+l{T!kt>5Ar~WQajbgG=>AU%*KUEAur<@C!=rT$(?{I? z*=dmLwjkf}s@JM6BvrUA;#> zM08L6v*S=DHUj6{%@1Xl_^Kjk3phqIq2B^^hS9$`*lGnj#@6iL@V^g6V5{TZv!G-V z92chPJkz5>kw9}J7Y|gQY#0~MbZDf_C?Z6ZGcH8QyFL zR};3OlpTO7!As}9pEV0LFdfg2Z)f0~?Ks;dq?uU5VDovAjq_`H3CYNcL0HJc_m zUo45TWW{G!Ro@@s8!5k?GBbtAr*LNRvATe#A#Fb%eNoaKXpFL|smFaGa3X`9wqP+M z+=0zuW3>}Immcrp2??X*K3W|r!dxmV(-;YGvQI0(AG7^@XZd(Lj=wSf76tf$naK4k zQbxh(tjyEj4$R$Ga;rbD@RX>~^KylkrRXs1%CJCqt4oR*4-ff^H@(yZw7gDtlrJs>jG5B4K$L#~l@Ih-sp`j7dTqnHa8~<;e zES#_K;=i84%aebcT!{eNp2HX6X3V37irI*7P0~A|9&7obN_yrTW`32NzrS$Zm*HKQ zx9TOfL9K~+A6M&g{zzK(;R0sfjQTK)HGR9Z=DxBfN6V~T`^J-rej9gS`E*TK4&3Ax z=WGi;OHDs&`4^0kZ3Q-vsI7FZ$?3RK#yU9c#w> z=(u2S!SN7z??EPLsANcow*Jt)2aELnDq;l}Wds#bmE?AW6^|o3{EXIxKJP_FE({s8 zO~m(~FoU$4hYMs}_$l07{9Aiyc>vN7JJ!&#nY=_JyFaz#ocTX8*^iV9>#f)UtxdBn z%{L2gvqd8-DCeIxVk#m+jM{1JvHsZXb-{dX%*0l~F6hHj4N#{iMAEb@SVYLz-# z|7Fcu$oHbZ_C*4nM8uWkVyQxs9s~Q}~!v1%Yp%Mcxs;>^)yT|2v?!71scV;dX1b2Dk zPyA}pTBta+O)4^Beu?|G0*B9&gw5zaXIQMkI-43b_hzXoS9Oe}R%gF=%~`Q)=8Uq! zT)gzdCA-)_y--x8K}d{XFi$E#bq672##;n!4%!QvjHs*+<4-fa_y6M zaFepm{QXJanl4*U9DDQ|bAvBmKiJHlG}b7rh3fW9#Z*T-qQi6ilS3^a|T9_vaoOm^`MN(tB_)Z6D6txFoVmFEl-r>f7f+|J^32*Xq4=>7 z$zfN3Usl1yZR^s#(B7oB@eK4-#(qJOBv@=Kk+B7j4GhkDtz*eh!aW&MAEp%H^FSQ0 z)2^^MefZ1t+#PpcK@T^s=lCMkiG#MW7sjf8Vk2LzGXY$V__}eyNCMyfF*aq5Bz2Z` zyshG*T9%}d`N6t*%+{0IMU zQ2HcVV>K9yF6kEO?mB0K`m0r)tM4Co&j`KNzqp7%^y6Bz%O%K)|MeRE1Ic}S=i}_> zbPA4kG;$Zi9@q%g$fX#AE7{CIxyEolkx5qz&b52$;}1YKKF`^o1(+fsv?ukq z>9Q>OzhjP>%~xNbD$5vj(6c1d3La2-Any87`LX7t-pv)rhQp$$ev9a#x8ZY!9h^Y$I$@%TVF zY;lbs*9a4ylJmtQ+H$JQY#HJG3+uAxtFf0kcVcuD(KvJB2z!C*UKNSo#dMr!`ZT3e zSm@>79}A+1n4Q5S;!XwMvNo(j3B@Zm!WkE0=9}?W?8I%KuBA%PlQTd>E~D`4u}ImO znPuV-ict5k=vvPG;5u%B*49?_Yq)$9~nNXUwp@D+jh8?pN zXYfc%ItJa`1%}Zpcj&WVRsz|=*r1mO!(lwny!SP=nNQn@V^7`dSXY^oez&D}ErpTb zaMUDu&fwsup;$e#*nFZqXwUzS3KiBzXXX=8X~mYL9Dl`S9bOQ|?_mK8pR2(M#c~#$ zirxgJr&jqO;Iy2bWyy>J6oIE?6&kbz8;Zsw(CJd(3$$;5DX*>!OT;P)X zIxJtTQPxtx;V8k`!i#Jek-Zy z4xta}v$aCsno4TtB0Eg)MdbSv&SC@1LFj_ynVuG*5A>v_Hp>R*Y6(N=X(RRXaaWL&O^}@gphVA^)E4c!6-9B_ z0RIBGwf0P|<~w)yTkb@nH{PkvK0t-hNCK-h1JvAROwkny>feRI2HOz4uL1GRGdMKh z^_Dt|OF}mt=r-ECxN5P$=^fQIh?=ij;;R};Kj^1FFHfyZsXc?onxx1JH8|?$L*4Kx zyX{1c`>cw4vB(*ns;!-~snpyncE-Qi^eVFpVo~aobxznlyQ0wX9qij0Qw*mR49lLi zxxj2FDJu~>x|V;~fTA9lLj05}WTv^3@-Cj(z49+csOLT@-zy2gEpNzJKD*r$+xE_> z0OJnlVR=~I&AL-%$1Sc>q46Rz^7_$307)O?qr&f%ys&lzAFOWv(Yl^0c`VDi+joPl zHt*yl&mv_d=wUgkENHxEv%v4KtLilp_3ty3|1a*dY zk2*(CbQ7*2_K>VUaWP^jYQRKd&&j&({srqE7U2%=@U(mq)iN~l#tEfhU}pN%=k1Td zqKXf=)e^1PV$TycuZn9?*(o?Yn9YD2m8>)iLfL5{c4QPRW3NJm3f9EXA{@x0Em9*P z*jKwqd(^0{wcpOgs-5puG z>a8$)!uN2NhU(ad&t@(mOKQpz9;{cpHL?t?+{TtgWuvANCj+jN(cxJsh?Aldu&`_A zOX{ezI9`{1R#T^@k4c~}O%^E$^{+|q?Sa_B7LUwL9Q{^kmjS90XqR9>h5iI?tvY&O zpZj9;d+`Jb#C)HtE1$H;W3iBF$LeB)c+`nFQNKM?eJtiaHurGL>5kXhGk%$wIu^sx z*L);cyh6z!t|57#8=s#SNlycb=l6@WVjI+zT5P?^{r$up7Ss*yxyioG!=j_+(V>97 zSun?{c(34tsx*jW z=Qp`J)}8d}3$Q07TfT$({GK{3|MG0%`l8FffV4_-u2A1ino4t1>2O{#^qLHC6k@e?B3~8T|TeWJ&l*=%QZLN$W7|YzwaL)BE@Eh`r+eM`DX^Q>CK#Ye7~MY2?{=?F#B>- zrls%1oXpM!AFmGmm5u8Od#NlTAy2~sOv)-oneaflzI}W|rFg|~Fy<-5k}MVc#V1Qg zAN5VERR}ZLJT?ikizIx=sWjD)wGWArfWuJ_RZy4?S{~aT^AhLFl%&8#4k0n<1d8rz za9&3JWZ1}5oem>( zHJ=_WuGNC7a)*Bhtq%$j1_LWE;%G9?HcfpXKy!dfh&0$K_E%yX1HMG)ML4PI$gt6& zz&yP7W>}XGY5#itSKP5>@72s~B-hes&KP{YCkC2GVM(3#(2k?#RjccK zR(`cU1z!%?+ZQ%E9jG{ncb-m_c&=AZGB=;jEq^S;$>_b(Q!Qlio#K88Juih;F|T=- zajs5+QfcdUauHu^tq=H*Nsgqs`&`#+_&)N(T5}8cahMg*|)|9dwXV@+Z>5o zF=FC)N?SUrD?bBqIK6m-3zBNbN~$1Z0+8jSsW6kBYV2Adds zcr;Hsm!V`$S~flI$n7kk)*C>B!9k%n|8`|C6up{IvhwZF`Aa-R-&40Ij_SyYKdq5= z6j)!i_eJ+DOV{nA#Et;{`_+u~ePST|ilf`H2NqAy>0=W_#GQt6;g&;cg;a$ZIejZD z?~qwG+n*gV==^ef@)-7Qo?Z<-8PaAe!qgD|p3WN^67mOcC5->;4nqFyY}q|z;fM!O z>aWjanEbQS?FHvK8lPA7N_HKGht!WOEwP-e-&;KBhCpU;G}=j=Ks0I;-xcTURe~ zCY0;tKDbCvx!eqq`;Z4@AzkLHRMVP|uIf&_U%9RBju(o0{3Tny8TGn-0I^(chQ2Oi zg(u@rDhleI1xRYp>A3vN<+s!LWqO;vb4rtur*DE*_%BMsmGHkQ4P7*3hop}RkrR57 z=gW~U{lQ?`g(0t0o%v9vWn_ftyqGT zF(VR!l1+h0;^nq!LP8M_s582e&K(s3 zrb=2OXjLsNlt34M4@KFw(b;&V#R5|d-mm>YgDe5uWPkn1pIx!FAL2QAepmSdE!9vE zZL`l_&iy%p7#gD4P)zVp6fS``6%zucd%gL)e|Huchbh^`gG0r9J;1cRYcVpmoSRlA zV?BnrM(=;8Q`n-Fl!4|;HNQPzgRzdkJ@iX&UU*oZ{7si~03VqD;{dz zLe3&m%IaU=UzguJo?EUCXG86Sb@f^9*n?`-WjE*DwraXhJ0*Uta4J4yuno~=Bw zYzf@}i4B1&3o@Y2Krf<5KV#nxrs59&ANaR*5s7|)Qyi0aS3*{&lZVGH;@E+(52y{a)s3tFeQSLMkHy3qxs= z@PZSi*Y9LuQ4#p9s;=^_bqF}aSo3)s>$M#=mI#`CZ2r9ITSHMf+n%AVAz4{#4hCcjUuxZ{dA>E^-F0ieq*I1~-xw!Q&xZFAGj?GBfiSyTo#vgT4o8J1& zVPuNZi<;f+lN!Ju{0$bevnAPsNm(`WWT@8nY&&05I5otA()>TdsN^%R};k;Im1?buvx>6SA$s7R26z}=?`y1zNp^LFX1Xh|8xeT+Cx5TEuS(BuhNb3yc zM2TSB-!JhRt^81}w*&om6Z)9<^2&C1-Kz?b>x zpA%`MIk$JSmyDklp3i+r5>%Uzkbs{)-}vESVb1l?ESQ<$8`1uUsyDzi2#!uR@ez?l z@`D#0~A9Tf1l& zrjB;4>OzC$*uIRsmt0U!t3IX7RpkbL$|R9k8&l85KIkk&vKnm%cN$orJ= zK0efLMu^t;MLryL1`{w>c0VTw*}cN0*&}i;*sbEH-q4ilRyw$=KadZ+k)koX~p-d@A|uKPFCswic>Bt<7Hcyi`NoXa7@&0}@(yRcK zfq4_5^`?_40fld~du=mkuqxvlT;vDdAo;|P>OoKHoU7cUJI?K-0%E9J3An2$r4z3? z!BOh2JwpymTwVGRH+`-lv3-ibC7hphqg?#K@mt<<0`4p`f74Cf!aqf?I>jzNNuQt1 z-abxo$Fn1By&; z@mtmE=;gHZW!T?HQ1m&+^qW!?DaYTg;7sjrd+hY$3AUXLCai2!m~@0(wIOtTs6vR{ zt0HP&W*aGU+ye!B@;U)=f5onTn5jbhm22~M{ZtZ zh<+0pKk?rp(X;k{n$l?`8OIPeS3Y+odtz&PrfHmFfGEhH&<}G!Gh}${nabIg(D*LRY0P@4-;z=d;ahB zC$l4sOlx?}ndE-9o5%eAVO?Wo2P&>?e$Y$o=O{R1in2AENmdfON%H2@FSn8|z5qY4C}O_~Li-i;tEJJ=t7XZ0+kQKX?ubg@N~{t4?I-fL)Zr&4 zoAVO76+-ap=Oh4_FYh@n*7I8s{E?7`=9U7XT}jNw)g&m+n&FL?4=vz1^&J;=^w&U~ z3u@r1PcFZw`r<6KGYY%5xT`Bl>6P2I7_hK(V_@_4xiPw9T@{Uo&{(zIJ;;H_C7n7A z_asGik$RNG8Xh_(cJb{pl$uew1;JL3X6wP1mcV#g$F{XxhD9>VHOU5X6y}7291#Ql}rnGnE6R3y%;r64PN&lus!1l-)*&H(`xV|4~6r5JZuYdft^1gkUV zi31zRAX>Wr{j&ozq6xbEfByK-W3q@5dp~ABU=&Sx0Y{lS9YY1Isx`jLTu$cPo_%yO z%61m^0eEmA9)2;ECrL*}99Q1$iT)BtG}F9+>e>L>^d6<=2v9FT`d8IXCr`LUk;EpP zU^;4n-Mxh^5C0%AM_Ek9vN+f~hx}E*^)YHirQ`Xc#Yel5arM$|>z=UbI#piL$W&Pd zM!BAtIx*6ZN{dN<*Gzh@U|?iE^J z_Cev!+0qOQm6J0XJxYl%4_U0;@4X=Re86P=$nj7mmqe(9wyMUO&FrvQtJI(V4$+VE zS`e3xbc#h&qD~HVQVf7ji?}4gbbLp}JV6iGZgTA2<;TrWCPx6zDe1NDJ|kS=AZT3=wOF@PLhqvg^588@Z~>6jyV zK#H{W12#4(jfl1|*N`i$;hoPmcId!O?T23X86U>B+juixuQ*I5dPZYo`UJ%V)oBwN zL~}l0Rc6E?v$c=|pvnYaUgnSl4F6ckr4ovHaN8WbUcC?p$Zjkbn{uGtGO@jql1V1W zv|eU9IkG}=)daa}mNr#DFnTWJ(3X$F5VgVQ12?qv6{~RTuN$s`S@@?9FG~WBBvyKL zJ5R~)*J1wTPkm22M;pG4BBHGeEjPvIciyK5 zw8C|-QuP_T`4h2+2}@X6fwzQrOD1wMXeqt+;XAM#dpP^&a$=@};CR}D;rBJ%m>#3T zgbT)*GyBE&**dG~24>UEbOY3%w`h7I9c=4g5G(gurwUp&c77lgsC?}q9i~PQBq^UQ z$EXI@&~C6fV^-BWRU!Vmq2~kzN)GuEZK&tVQ>S>{8A`?XYhSuRAcdZG4e&9)#Y!>* zXxI?kyKx3R8^?)MZ-hK7PI-(Bv*7fWABOp56oZwOHpdvV?QR(_s|@!r+cSzkLhOJy z0rCQ%)-T1+I7=8)b%ow%o$V`@LMXAxXUXgjrhZ13=}LBQ(;U!K`?V_hK{VOTgo9u? zMR%|)G(XJPTHzo~7tI2G1Kd7g@hDkF*=c;_l z1p{swj!-CX)ud+$dca3jrppZy0R@F3Zgskim$gXgWd|`qq}K`kP6(-a8q`O7%pB23 zqXFg3X6I2H|4B<6O#8-o)JrDwQ{3?gW((z3F*bhfJaPKjcB%Qi`=ge;@kUoEP6TIOIO0m=`V2Vkq1jUsjFcN!Z^!HEMG4PpZSE{(gRbITuyD!HHI%P zl@NTmJPCTZ5bAiW&+SF}j!AbsEFBHC^Os^~-u+skJaub?ukps8y8VtFR*`J-BI2p2 zIKBl{?s@gJ$Kn|oE%ti0YEM2?8$y!gJE;yBNvJpjtd12POsZf#OOOY`17ACk=CMz^ zi3%J5&LwiL@`jE|&t96C+_9q{tk(Ybl`?Q6IEcdpH zNQcYZ^WYq)H|aZ0JHbh7zST~&^#**I4d@xeT7*<7!aZ=G)A!GJ*~)jQS(+6YWM1Lk zMYOF}t#D+`dU9pxt=VFbfA_bG%vGC=VrDLz1z_A?HYR82DDWcOf2^L4jMN)s<9x`R z5@N%)Up4&_P@A8CuCyiD0}aErocx8vPeLz%+n;Xyz6#&uJh-VE&!$BSrFEyeeN?H7 z3$J1{r6J<_BPdZ?T~A+Bt_YV|(9`Zv1-LRUw)Nd^e!BCyJddU`C7|8zrDIkf9Qm1H zDK=07t8h8%&%4|qBg7siph(w^YVXNpl6!v>%Q`Ef_6i~qK4^!1Xw_B{nnlX*i6yZ1 zrS}tIdo*tT7fyfPy6eS1{%tv)bARZJei`eJMq`v}j!pJ(Zj1`LHonkG$%Wh~AQ(X3 zoKEgHSZjEmsnZeElzfyCJQSWEP4!^aq)ibL=l_eYPK5O{`6t>>MMMZn0DvIFDS^W2 zu@I#Dt67viN&{P*JoL+-@yQ;eU*nC&xR;hntR7;!9k0Nl6u^Z{a$c6G*naUZ4>@zb ze83dZ{ZEYJMOVrnV%-$AkGdXR%UtyRq(-pUB_vMAiARKM8u|jhAG}czvXcDJy+xtV zc%*;l<8in}_|;bCV7$2F*gWh95&KvXv;jwD^0BOomtg-r8D;%1x2o*2-m-q%-HgUJ z{+1KXB_HdzDrKv`2k?xEI$UHT?rYEx-zJ0$eSN2&6NDx=2l$~d(r7PGBzU}2?7pB9 z=zLd%cm5OfA!x~@n= zF3ITq&q+E0pHP?m&eSmS>1DP-iOPv|gHnM8ox|eU@bMg^xfvjtJYoAPsb{Y~=Q^js zRvK7iQdviUHa6htfY`j#?(nj-oaM2{f?d+W8^j+q_RIfKEgLri0IWQz|I?BEug9yS zUo)oZp=9;-5z{y(Pxl&Y4N?QH?FwS5yP0LY`{pK_SueCc_PmHxkXvM?nV_2*nAmJ3 zZYx?6#`KBDB}|ws&|LfFH1=CvZrw-US;bl8#mDz)l|Q>o%OV!wON6Pqje5ml=F_jE z-tpdIi_sHeIWz#iGV5-*wbai1IuWDd9O<7z;~enf&vj{0TDfpl?|GA?Ur~~uL2OXlKoANvkYj3h8sq;)bf}v-%6k zmuOyLHE8cQ)@Le=ENUy4o4LOc$}H6uXB#ZT_&z&LVU^t?{Fg>kUdR!Urs3^v^d)$d zzgSLJD?r0{=N$yoH#4S1I*+&}XWGt~B3wU|Q*WhCJnx;oBrA2x%?+oA9EW>hl~?|} z{(?h>$?e-Tk9|xv#1u9Msl@gIdo1AMab1tB%n|OZv4!h7Ol2q_ct$O4pqWyHyOWUO z5?&QLEdU!)hPPy^3_^^~LGmUZrkNJR!U$5EJKZ9)Ub|OP%YUcBGGg}ncgptNDnwt zx(Dy_SsJjoC>XyPdSF=5nlw+`f#t3}Ty-W+PHHW-dJz%POaBH`cIIoIhhh;OACD_I z%mDBeDv3PHHggz(j!L^gVaFG+`T2DD$zI+c2kAR>^i`XV-;>P$>eB*!{?(^#piWH;y`n05+f|+P ziVHQfu`_C z#qb|CH6lCt76arGJ|mDEmoOf!C`@wTlk=)=rJju&znTXl_PpmrU?Ib+@^T2$WlTP< zR{raEDuxjdz7Ck|_S0A$6O|~(py)e#V-elemeh@Iv)wudF2N6txE1P+kZ_i1Iu#DE za$d`{Pkf|)`FOtr`?pf0wxKk0#K64IE~jdR8NZ?MsmR*6)=WjSQf{C9 z`@W~A1hY|BAybX=zXjEIe0v8ka8*m24Q9e!&v0cYJ=MlCEIB?5n=U{NLdB=3M&77X z>CSBNY7LA;D~Es9hY5m9=hLskq;eQ$4Wz0gaE_nfPM-`dT!@p^mQ99aa|o&vXY|^e zuL{u%=Fv+D#*eyeN-??ZddtjUkxv0tncRhUrG5zcteri>Wk|K^5`{BTK_O#i_J+3W z!@hWI=v)v!jTX~3UDrv(TW?zCEnJh~mnYvTKp8VQmnw>x1Z^K6LWtnIT+wO^$cWFk zw^b6mq92abXtC8!`MaY%`mpHPuT>{o!Z6`2y;ku7-M|~uKBuS9 zm7aIAd8)H>V`dTuQ+2Jx_MGJ*Uw!^WX+?fxx3k4S#}W7vqfP;rmomM=cA5G z;Q8sSr>rH6)~$Zz`K6X6-`aZ2747ziQAxIAUrBXQ*Q@=8g&cEPA$;*^1i2pgf|YU6 zFYUtj;7mAw@1>4HWfG&g4O;SkC#aN9C%&dn_K$FW_JKh{E)RwPK;ZVHmCBeam4VSR z%@kYfG0h&N)`aP@ye;+7oMHQm1Ci+O2;!YLObyUlnN`ojDh<@s!ZLVmNGwDkrIxT{ zggPC<09vf0Z7J?+Gj$)OS#V5f`XXFG_@b3>!v@Pigo|5U<7E95H`Lv8W;iP~gr|(D z&3HyCYwi6{<8!DdMn&LL`78TS0^004-p&!XI zbn7u#ZuDwrvtr=a%3A`psOD4<<(BPcGdJHFs^O5D5; z`i6BZ8Wl$b?2+P;)->qu@nhPAX(_uCyFIVVGGpb&(T8!KofSrmfdwsmzeKxV7~~Co z7fZ1~_tZLi7Sg9Ou4FA~bH1JCCZt@Nt2aIHfoq^`$5kxj*N9a)TX`JQ>nbN5%bu~H zml#4Rx?>;WH~R4Rx(`RbzBhhXOTx2QII$~5$NtKM%$6|id&5h523g)TsWF*lwx^|` z6JeJT%5a#a19=(LH+3-15N##9LhTFWXQ4~`(YObHN^h9{VK`fEeg21_O#d;9(GwT; z8QV5U8rt-#gN-eKX=KQ+DOhQ%gZ}scnXS9;pjX!Gx|-Yej~R|?1^YT`=4zZEO$f{<>3DX)Q}YfOt`N7lI-Pd{Gr}4GWVXNGa~ol zE|BQy_~@~uX;;`gpkA5vi99?95H9`=e26KT3~NBh<&SJ7y_MjgqPdl zdd76#H_smT#R_D3N@b*D?oynsDVaMbw7z($q~97I1fho}w5d`>sBbsz`hMI4yuSX{ zY`U`ex4OY}ZG8>YvR!v_sn+FL`^8PTW(aGkL>Q}n@A6ryr;c~3(ryY;oHsO~UNYtO z^??r#EWOht>)NTrB|~sKOIbhSrVY7={`p3ZyLG+w``h{ScuHBywppG-1?INz;l~6s z-YMaZ#d&{@H}4B?(CQTiHmE{^|KI|8gW%~p&W=$TzX(35<9OZW>=~sB+pXu2E%Mxo zJLi0kSoy)=PKkfKP1ndGSK)Y3KwKB_-mPXPvAgYlz9+65eR?{q%+F)uiap=)ARhxV z$gtZqRb5d}=i&Fgr$@4QwJCri;jhCI{W8cmK;1?f83K+D3zh3=i`a2h``)$*=~A7` zg1p5fQ#MYVp|c;XQKq$BN)s~-002!)o%k9B2serl!*?#b;y0u7gVN7GrVM2$x%+l{ z%*7W{uZwkb9FX^@i|y%6Ta6sOI&7Y*ULHPO>dlb(e8d|vyy9~uK|UG95|$P6vo(izdwB=M-bmDzQJUGG*zXY# zxbdG?AL~pyQ}0q{=@E$oCiPlw(HB;!L0=9c4a>dc~{(CXo?3a(mWYU|} zg?l<{7HPpZQI;Irx;)H#)EtZN&UoaMGQx)$EkEb>@|MP@Z#YtiJ|`EBlj9n;@f(?D zxjz*^yh3=OGmY2}OOxJm{}L09uumKblMd|wVin`e=8apSaI9I^E3#sIBzGv@(mrSj zV@Rg9gI8Q89=mdOmrd`7=6M?cjdM1M(zzN+qY%bD)10%*gZdLF1jFDQXHm9a37$CY z_t{nnTCJ<^J>PJ5lX;{&H@Eiva&oRYJkB%Sh;@?eLArHeN-JOMZ~J>ce(F9?K6qVR zGbla6znndqYl9dY`$dD#U(DPn1T<{W&0kZOE-_C(W6HbT+;ahb4t)@1mca%j)!rM5${IOmy!&xV>g!Pi^|9B^ z%!9RUH9P7$|3Zur;cgNk1C?F9`meo!5XJJvkC18syt+7TM~VB(qenWEz)O6@fe!3| zjJ@%w?Ma%r`2$-P5eNzdJj*9<(Qy`E+uz4GeY%}Jxq$_m1U^Dx_xK(6TRg*8b7p33 zem|EM7k42B`SUX;1|s7BJnn%V8z^=E-AN5-L2t^aOvCY~v1oTK!Jk3_uCd7?(($G@ zvyZ9Qfy*?<>kmi2=7rrcz`&$!jOZ>0wEn3)8r+NyEm7Z>dd<+mA{*@u)dE2VsIqMO z6Lr{Is`P_VH|YfI%13PRnKA~(N)}#fdnQ~wo*``z8!|;Nw!0ObqgJkOK||0DG+G~@ zIvo7Pl+MoZnHvrdN2ZZU!n?lTYJAuqpIF-+N@z~NwM2k693l0yWcX`cGoiK#r}0Y( z=IBsr?;Y2!tb$R0b@bR^ghGGpk_p$Cl$XTTuM*5C?UwMc0YR1H6O-Kg)k+P;m zHyrS35YiqqGHFFZ6RA97l{$LaIUI`;laK8MV!olORaq!?S@4Mt3sUru4y|b zGi$Ny(1Hc%@tpEUxH#a(@zN@RPoFF8UGoo@G={@00@)eRv?|>ceh!&oKdtDBrGC*N)I&AFj2f3b2QjSFT8Tg8~2opCc%u+qFQE z8u)6qZJ+H@4e9MN7f>p(Qo7WkHuFD~!w$JH&v37`OEnP*MZ4a2n{I4TYW)*+p;tit zHF3qm&ou-zFH}C-4w6_Z?0uxDU393+COW;n{xk4aBZrN_zpU8(>W-d>sg&IG7Wa>R z(Gg^Ed3k-g9dcc7dM9+>!*h=o@Gu)_Tn-D%5`*bGz8w8*x!Q8VTDfsjc6Rwx8}W~$ zyz%dE`3LtSa`ai*4YMx0kD`Kw2hX;Lcdk!)9@+aR)@5!wA5E(kO)QgOXXc4>>TDZP zcH#`ept#p-eIs!VpwCr3sxU`&?W8Q?REq36t*>S*n$5dNJwy4=$ujIqS|HG@s=K87 zy!mbH3mY7fNe0yrEw#$n^sINC&wp4^hEcWN&dQSa{?eWu)g3?lNKpIDGS1_}`Iw*J z4Mkhe_;P%&xCGkf0PL3(z^WtDKE1)`?=Abt!m@cX=7S#IEJ%vs(D`x6xN# zY*glQpjwAi06)hTZ@?D!{4h$NLmVzKG&9X#F1`KfE*XRCRZ(*G@J)mTlKCb$VWQdE zh&JWWB-C58+*<5k5wTNHUyd-{>8ZD;mdNd@pk8j)>}CLBMED%B=OM2jqBX)G7B*?F z#Y9{O+h_ZR)VXl=ZhK?pkRCyCUA?A3sTHqm>5D3QTd>6V*R14*?z_%ZVHR3t|D=~x z`y&rp*y7`t%NDIBdncKtfO!S^#fjOE?|dutgy&>R=a=#v>TT!?O|dXHufU~uBg9rY z#W*Y1{UXPioi~}+)20vFlD_9}cld|ASZ6tOEbF9Yi8q!I*z#urAzK5M|G;69c$U+@ zXJAdWA6{K2CsL#&>p{IW$}$e)ji@pgv7I44+}E(D$tSE!joiGIbL@>AM?>f&+fMms zBBlx_W%tq`N2(;8&(Fn~-=t9%3I0?}>t7-U_-ItYerU;eqUR+B@h311W5NrNo}pD`!h`i-f~e`w9I8NP0rP?H&S9QXa`H9bM3L07B7bsrE7(t}G8)URECkltw_JM` zX@e}hLufOur_S7T*Fk}sb|TW6PbcZzJ0RPMuqHtz@+D|$py+a~H;u%aww2}eGWBNG z1Dc;-GVcW{oaM(s8oHyyg8iU6F7ugQ<}=@$CPqE3hlB073dC_XQO)Q0?ZigSHu1Ws!?rmRG@ch$3W6n^T+C+!S5MDuTO+xFfcI7+#n%465%Y8t4n!a}QfU zaCEv3u&I}2wd!XXhWw6gE}0%P0)-v(teISb6u)|Khk0l<%*YGEFKG4?=W5n>?T87d zr*ps+2ARUv;ePr6yx>(UB2>ZrY3fZqxIt}sNS)$np|QB`v)cI*%kW#v>`ZmjBMpIM zWv%>`X)cW^AF|BUg5CwCa&6tT<1E`#Q2dJJT8j~_buydWLhjEb>{k{ku%$`044OF zp{Mw+d&NTnFCr~xLv8_wVfXa^f{4NbCZoVx2F1)(LRRTdwL5TfJl z9r>!D@HKlvs2VlhGRS`x$dSqaYWx}a#_ckhT*{eR2Qs+mzbeU%`RlbCBF~PXH`N#G`XS2Ky)yjZ)U8eU*EIQ{(u?2zm=p& z=)m3BB8E!GsGXQpyt+>rr$6`Sv;nHE1N}mw$2I8n%V$Aeym2@JF8HBbc-j1Z!_E(* z^SqB^_0g&##-?-WU9{KDODjfep|ZZ#G!5+2{&{24Uk5k2!&9y^Eqb(yaE)T4WC>e1 z{S5tzzQOuNw9vU;=*8_DekUN*MH*o{GOstPX?k`mdHW%#GCVaaufF$&--ajcL5LV7 z8^k`#1w~bq=BXIh{sg44x(M-~zlt+>UQ=oR!f}jO6 zB3&J}?t>9Qxcj+hR05{opHBm=pMNS1XyjN|%m@K`hUr6EwKtM?te$&lJtiU)7<}_& z5!nrI8C~y(od!`D%0+yNLOY-+wkNfoF+6JrQ7J{a@npaccm{ z^8YN(sH1nH{)$dUJWJCkOOX%$L2^m;lfqJ+)uTPU7>fF_=#E5*}|# z#crXV$3g04Hm1wKU3g)c+Z0&-us9^xo&6Q&jW4)I;P$Hwo$0b&1SOV@Xs_MC;jbPfq&ZJE1EZ^d zNs4*a$*t6z_iw+$v)-HuGoyx}zh0Udji6-AcTnX7eAB58Sf6CvN%+}4foc$5UuSP% zIXa8Gp1j|Ij*_j$_t9w*HCy<`Cq3>6D-s&y|Lh~cy@im>Wt^~CM|L@luLP!~nw4-7 z^DxPgcj&l$rmOr;0fH;6{E+~oWo^x;6EPLiE_??EW`N#|(HmuA319?+vG5RGTj}fz z>B53uZ}Sz@Tzb725g)KaiuwQ&v$6QN_PjxX3+#xJjqA-F-D6SeVP01R)0tlEq!Z}eEc$tf<4&|jt3#=)r68|TUC&sVZ_LfORX`zCnuKVTNg8T_ zj$oGI<$EWqL>aby>;!(WPc`ixjE~z*YPw+L_C%5+JmJl4BHw{fdtCzpX-XmTIu|i{ zdfRu)PQ=~m=IcMY_amKu?WRt?WUd{S=7BL~Gyd=%Q!&o9ksLbI&Ohe$pugFL+(~ia zADR1|&pPxBX3Kxg|2XRR@~QRdMErMB=e?Q2R8YsUxRJLnk@vqsR*}pnuK!G{&$>OU z*J59DIS1>Qf^n3ENRYR=IST`^- zush29u!1Ij!(!|D5>J7>N)sG@n$bnI=`Av@B$Vo}NR%dyWXhA^(pGm)ZFE4_4L);6 z8)OIIA9Ab`W&`FLIf7K<*xA{dl3sGYjz}1fW4$enbqa|<;zcKH!40E&gQw^dm=KYh z@){<%(J%(~(>&=uv)=5Gc5@7HY?b3F&##J*xg9=3^6BgmQ3T+5Kybm(DaN;Bnn;#J zAuQ0BBY8+Is6hEIP2P_jMR!b(DaDk-XA>7T%hxv?GEd%?3i=Cr>RubVAL zP5I1*7frV63DztEhUI5dH#0AI?*KWegGPm$LUSP! zb`_t8a@EuhC+XFTn@Qj3l?Bbf)(d2`{JlG(XN)rs%kArZHBDBL*@HKBlxt@ zK033WL_{R_GTsvGyqWS7_UvRn6?*8*N^~>yMIYaRuE~D*8eUf$ia?%^`CE`Gp+hrDLGM!Y02a*($wr;{ z#2lA6=BK)GhIz``p7d)}(}&loVhsjI67Zj89lVTAkKLnc0Z&gXCytjGRNO<$aRPB=V~u4)B+#UxfID9I#=N< zcX*k)PpHdHS1;6jL!u-q>LEisu3u3kFKye!CIE@2+1tx}xUIvct_OxH_WJu#gMC54 zCU!v&+!IVotM;wEb@#7sXRyYO#Jj5X*UTF=pdAKau3wV{mzwiJM=0 z3aeFq03$WgnifLj8+C)Z2XAU+n8iRbwW(28NWGvWs%BS2%U-ZENi^?S4bD8Kd%^Bg zLf=+N*KO3(%8+R5oXIOfai9~mZg(i{g@^7@*DlBgyDUAtK+j4Fsyy8E%2$i=$TSPS zj*)5ClG|fsYwJ)oYy*L+k7@`~6T2N)^{DVA_d7PE$#DN>n#C0?>jbA=g?^^B!}FXu zmDOC^J7dA=CAYvoGIDavZ8(k26zz@MNy4siI`Wx=KRQsEK3#roL@07V1XoA-4F=+d zDe{dk!Ic|LJ6EgPIp(vyHw$Es{Qp0n^6B31R@nVyH;24aP!1Uyl&-o`;J=G#$KLN#3_1o z8UJH862Wef~=1% zqmFM{FZ9@c&m4tgcXj*}n)EDtVOoC^LNkvAIT^BhiFqgsN6F;K;Qz)YI`kZb{oQJD z*ep6ty8pqYi%tvv@}~I^8$k6OL+*rcKXu*2G@hL2K3PpJGHi_PIvtxRLV< ztIN8^9vfHU+CiHV$xT6rh;!8W*L>0DKo7%Xu2IBJE$p-T!xK3ePUh!f#o{%@M}x1j z!HF>c;U(hnBzmqG7>*Gg14OaKuhaI|ppYth`IYq5R!^n%tJmG@*u3MYQF%SRB~tl{ z!9R(+rlLRn$ zK3j_dU+7!&n^n9r`(@S<6ddWnm~YD`S?0(%5ZAUreYS>NCz*yNR7$KT#!-t=vYYIP zwtgcAu7!oFAKAdc;5ZbqFI6u6ZQ{&>Bd2cMpYXIs`y5rZ$V&npE!g-soCTTJa9Oj= z@W4%VB1yMFFAzyp=p1Y_a@K+xYp8Y`>3iY&-L2f|T(vB6@LM}*0+|JZxMXjF>RrD&ry@_HM~R{7H$W1d<)7q&o3ZqArn1N+psw%+reBAr zUUOishiPGkKuS0NpgoM?9k5BTjr&aa{J8}b!Ro*Ks~`!R`?YyNA4K(M{o;b|jJY23 zh@wE4gYenv75+`xgGMGx)A1Kw`qQu3VK15HCOZi5C9lMF;?czKo zR(p|x@bOELyC$_^+bO#m{fc^O;)V1E1TJ~vf+; zRdzP473s>^fO>NyhM+VO;U6wjfQl&k(GWZqC*9ARQeiDU52#o8b<V z;j8Qu;Z8ftV_Rn`P^WD!Xi1As8{3W z6kj)ZntS9+&q!alPkxij@M1;1y(v?ERT#BV{NF8SCe4Tc@s$4^I^KTUA*?UOR|RZ{ zUFD>OF1`y@D?jo1FI*DkVR0?NCfy$3bu5o^T}HYXxzBS+*V26-=EAK7gA0IO5YSKnJVCL-J{UJa|Q zew4L`v1?9@QE~$+BEt)}J2a>oMhu8~04<69p#D!k&IRpTId&^AyBa$KEx4n?l{+JQ z=T}3jx!L+B`G1Z0Z41ce`}Vx&BS@0miTU!iRsX6+%O8kR~W5FzlKn%x*dn)C--g}6e6pY6=f$Pm!&YHOsUwb=Tl z(knMc*nbThT4emf`4e!TMUYi3fooV3Yk!JJ@mgy?=bL46m+fS~dj!DhODxalO&fHp zIzPsW2(i>!_ZayvCK9$RXQ^h&+t?#h>3X?H_36?R50xL?OrXwcxyvL$S5s8>-h!byyBl z9S;Ytp$NE6iS?3CIZ*W3NqbbHby+gwxraIBG+Gptu*eh6dcVQH(gV-BrS!eR?}&su zbmVQ^sG!oYUPC%B=T17G#P0{L6AW&M_8Mz)p94~!E4~-)D$h7^+_rc8UcET)3=`8A zv;SM$DSnjv?_%_3DiQ_y?{83e-*uM+P32CJleZ4OQfG$`X3fFw!Yq!nsgzU-QP4*$ z;do_-ANn1DVO;ns0>31jS+S%)X+n^eO1oP|H&JV1z8B&*d{jXFRz~Il3MaR3?cj=Y zdZ+)Tl()=40Enrk*!lNtzb^n7*(5hVuP|bBxD7CgZSV>x>NahaocRst$X+c%f=1@# zW6x3_PJUsT3ZcM!i@`(H3j@OKO=_3C3X8w|E;^ZXZ3zfmxoV;S(H zy-ettwBZmBJWtw@)vs0Z0486Yt5^Q&V7R2GQmQ51KyOMk*-`A-I3rF=sBp;x?0xr2 zlkFZ$rV#bZVQUO09TS4~Ee?y=$t~pUC_6a%(j=|iGAMDNFTmB}56J~Dxwr8~QJqWC z8QQ2PoZHU{CtQQZEP^we3%?>eDgx)@yqck=={C_#TDp43h4oj<`t1^(%kM6&1smsx z3Phi)2FxM-tQ$fLnY%Pj1E=4WVCqh_sm{&l=)8^n`*CtWHRlpmt`!buS9a{4T8Fu)%vq+n1oW+UfkTd5qP<%FH;xuj4A=4$o?@_0 ztPzY}|L|q`&x(UVzh*EgXAEq&5nnH6y~kSy%|TfUJ`Jb*Yg>8Ps5v?}Z_#-~twsMX z4>H%!_8GDfk)3>r+|@eXdR>QfJpqGSV0`F{16H3h&C}HSnqR2_ zNt;WHT^@+_2*-HVy2OfJAuhi2G#4d#L?;}GUez9Iz>0tvlA9d~KDy_(W{I#||5U^F zkw60ypSk@&6z}331bVZZP{fDTxAJml0P9@VuS>)4ILbynD7mHp_^o0|XjG9C?bB)J zZjQD6&pxn>UfGT;Z8E3M{ZmNQgGk5I8lm@DbVr^XQ1Dc-`(-X zPRFCm$k&%~=iTj_ZgFfr%{*c>epmnahD9=I`2PDRJhfvzwbLj%Ni_;~Z#1rSucckR z$~vxQRmo#sQwrvW1Ht`gMyhWwO~ZCAon=4z%^Ic?A8h6JU(50hr3Ue_yjOH$&5=Rw z3UUW|e0yonPSo~3o{DJ1``$25f}zwbiGW*i;G_0&20psNl$ajQrA_H)S8_&h_;2GO z^rH~EE=&|Ltj$D~HM7S?0&+!|d1>KpLjXL;80&Y4N=0{$;jT`7;0U8!V7EzLp(Q{- zxemETmxVlUCuYPlBVyAQNCmT{qhioH(+PHHb@~1E9q_Nkn73c}RR|d)%j!XWFZ6xN zPQW)tM4!Xp3j>jH0bv$RyV?0eM#_FIf9-%WX}VV09iQ`a^lM~$p9jk0$F;_j>tTcE zE90cQ22^Y_qqo*<&=jchC^}=j*Dd7}E(n|)pu#dtS&_VKYx^oIN5>V}AF5ux?+;&M z8;Xl2Xe9!W;7`BKG*iBfi>#kp>uZyny0V=5PKC5dCwR_km%%{UqQE7wbcpe#bqIQ%CN1?bYLChr-r#pab14t zcsP)4othzNkUA8eT9Q`@`Md2Y3=>UYSs%rCg*j6Nun=-MueO;POM)1O`&c8`d?<#F=Pgd@GpUX($?d9$DbxZ3SzH1~W z$4?kfNFAq$&ZxL4!kdF^YXxDxZeJ;AdQ$hnzmNRDSG!S9sD*j0=} zy`pIhVdL{`L7s9}-z87E{Kqd^RIQyz;-vbWQVF))cZjHX35a{q-hg|jr(F)xCS!|G^ePhHxH zW5Y_2lT72!rIu5^ijmC+f2H7fqCs|SUx3hZ=|ga2g{j)*>o@cSpP}*7U>r*RZe{K4 zy2l9GJ<8UcZzj&qf8LiUNv>lCgtxN@xT{ICld%j~0SgTru^7^Fit3wEu9(J~`Ww0I+4$75Dpjy?k|2!f}r}W(iNQ#`j^l`(PqJ zNer1f2zldDxF)naPsB7iR(r3Kq2blk;BD0TB>i?_POr%u(qYW z508<&p;zPEdQq4W8zhzp_U*(I3;V3L-7o;G(}_LB(PwCQBO-)-$ZJ&$j^YuA@YI7`XfhU>dSq)xdy=qz$~nopgy2)aYyCown(N$eM2Z?d?2+> zC``L8cz^4Qz-j)$4Yh3n%ev%Eozlw0QQvHq3jg7@cJHl+sPeYp z7*=m}ZRm^h9DkWvVJ6-V{R*f9GN`}4wOeXZ=MehSxXi~xT$#H{Hs@4I!jqPOV}&i| zn5ZNcKtWsC2B39lc9g5uK0;`1D`RSR{N#MSo`@Km4J^0l(!Uj4z-u`YKi%Rmbb8iW z-#tB6ogQ40Gm=4SiRZRK)jqcRuN3ENbiFA{`1!<0o6}coRwGkb_CknMz4rBCU(T6Q2VdumAEU7GO}UK z6C5wW&c4ULg0}rS=@ql?*D&}lz*=`m$N-JU(oGFO=NRvzWw~QJ@rf8#5MGe&hY&eVAj=R zw$UMWA)s(pB*^q)ysI8Tg-S2sA34|h+rQw&AVI0={iQ`+yUbk};`(ee$Erg>)mqB1 zf-~#1y45%bePTPNarAIkop!M(hX2Ut-?S9J66(!fXW;!RS0Oq7$N5iCi2P3g zKXGQjhB+&HzoLoJL(TT){fA^+QN_Z_ZGyC~%n^PAO7K9Ga@B>h*W?gq{@P5s(V4b- z`bGWoZ<{baqh*i&L7{g{R5)R_KmDI>Um%ZXkFZjPV?V_ z-wxz&ceisUr{ZHPl%tVN71p(2TOpKM1Sm@QAq!3!;pa3u>6uyUYFT$8)S{k2D-W}+ z?q^e3-3GUiXUE0eLe(0=Z;#}(kHZJc!lLNbu#Z>qHb!jR*`GKHD8Zv_(>^$dFWw<87VUGOR3VIC}EM{teP98Crk1}toX*)Dvj8lEszhfJFX3S${D zw|(*DRzX;!Tc-YWQnI?Hr&P?YVY1i$v1Efu2kxXyTCf9;4X020isEEk%B?1yIE%r; z%Gg@gE+VyD=^`QZdi?de6s#2lPMn|8ju-md>tw}tc=KQhE{WJNO4ypEWlitVVsyxZ z3wlFIkdzGw&;f*wb9}ojD$5YZLIqnkgzu?gcmtv)3*X(rDe$ng+Z9IuB&W-;x^YSz z&Cbs0+a^c3m43C{cViW}wuv;G#%CL#<&qZrTuA~G&psALF*fELR1du`7UQj9Y z5J~~17-hvXHPXV4w+b9mOw>uZ2VReqw}0{(DN%PPHT+T2`Kek9b`&_eV~J#^^)hH( zi>Gg7;REHB?!&Oq3UCM9C2XD5Nr0~0H@3d8NE7vK041={iK;>p3RJ$&IQwKJ!k^VF znDQJ*a`oryoAo1>Ya4{oW^-yemT{74;X&clExp1@eBOhW9XSls;~G)&;Mjr^eRZ#McQuIfw`qU_qo900hgjz@Uod2 z3v_v3`ju9vosF1LDijU*(OXlhpM`H3D!_I%pSz76~6kJ>7*({@tSaj4&9M`>WwAPHrc!a5v&!B28X%C{f@mX^GC;hE)b-*kO-gP_mLNO zVGgj*xaqt1m<>^P?ME{3S1w=&CPK$0oTz3}@d$dFirahzIo^IoxtxM_VLCWO~K6$n56`ldnePc!e+k(ubHy#9uO z4&5Eimqk(0Y*F%DnjJi0Z4ta=C_60b)1>HxEWg2pW&s^A)I0hEg=9^|Ar(Td!#_kt z{K+X7w8soRBMXKW$TRu=TKMv*MCW{W&YV0R6L1;fEx~&3x-6Na0x?ZRd&<0#b%_Vw zg8t2WEQX|?04Fw+;CgvLnX9dH1a_W$Yx^wl*K;2vKrddlMIU$mrJP=QqTqJrG~b5_ z)^YCb6c>rqG$uEn?~=!8m-6=7JG~Eyr^EW0Rbvhg@4{KGT&b(f^uNyCQ3Q`_9n&dJ zq$IoLUFq8hq0L{cVeXm4-?nAQf;*{ZHSyb#C#Pz6$15rFJM|MAB(=;J;$$k5_~NwJ z=dnbmJ{jaM9kx4p-(K*azVy4$I4$curMNy%>U{Tdp2dFb-8KsTy;ABX`Eng4k=`U9BV{CpE|kLg-}BLivV!bR&#JIcvS!n){D=a1LT- zy8NWpX5atBgBC|TPls4f2QA7h3cH?!M?Ox8-e&kA0-GU?O!vo)`t&UhE=jQ3^Ex&m%X@sTPT+i=5j2BEO3CAz$3>5XS4 zfd7h!6#uzw$MB+}%Qegux7&;lXu*@n4|FO4y}{0$#b^Um|~ zjA<~=>#Z23U(S!`Eo1PqlJxW{GTtrc)>D~_*-NC=O>1)CI-^t!qrKr8;51o9A><}0 zQ8?K0{NR*c0p#}0q;nw>ZVqKYX=r}bW_;DYh*Ea;(tFEVJi6+sz@XpY_Ia4(EU#{D z4!-XiH7dH2n{y)tu-uI0CFt+t2OizDvAP&IG@@fipS}DMazNvR>s(BHkaxQL@6o;w z|M4%;nCVw3C}mXe=Ki{XDMAPHO+Rh}zF4HYqe0Zt+Y#Y>4`B9%%1G)W?J&(Ia;oif zE(M@(RkbHp(3sMq{;yau7QcxMb6sj$t70GnDU&dvdHz~bmo>#q(@$*lD@G)r6{eWa zoJohpW(oO*F?v=6su|EGD{5D2nT}NC#A7G3oTM-WD-rQWqG$X)lfAVu zJp|#170EP$YP43KrG&C=5B(8S;3~uy(zsdB-=d%yjUWhIUqA9O*g5v{xjx90-kR0= z@$_525`*_LJ~ll+_f;?V+LGOaSYl4^jfL1t8_ye`#M3O}&MZ~asx8y=pvpAa!~VLF znglUo_Ky5WbkWeEKz7{~!!9p#cJXTrUD|15L7{c!i3>CV5)!MqHpEXwirBGmX zvY`ue0j!fnWeFxK=dNY+Xp^|hkY=P+l!a)flq)_t8(yChtmbLx9=~mvl4P$vuAQED zZjW;Naw*ZJefhx2;TnyK@|dui^k^R3L(xt%$!%Q#dq~>UKd4ryLmovvmu?Uei-UP@ z7>BEotvX(yve9^BhA&SEokYbr<~>BlEfUt$5p75#|Ah?}28nzirB0_52c}OMwl1&NMH}=c`QDbfKR6k5k#t_ur;B+; zu+Bw;0EU z9~Xc6uhaUTNp%4fP5>0}Imb&q6g{Xi$j;kJT4lhk{$Q;CD8`gpaZ?eVhwwy6#L6!MbLZ#aP-4?g1GT8$z5IoLXYA0i2dI zgRsS=?BoNI)g?(3aNbzLaf(z#?@NQjAHOu*dJcUPgzg{R3|S}wf68Wi@7J5f4grI; z1Mg6HED%;Mdk9~mj`U`4e><)R#Iql`e#@6%zC@F$9$z~r5UWHH zGvYSnE(I61M8hEBbxD~+c9NYWwCBn9@S2qxy`eoZ^>ebBsE$VonnvsjTgpd#?J7X0?zgS3-ORCAslAfKJ_m!7c^L*G8 zay&1~+(Z_+dRlynT`j0~aYIdNv1nZ7~je!wF0*2z2~NaxlJC(f}jak1DKY*DJaJnvO1k`a8cV=iaq^Wl z^3@`?W+SiRb0^+r@3XcAQPm!G7fo%#kFlumYUP^$3H=!uu-Y2vu5a%-X`KauLXgz++B)NPl7ntuo!xDqMhC0JJ&(!odfUjp6Zz{Tw;b7c?Y z$<8?8b|PRw17WoF~Ol+qYOJf5t~si_}h&0 z;stgavonj6U<5{BZcVqa9e;ciFBfG3OiaCykl-X%KCL-+B9&HU>5skP$+)0voL)~KrDr=(Rc z{gEMsSX}M8A;eux#vf*`?psQH=yWHk{y9P zvu|5>v{SD+2$?7ecb0J|^UJ+{L98!m0~4mrSq6r@Rpc*A72LtXt2|i`l++@=5pb@x z_UGCC_^u$YCl6LtWhMlb& zCXM&^0ms9I8^PPkKFL`lcG26Mqzj!gpS!nCBg6Uq>y=!|sY|H|bB0D+t6LCDFsc6{ zkje?W!J>t#K2dFzEs%`FI&7{~HI=0CReL(r(eM0F{)}TM!IC(s&cb6w9$b9WDm9Kx zIj1p~tm9EgX^OjRiM1&f(#s-)}BwFWV{JR4S*t(OvQWa7ghIPTuj%#2efQs;k4pA4iA$ z?sPgv5<4>tpYe>eXf1L-ouFU#^Nozo#JKlbxkgxW9GUPB&0nkZLx+4h-r_DpYenVi z>WAC2@u}xa_Z{u^S(ffB@dc+J6unhwsYS=P2vwZ!O*%;AAI3OAUQH|x{}>_$Hg2XO z{)O4kb|Eq;e^eJD?L=`%uxq5U&)Ts2B|go%zIzD!>+jU|qH?Tt>9LHzxoH!=H(1-d zx<>gzq~4io6Qk*;0hm=lUsm(0i;%Nx-i01S^}|eGFm!&x}TrXS&^m4I^}Ti_qXX6CQuDk*geL@(^=YdsInS7@6V|zG>5$tU3V}8f1Ci zzAZ4r7Lm73k(l(+FJ}w7ANS<0kFI=a?0mL3-hR>nM_E0zv_T{gv5}Ws7&neLw^XDi^vnaQtT$kOWA}vnOF+Ct^!dBv%#`Wmk8P1bN=|8_p@gHk&jHOCBS*GYd(p zDUF_UZimOe9O~pO7=w3r3Xq|-{iyk%QHHWnQFhH~Z=1Bt(XSP6*+%=&75Qt}fieyV zRWh~yd@vg2CiLeN>PkDrcg%ZcO)7Gp8jY;0nk}V$n-n=dowF<4$j#8e zgSbU&aCM8t#kQ%_c841@6s0&!e>hiMKu=y27t>MEbV8n@9y~5|RP3}tO;CA+^=?=Y zG^RP6tl!z#rxVR?-*9yD3AU6kYMa^7)ug<;b*41d;dcBc>vqI~papmo3W23pl@WCy zo?BLKB3#D#{+t=}!DZXUuSH**5*D6jf1^xaJ*`lAvd0<~w;5w|KrlLoq+& z=zpS%pGCLIJpTi3*t|iHzz)bQ7!_DMzi6CrM)@lRb^Ar)KeBAor&ojy(%4eZ7r#4WrNoYJ9xZJ=e4G{{S$!4ju8|_IeTjP(2cC_a@QA5|@?+-W?xeSNe`M{qLGd6yD+Cpz))S|pOqaX3k?@GSCkjZ; z)l9RjfPWO@w@m|rh~N8})87l-U49ay7Qg*fj~kpY-1KW4IJqd>eKH+kg_!l}3NZVC z?#UvN$yLUsspTfmDS!EELe_X-@2;xDA(+VnN5xb1aZJN5+9yQM?0Q zctDqxN~MAqATe3W=An!ep*Hl*$dtUnBKWwOy`Tp%bFJ&Q|C%T>?agu|uRe*%1su4v zEF?a?GOt|p-L2ZE9LmFW?rg)KMxXwj{cwSvxT4I|rh2w4wa7^(HoSYLc{YEELRDvK zE10Yg-^wKPS+E}5qLwY%X8Ba{_im!HTy`h@uuYR~LI%OIw%ZWZ9kF566tcuf#dcUP zJ*@eQd>D7+2sUeYLOO%qp?}fj){&rnP~tYw7GbriG`aJB&b(yr-4!ZE#l?qKu4o>h zG#$hxYK{YVeU2!aANpXewc*<$E2vBNWrDA{+db{%ej#A|sb_pOa@<>X$iAm%bkptY zP)mn*@msd=!$9o@EsI*}7SaZI3cgV_sYlhRwe;;*6Aw;L(dnn7j{C=Cj*ni3>(@HP zzM%i{5J&%yhnR}zngK2HC2^`bO}qc&vdMwiBdL$z+oD&)A|~$^=lMuC+d!Sta2yE` zmoh3EZoc{p)tpIS&Pe=7zdS{yWO1HUUnF^A9P)2HaVF9)8?65D2d16)!Y{VC?07_$ zVndqIFW&pBQHlRhLmrDv$x!18tm=t%{~^RIqKD(lE`U5fLrjG`s3e;^bnIvgXY&5z zLHbGF%Sm?XqtC?jqU&ET&am`rp6=s$SM=`5AqwrF_tqM}w13D~f96FMvCz&B%#52+ zPp^??tdht9FldF^&L=m!y)V{zvpwB_`C=eH|FHD98jMl@_bwb@=!ETkw-muxt86FU z7HUiWs)}*X79ib-T+ZDHf2I7mn&#fr5!=Zd9rn-Tjov zzQEcnNd>#rD*kGW+^CD6>X9U9?YCIe+cXe9yki+k48IfgB(vKyTmC#NXO#_wvLSMS z<_lM{{qFT#EY6hbvS1Yl6A8QGE}@(ic2d)$nPh!)L#&b-?lodaTcFhm#qNbC_L=CBiY6RgU*Vz(zS1NfX ztL-wReldZIEbK>2SMMV?0}oz~Tfn7PHXirT@y7s`Q@OLd)g!Lrp?GlzCNW-bnFZ51 z-;~sFt}TN?$2(l{?N-G59$X}{@V~mwVV+Stz;cpQsZGb~${#9N=fByW%ss7F<+Quw zPMP8Z?AL^->f1Di^p}U{F+0xIkdQZ9lB^6k(1ZASd-+|npg^UUqTP1!t%{~g9q+>Z z(0){|@Vns~2;3js&_szToP9Z>Zu{4C|A?{vZ%WWkmbEW|?QKmsvlRuJq?!_G!_F>E z+mGnwJH}^^{x_OxuyDi&=Ah4XtT}(`F370B5f zjkngZG*LSP&!q@9N@e~m3r?zj&e6^r^Ovw8na(V;wE6&h96__jXiweHCFUWHn~LC; z<$GV2n{2+0G#c9xzrkzm)Q0*i$H~-=?2djNPP4Qc$hFpSezw`NYX4%k437n5GUIUn zGSRqUMZx`B|NJY7EJhqcqeZmZ;CUan9=EE@HF-N9YznQsZ+<^BV=f%Sc?Vs%p%+ZY zMHbh)SC2?yS8C$XaYli)21d4^C_L^Wsvb|I%VLIQY#2c6%H+e$V7c*OHX`F!g`O%8 z7h89q!ldKh$LiiX3+0#sTi_Knb5llsU9s#s`rSi*noOP(q2Qd>+H%N1{zS7T%|YC8 zXyW$C#ypPp@n(z6+yW_(Pk?DRC^_rLd zu=p1X*TLq~?iaHk0UFh*p>1OTv<{gwF5o0PseQ|@e(W&P}ig7sq zIKL(v9dSu-?;|=R+p*tm5eeG#&gA++&T5|Ow(k>??`@Kw-nNk8kMJlNB#Yqma8aCB z=i5N{X2By`9G&Qk?%)l@@^SqW>S^RiSV&4PA`eHrt2og_p2JR!XW%K>@4@TwJP%(~ z0QDa)+l@uoe;@2<0&T0ji99C3JcNe(_4dxmI{p|Z)#dJA)7d4`H%bg`UgQ)uv(`Ah z*dL~1+L$y`)KovszNkj8;fv$(H+wh!T%G{YrDHU1gv)z{2MqWqr>nvY7|H=~t8M!8 zCZ_2hR!R)-znrnX(vX_AX{0xWscQ-WcOzcB&rwV@O_DLHFRT%Bt~8p+vI=0&>51m} zEQkCDTdyuM{sQ*!2;6QmY}d{M>jay5;E9jZ=Nx*q&sN4RG`O}`TBEHCbjaqY^O%M< zR7hM)ITQ2qV`vqj%fQ+*2s7I?PwpM7Uflj^NwSU$7&e!*ePg_pu)$ z*i&OowyM82_x?;lFK^Z&O1zMas*#iIMMb|ZpEE!91Gynyu(zUcI?*SkH7x22l(DgK zuF`@u39E2lKErL_qPLJfy+mKrbkq`Iz_Z4WyvPQ)1xO|Hj%`tU<-w)HllC4b%SEpA zLplPdj@e1vn2)E-CjIhc@G@6&naD~`G}IDI!lD9RVlot-`Z;`6x9Puy-RXblXU zqC*@lAc1FUw{uwLJF*~4tw5PsE=!tl-j=xAaPIC*iI&BcpvB`1gVobOjLE3!nYOr% zx`U1K@Ab1aBhe@B!p8+vH%bDBC$o8>*9Un{BFhcCu5(>8a$ouB)}6bUwU_?VhEST~ zeB~D|32$dzKl^`don=&;ZMUt9yA*dV(Bke+Da9%7uEC)=gkpsjYjJli!QG2H!7aGE zLvT*^k+IKyzxPkZNPcFl=egHC*Ie^TCv|>4PQD1Me5U9myB3jGo)^wGJR87 zEO%{WqsH-L9$>&Y@mZNUSDx28L#x$I1s#66P*E3#WA(iij2RpO;o3oN3F%bOGw@z8 zuRzu|@coF4xlmzbnI^^Iwft1{_7^0^j#9UbO} zn1t(MSr*wck81n1yi@aVWwYNp&aBwuV~iy1g^>z{a2#v7B7~W9(8C6xk6$2$SO`Sj zyBSOp#Xh=lSAC`%=I&H`!S<<;lNnQpqJdb@iZ-0vN;pGmXBrJ)&1|0xWUIQW>UJ6h z6{m6)V)t$m>ta1x<>%hh>mNDU9*y2Rx@txYEdIyU{ZxbZKgl^{bA%^4u@P zE^1D^%1evDF`TC$Ai#bX2?HgS=eFL~R*0Oey^HwJvkOe*GzVTt-`a!)SvVOWMY*SP zf&4bB1(hV3_HIL$XYbO-HYvxZMBmgTCKmx=B64w_uuQ<W{hlwY7^#@apbdCNJFZRsu5=!9q_~p)iKu~cbX;@<+6yFe zrM-E1D9w0XqFB3m)*Rzero0ADog#O<1?G5Of$ulZD$vmRV!Q4M_RKTMjqex)KdPr#!!D zS=%>u!nRbT6|o2!)1qRP&~dEHc`s!a+!vZ-U?;cnC2pRRX6~C!dV28E(E5}vnD(Q= z@Y|VjLKl8-gRyFcPQ)I#XV96B?--1_F`k}1d8r69BivUvz({FJ_W63%?o%%dPMu37 z)yd~K@7&*q&8Kmtah^n$*{8l@$JcfFEXN>$*@;10a8{V!-i(gCk^05)n9sZ1q-#&sRuQ661n`k$lmQ>>#W)!6p%a%26yC9$DGBqYX| zc?~)<;fOU$*(bn3sl&PwRRbLvwmHr!(GmEGZvnn;!91n;K{&#L&jP1T)qc0q(CCB=3zIV$IZ=uq*`nTcQe<8I)BWMf><(7wi_Wn0FC zW>?Dh;zR@w+d?%f`^X>Uo@5%=l5HMk6cVfQxk|bvMAa&rzq7=C9ma1N#_kL9$Orbt^78+$U0+GHt(`pr-KucDH4tE z+1O}H(7hYku%}IXK_f9B&o|q4GF_MBkEmgU(hX{mTm1yk#W=w1M?i}=; zB4OR4v2Aq{5pC*}O?Y9vcL*dQl(y+?C#$oHWyeXWw8E+I^1j$48!K+meaMs^(Pd1X zkKnIE?`SCKK7^N^5@MGVahZ%Y) z(zTiqljE(1ob0~63I-51QqJ6y`>=Qrah^EAjMg|cw`K9s$5JQ^KgmNf_w1zC-fk8W z3hy^1nkOaIQS`jsE1wYQva9f_6Bq2Dmms%w)ZV1@x?@$Ioc>L^YZE#VqL*p&=*y_pp;jB42Uv_|aGXCswd|&0sBoO`y(r~-Op`dVDw>0W$}!)t z=$W-ox9d+Y^cfWm-nSd!cXy9#(?Y8vVu=AQi5ND#lpBxl*RTvkyBQQ2ywBDwO(0Ql(&KE!Bn#9IS#$oF~-~1z(uV7?id_^gd+iEIIgIy z?(lxUMvZK<zNHzj)z7Cx{f);`f=%maRq?GQ@iG!GTEBPcIViMT$a4 z-$cp30XS`Yff5_LyF~8#1TF>T0)N=a(Ag@!CrTHG3X632UFzxMu~)KRZ1xeVG#EHE zGNHdUF*5p4u*}Q$J!^d*^w$!BhmV+#80C1Ny}_M%ezL=XeJz=$HZKWL6QW(2z4w;m zxkvAQt%2|O(Ke2MVo`Tw>kK`57_7%m6W**}eh45G+pD7HVv8UD9X(#dkC-y6Hd;p5 zMun?CZe!)bG1lrc-NJ4y3kL3=_xpxHB7@IXV!SUmG$<@rdme)SSR}|UW!YRu z^K!ncTMpEl*Csm}{&rzNz5WaAa=Vk=yEQ8$zCc+8YUCDJ^4WQk>9uA?-^V))JLMoK z(k3z<-=Ltkwcu~Pcq=?UWV(<=<3aAi^7!IP?1F<+B`z_&QV{LCfa$i92g{Ioo~og+ z{lPew5lNcfJdR$c=rNA`>={~Zrfr#pf0sw8-*s%txGy59`=AdpcGHrUBlXkx0Z8Iz z`IE@Mh@;+WFe5TaF?Md{?hdbnLV(E`|Zf5I-SryIDf;-*M0GR}hAX z1^i4u1FAkyy82FytPUGf-lp8vP_bb?s5I=+H~V2*q1I%FK9IMiz-Lk6X+ux9k~=Tv zFV1skI%cw6$K`)GlJzT=emYaV>;0!o8J0^4{Fg!=q-gFobSyh8R1Ou_v88Sg*?97} zw)og|uLeG8L3ZW)=C&m}uIkfOqog~PcQV;Vk^YHo3^5FiVR~CpFl9hhMdf_Gt0x&2 zp^(mye0HO&86o()fMKC@AlOj#jquj-^+)iA4U9ypJdN0dhRbo4XZxIXn7gR29qma=mGV9qh5*&~_Yx;TT~r`0~FhNw=4{F3qZHyRFfJ z27LR?#Exa2+YEfVtrw-u~B|Bm5kr7dIiDPa99Zo2d_IAJZh-2W zEMyl=mYT^OT~xJrdp3-UEEBrn7rvuJpo&w&7H}{n*{L6o5y{U_s38sqt#JC*2x%3a zyms+d$jkW40fv)t=_NfkdCa3qbCbjP+gQ1<%=A7GxOW9jiA91!H)QCrw?XIi{n$nA zAsU-I8mbS?jtoAzandXi9`r}U z4p#xxBnMD?XUmgwY6#iBWx|X>Kfj^^eLo{XwHC0|&ZLNE30|>bt&%?-^9hV3K9l%; z!6>=_DsZwnHt~;0GJVjC6rj%99FrsVzIr0aTy}1S22wvl3^E2&zzs;{Grgi`oJV4R zQD`xlnR-T_zeMsap2e6ph^15+kQuVUiRcwX&ad{SOWb&Xd`rPiN&4wC=g$KH2Cidv z5Kq)4jo}e8)D4pg%BQ>6oxlHR9PX+AZCOhAkB;^mjnck^w@X zC_Wr=#XWFeWs^P2|85D!qE9jFC^W@Q*kgNDRl)ahnd_DlC>igPHa0dQ^CNIzO(L4^ z?7j784;7YDb5r-$gPWf)jXPTGXRS<{|4i+$MYFYe<_J0PE^=)EJ{mtVyf-4y(n?+ICDR;Crs4NpQ=ToRwYz@#+!eH zVuTVCvVJ7W1ZpH_z4X)PhOK&CUo|u%PR^;rK?0b)T}8|w$PNY3(b5Hr>rTx{+M7ae zFB9wK6^_kUWA~RLWarjD-4+DyDLaH~EI*A0%TQTi)_ia3TAXl!A9Vk6bj!Mxl7!p{ z^Ie{G#5VF9i2CkPF8i~(lkzNOwOAd9Q2ffv0Q=M$Z<^2T+5X}fvHS8yd_VHnd7KN1 zWHOCCOMqv9Hpj)y4c~#!a^MNA@1jnOYG8%V;B@d0D`rQp8WTso8D`M=g~Q*6iJ^?L zJ_wOX|KDeTbl5{AZEaAMpC)HB?n9D{M#VV~CJ>vzAUS8fVQI3SF@!8hCSM)!Zu|?` z_N6ODZAMQi$4v#GFxP|QhS{ETuOI-AeNUgGZA@V<%`V;a8_=#!C4LqEyMqnVE*7ed zkf%}a(4Rhl?M}<5Js#IryAR0$7FUOz)@XI=A<-a{>9UJ756hP6+sGJiOs3Ux=&Z2SCwDLS?gDw)AS2HM=N z&n>^zY}U-uxW7B8{fakr(dBuT zUAH*x9O+8IrAl9|5SEkLU{A%-suZ_#k(xwjx zq=1*w#Z^mkkEl|f_P42IoEdSgm?y8l#Y@R5k_pQl!yffSEsfc7;AHNaoip8Uj=J;< zKLL~{oHVU{s5AWY;r_5MqJf$qtB4`(xsi}=_Lm~lU$#9}7#a08k_^ZbRbwrJ7!cp4 zT^!mTbZ_0|!f5X;ypa!qUYy($bzY8i{(LDHaJ_kkR=uvbE=c%LuLYi^b35zpd4?QR z>5zCYWaSw-TgCeM%gx!_B4ZNAzEuzBCBkg~+N8XoI6!H6vM2UnrsmGIw~Qr4d-k#2 zt*ZV@D)*kDndeJ~5;AS2rK*kI)e*7I)HpiybOpik%!93f2yMoMttRP`8fzdlQjz7v z5+S7Ckt$LhgA|>S{Mc}GbJV0qk^ZaTsfZVQik3H>q+JG2oucA-fT zcUWqMZp(WX%XWzDhROiWHxid^(jV^*OkP-;UibNwYJALW9RgUg5wv) z1rw3r4#zz6X|6vTI74W)T~b_*d6lhyCS~(lCjq#fP(%2CzT4X9L1}7k?}|K9Z0_jd zyuNhLm#l5}&Fu$BmpTXx>7pj!@fL2Rn(|_`hU(c#S*Krdle(1Gxs=dt<|Ns+C%ri^ z|H>d?zaw0sIL{Z6+s8}A>2}=rfI4fsQbJ1RwQJLno2n1D#tSjr*>j>I1Tk{1cm@<& z+VvV?qB^6&siGS-^4<}RW2Kn?RVkW}Mw)*Nj?FTLV@ZVwRgvapsOpNr06L-gPQ?hm zdMsqA`NukqL*CFsB;a0b#}AX1J95FmlQO}_ZsK1%3pLB@$0D6f0VCPE18Av=5In$H zwYXS(yPeD9bFufsiB~l&e-CR&Zycub09m$l6;04a*v%Lor8zT>W}xNAuXOhDXOdUC zQYLhrPnkFMh=eiuFl@n;kMf$)f9Wh$#)`^_O6vC2nG+!7cY2Z9bLPLE8Fkw%7X|b) zU?@nL(UW(f4hBY!)64jCKdEZDcF-kRKqhNPsrAv`qVEH+`veV<{6gvp4~1Hyn~kX* zFa^h(OIVDcSGHGkX=v8JumRN0fLG*m0f2Rp}_swZI#hJMV+fund|9;Uv9|qh$5l2DM+S3&A z#klGdC~x7{Rg(FdoKZ=|9-v2?-c#^2_@Buc7f;^*vS%8OiB%UGmTqq9s!7l8IPq4w zv#*aly9IIOwe^Q@7FAxsF$|O&a3a$+8!ND<^E& z;UU3oyz=Q{WawnIU^UEMtrBjXFzy^q{td+}8G4@B={Y4vkGKZ%W*~zjzcR~}NXf|} zd!3LA1+RfSgI#@6ASW2Ve@Jq_f!tD=jE^Bchb=l4=S}cLxueo9!~bd} z{aL`x_tPRVFJceZ!76Dl<*4cMCbrVtv*U4l1?T$F0a4;iy?tS2Q03D-Wwp}tdGLqan_QP(`cXMb$(3&UJ(Yp)r&A{A+ zy;JURU=@$nSj}CNd{O>yCzNJuEjkD?k+!s3g z`GL^*hIPx?^{qIu*aKKA$*v4uvkm)?f4N2RaCV*d>zOGc}GN zA0q-7@e&U|CZ;9HVt8TXh?7aQ`S%$r=G|iFwW9aQO{1l%rN{OQiPfrOlT6bEo>}GN z_CH^!q7t9}r4699*j!RS>60BB#>kzW7z9TRIJbpB*{RH5?7?o_WCwvH4_CEdG|uJRvm&3VdsDoRTDyQZB%`6`sL9J zyaHRt9!}~}FqOEt8dM(*#zsHf>QNj$;$B(DC+DyG_)j#6Q2*Sg-7U+>M+4+xl4q@yCyjDWuim`msaI z*v3BA$*Yk9e5v-+?PrD3T<2)^9VR(g}+i6Y*md0*T__oZf7zhbiy!?;V zRQ8ywMn!Z-1T&!a!KJpl{nR$<^Y!|~$ z)#KVjIjZ&G`w#B?>cz`^n$4$Lyf_7oY4TIjg%*% zEj4?vr8Oa1rH+JuXb#@~uppj#y(A<# z{G=KZu0DrmT8ZJGjC@&IU$){vS)}JB+)e~zUlK3GbNIvc+!KKtvBaO`1W25->1r4s zU|e}Ni_Euv#yl2*t%DT82Ls1TmKMZeNm%N;{%F0IBtNXE_W1YuZmsn{8H#s)>|9!Z znv?-G7XPx#H(u3%+>@>zOf{=*G65;`kWKwGh8@c&qbpLJZp`> zZ$4A`j$D~t=|aokBoERiGze?~dZgy&RbC2E#|Mn_zCWPA!Iedi{ehYt?*P_<==)%VslY4yen;kri`xc4zA^fNC_fHk7*6P|Jm)vEp z4ff<9ZDbLiw(tGhb`;eR7z2z!H^3Db6D#qe3%46z%LCKzyvR~*Nb!S?b-Q#NQLNLo zubk#tynPp_36^#j76(pFRA^@_sH7i=4V1zlJIt6?=;DEX6)@-U#dgo+s0EEW2*TS zpgzqRG+j`|elim3L0^$gT@~k=QO-}27Fz#R*acgod-^Ou2dk4;@@i?qGiBO%&MM3* z$pf>zjYF7vsvRE+N_MyCl47g|o^Vm|NkrOtMMEo1-ZUob4!!VCnPg#e*MF-3aSnIX zu{o}IJPr4d@n&DQ&*%|bwZ|RiTJTQauv)y6q;4e&U!ek9ojgpl%|XY%piYitXxB*a zVux2v3){mMJn`^ti+v1pP5s)h)5*V%0ySsYu4q3heM`QfU{*^fs9@9g;#Gh>z7@uV zr^FPTa~58=iVKgA0e!T_*!%GF7(&yXqiPY}1RUrqUy` zs76es_JfaQ`(ks00b*PFw@(LB;%g4)_$H4&87qDz>+WCd1srFh{hyC#j~zQszCGA} zCybv4|4(56=C2)i|1bVp>6-53=qJ?>t5lacrC-!3Tj)Z!-j7TjG0@jj@}}o?yfNSC z1vZk3ifT&ax)TMn3H4j=hhscDd0RWtEIjP3h>8=g>-O#*UM!R_Bc8c{@aI$QjajpO zsT?;iT>_x7Bfo8d37Hp~`06RXdfM;l;L)JBSxMs3_4hT|5OO=kekrww-G@(bfbMu+ zA$&4NZcB=(J)pCDQL4UpYmz&fTy|Dw&XGQ-m1;zA=#KdMQrt7Tfe)nZ`*K<}a07FN zrwefegO5XS`GWw`i#qf3=s&(Q4)L|a_s)lhRGdvnjV*<9Dj4L$=WU6aH>cK*5evjq z7`S+%8&1X;2Ez{r%$qS|IBEQy?ok<+Ul`ddE(@OK{NOK~gnVK7ORv(UnVPE~K}BWm z?h-<|+G%`l0AtDwP(O13$(;jESJ6PTB)M>T3hFNoPMw8 zSZ!AT4CCIhx+_1{iJ}*%4}!tLSls9z zJL2k%EGhJaGZf4Atfpy3s0Pyv$M>@djrw%^&S zR1w#xOlg~*C+Z||UBtH*;&N(X6lcIo>Goxc^ z2bP_4#Rvu6T&IEpwQ49sO72Ge_7l%Oq!q<}ZruZWNu!?s!>ItI_&2p-2Q@y`jIZ0F0uk3|8)h~C z{T<(Ml~HM$Z%M7{5QfKs)8WXnP>{zOj(~&IYSg1~T#B@SaGA%3)mYIr$oaRsW1 z2Lk`n9JR$)k4;-1G1SI3@&SU3HB@94Gj+MtB|Cz9@db-6{BPtr>suJJXWy z4`IgqXymC?!LA@u0cae+_ZI)K@IzYJ*AcPyOqUkN>c!idWHbzWtrv@c!oR{l1cicT zHQ2qH*jpXZNClWT=K|+s!cQg^ywPq)j7xmMxOrI7(i7n;Yz=$R^Zn^;xtDXOtNQK!aMb52 z9|c_^vJ3;!&a@pU>HMSYSTnBzG^DGqMDpFxcpRSjFL8p;Vco@b3wZBixM;wHzU(^; zp%iG7{Kyr<%^Lcg#%D9zBlbR&8{1a9Hu2K&KX7Z)@`#5$eiO!x*^;MK)V47Zh?EBD zcMYo%e6tD!)FBhZoOlBv=8TqX?hG_aFnAKZx8fB~Z1Uw4a|V3SQq&I37R6%V95LkV zQKif1)}_94GBH-cP5{0A)X8(dRcbtS(gWkb;NcsVdW?e%_Rz0vG1V03+a{`Sk!&tC zc9UO2)od`Ub8rgX%D(?H&fvSdoh1amdI5<|UK5>hWg73_CsAF<=iMZ(*&et7 znC(2r*cUKB8jR&%)1H&27XL;L{O4#FoE`fwh54S=KPn2DC-^C26Wx6ut5;LcIb>e^ z5&!Ov`ZeG<%O8V@gljCaJ6Z^=o1G!OYTik3JP-`DC)!L8*{G+bh;Cc_W^qr73S*~f zYyZ{T*aE3>OAY3rFL~Q!jrmOFHgN6RApS&az*}`#`j(Vr6snRErX{^BSL|U<>+E_6i?(Zt!Z7Q3~{d{icsTK#=b9T$2q^5ZRdk*NwV-cT6E#o`xzJ;Um*!h ztY)}g7@{8Xa?|;AtZ0I@pq`J#wjQ)o0Yx4gMQ73Z?aut2g*dDp%X==YgrH75tGYoakh&`pC&p2yFkga_^jyuf!nO`A*yu;gmF`&B@3 zqhX;!ckBPDik<$8Dh3;lG0G3<#&!Qy+@FD0Ca68pk!= z2XKZ5U${sf$33y;1m3&oR!Nqchbh=)7uq#`Syulo&j)F6&AHQc91HcZz~9l045O)i z(tPCc4vc7ed1R!1(%()0K&y2~@in`ild zIN=8~{!qX;A3ab=ZwGbZGWtUyo#C;ouWy|F58W3|K7}p2*FSdv0$)C&X5w3l9ao>C z^yc<7K`i({t}o5tGE(766#w%R=gk%9>*rmFh5IWPI@Wv-`jYv^pDMwfbPc#Q#~{d48Tl5La{=hz>a+@@o)?SrDk>^lCV* z@%{bXbsUx&O*TPBV<=Yak8=HrVL4RD6lN)sPO6AvTWw?)o-6QHX`6>_e1Cs zpd34d-Lr7O4NYGB>j_0Rr4YyXi5*IFs>xwNT@=coo$t@Gjwp8AGQti|=!`m7w{L^_ zTiw6B=}g#KF~z;E#v5iacPam6ks6AwdQ_crgwy6$;Q{i?*bxu(720GRY8T7cQj~yV zEUfx{3mFYmi}#xI8~OYEY3+Z+)Cc|V7$}N=6T(nQ<#;w3ZzNT#nG}9jq}cfgN9~)u zicWIBz6SpytVa%q`#g)J2;*kraDcdl*pjKrRw1U{{8vPfLXVw4oZD-ON}WT*0e04P0rkwKSy&G{hkR90f$iwo)( zDs1Y&i2Ed>-7j;|78C?-(KYi-GQFP|-8xn_FWUXMRquH?=6Q#GhtLGV?f)y-*fGps zV)OEWh$H5IH-J;BEKE-qj9c~dG zh$W@FlN~&@(R+puI}ZF-wqq&>480j@Cu0wvrMb%7+uPlQnKMIHL#k7ZR+jvJ3{gWi z5s(4C0wzs*d5i5|6K;lHvTyOZsYKloxiYOSe{Z`CNZeyR>1;`H_oBkS z9dPD2>X1Bymi3h2-l*%lO~EGojMob9(|0_%NSZ8_hH>#_PYAzd>>RranC}aR6-4KO zvKHU?!?>w)`n3WmVM&TeS+VXzf2y#iZoNMPR8L!MsfX@tNKNMRjj2zB@z)P;0A7rB zhD0I4JF;(l+kaZe&Aqh>t4#0jkGBoMCUs1n2-6-Hpm#4pwpA|~v+S`Tw`{mFn`%G} zoZ=OoW?Y^Ei00b~_2+onm+=~-3b>=%<3&zT^(fEnCD{X2&Ziu?9_9wU5WtAK6y=3HFmo1*)3Eksa?4_2^l>BZ2prl+1VJRz~+Ul4{uv zGr@CT<}PRbSjdc?4ykJ%83Al~{>C_e7x~RwPh1Hw@9aB{*W-fMVidhvvoISDx)+_moyyHgNOrD)aVOXgICt~QSK;Arw<660 z*xeZE?{nap&p&QS5*`0M*!Ko_C2Zk5a38W&ZHut}xG^l)t><=SAeCAoWXN>&cy)ZM zonM0!Z|nPool=Ze)*$Dg)3iFKi)8@PDW^zg(FmCVah5&{GTRD`A#&$;rvpliX2i-S z-a`{OZH6h6&;SzrF}!ORsa%@-%?UcTwjr5VbG)2)s9e-i)^zDhL{hrl1)(hUoe?XJ z{PFIi97iK{1L*uL-;`xs=&$7KJ`uB`#F+3+)L8D~Y4`Jy_?M4S2u2(*G*RN6E~uxu zg+z}^ssVzHt zTU?B#kRUW7L=2%?nWjvFkgmNjbw)F-y|PoazqE||4DDe1a3mhF%TZQQH8kVxj?WH= zG4aq915S5*T^W{6gvR-q1pTfYdE_`Efff9Y@I5s#hS@$|A^sje%Ffy&ZPxCooK^#< zTX9wgE;o@b$N0#EQzD-Zpw_V(sXg*kzlz>A)?f>Ov)9}2i6A%VVnKVkeKYuX(J2Lt z9`?3or+M9LTbAyc4MLiaT!FtYzJ_MCssy@a%d-{N<ePFwDz4 zK=!i(`bmk{yB(T(yyh}jUVmvU44w0qVR=?eSp4>6#pKQ5mK5bHA+)Ntm#iLXYLS%u zn&J_-GYzBt?zWh{-=#)EqwU(}01C*2ECfbf{xQFa_OFL65YdB<0zEq<2*Cf5%XzlGVJP|YX z=;f|zd%$w;UQK?1Pb()2{}5Zju{!@^dtFFKzTI=MWsg~|3hoRwbXN7oC*lcQTz(7`&Ope5|x9CvAOUGiXN5nTXh3eRE=wtOz zER<(SV_JXBo!i~NA$U%m5|IkPI_7<|v}-=MGPw0g9V? z37I9lv}(_B6H;tL1@WpF3OZ8Ibt^bzCIebqglG~Gq&uaE$qXG}S|6^<{k2TnBdjS@ zlz6V4vMD~IBFG%eHe9@%!uCg;iQad@>IPBx=8F&&R$5YkmeQ1Y440(fAft_-um`_p z-P~3vE)Fc#iXG?~yK{+m-xJFz?h`EWkh#`13+;$>xuG~XK;b@PQg@c03+Tc{dt`6& z_Y5;);%n9~z42+coA6*UG*R4%M)UcAj@-y^$YT@kCcpqc#OowbQ!P9r`o}Ewrg$Qt z;!(aNIsT6tNAtceCb~9FbC#Sn6!lVI5@_rY9EGDh(&9Y-HXWa=Oa!DeRlF==mAbvG zx1@8q7Bwcc2KORon-S6(l8C^33|_^|l7O!j6lYZ%-J=|43fe77s?9Xd0saXH!#x9y zGkdUSzdzcVMzsE3(c76;e$xCyMDhXk3GC*QAh8mA+3u2#_j3Mh@Wj%2@4N+j5k)|5vSZzqP>N zPN>%_&(l-oi${i-_Xy3R=E~mSUt*|`y|HLJ6u+A6B4xef4ort+6twd21?Z z%7vN`jL0qT=3Q(f+_P$5;<09b5-@%hOo0>;`mS;Vn>hK+;sH9@^HxW)CS4`K5-U97SnQX7l>tD<5Z2;mcI*H%l~O|G~Owwk7-X<~_2j!Bnb6Vh-OyLlE_MX2Ntj z;GCLa;wwP>nm}1}>`m1qL34}#0JpV9A^6J(C6by&zZwM3=ZMT;`&bZtj&EaOPG{jj zvG4-Vge5?e!vil*`I#jJ!gu%;^2l!|;3$z}pUyo#XC64YxwMEp?{wRAbSX~s=;zV_ zuD_0G4$MYr8=7}&>l&(&qlUMh+quFk7c;GHEDli&k<;BfW8YM7?8-NJ5RJ+9LHA2O z{WR!q2kLS{J=0xrRcV~puVNta!Sfmk(8XMRk1t7AoefRP1Z?vC%8Ch5YGa6>`fRmvtS^=V7lJ1!BXM==h?^tCJCZ-N8< zZ0Aeg`Zz{M$3>kQ)x!6s;OCq3=UIDhffULGC1wYnY1GTz>&!6a6}76c90-*Y`c!m(&SaCcNNyglWSZP<73{^;=k z>IhakS;T7J82`V@dhE5mbRf)84l$)@X$n^CC_vF6)q_3Rs0)7UvQ1E{%C3upe3zmD zfDeX|#idR7&3*N7`uX4GQL`l>3WdBH)XX{EF;H~1Aw9gUrqpbYUo0)6^0zsH{k%{j zE)8{uSUl-p?_Gf)X!LiEFq=j$U2Y5TakO9q|rAMR`b*NLc-{Zzt6zML%J$DiQ4fN`|WNZf^aZ+E22uyP?VLE#E9IC z-cbr;ewb(8ZBPA*Gi*Gy)$B>PqgWntEW098U17$2>A5;D1w=Hm^0iUdY722mt-te@JC_#D%Hb8@UyI0`v1a@6 z*%=B$M=B7Z>;`by@8)hdB`QG z681-b3MU_da{`8%ykvI%&aVA5-)L{3?5{d)J9KhfB&zZK|1T9LV)TjsuAET+>={m7 zLdty9xVY;Ke;}4Du+FjLuhh(^xA8WSicM!5I_sseuNI(9eSFvQEXewk;Gaw(NcNT~ zSC3vcq}s@dY16X33%9N4;x_%MLoa`x*CZB5qPdI3Tof}j+N$m%f27P@)!*sDu9EK& ztpF?9Y8=#o2|uR^7X3zvDHtsb906 ziTL?`w~u`i(jHLV3iA0zsq*%5I!!wRQXf~BLCk(8mf8x6W)zs0R>X4OnW&dlPkt6b zKNg?=($7A$L-#tM`_4~(hDc91?IgqEYucS7-@S+}SuKCwW3|%4A^)ErJ023D`W(MI z%Q!$s@1z@6>DMC7O%R{ddTlMP^mhUZ9|dP@FPQI+PU~-c*tF(s`1dDz&$(YUE7+T{PQFNdKszk~+bb*qh2c0rN{E zlrt^26C$34@y#He4r~+AHjJJ{Ja+*ZGT%`Kq=y=rY5utIbOY4=k4R$@?II>Px|vy9 z{d&Dq2R z4Ee#_FTOf=xJv7(UKX8?QTbrPLb^_&Onj%AArWN3e9+$Tv&llZVwt6f0#awqf*HjAF~RKGbN|TN2@OFVl01t0Sy`t1 z*R8&L(k%S>SsUNS=Hy#D>@Y*W+opb|eb^!0twnI?FK||ZTbkBih|C{(WKi~FT1ay^ z>F2#rPtMCDmS31@`6=WyplO!qU2dt@Gn6CCiY-ef7jIJL4(hz{xK{PYl3OtSZDK7r zKcrwFbFB451lR29bIGs$Mci(wad0wCrTk+2)4*s4l+@ss1?TfmAJ6>l))$P489Om8P zsudA#V;*swB2fW4HN{;I^=du5spq~KCuO$A7S#YH;&;z=`P*fh;?LY4|M;~XU#Tg1 zB#zVv38J6xgp_tSeO~aH(cSG-xm;~+zTN*{T>XVpoBi|tfj%u1N(BnV^=S&Uq-b$( ziv}7f1b4R(JUF2(?(Wv&?yjXsaEIW*gIkc|hjY%%@6+e|7u+-V?Cf6q+H3dCzTii6 z9jD(h8dr3Ij`|vTmlpWyz`Um72t!QTmceET)3!#l$!ZkRsL_22m zdm@H$WpHpvhm=jl;Q8LUKuh}J-fG}Am}!#~O#E5q;(Z+(eWAK-1Fa8BP|GXwof#ew zlNu=4c~XAzp!IRSuwkE6E1l@J`@H&GlagD_fYQQt{X6pgz>(uY(j>mq9Ic!dlB;*N zL-AL2pav!aR2WO6@pNgy`-%zP*p8{)$D>)P+oAL>Ubh>;15HZ36Yc%=je+LJHz7aU zCvUuTD`K!r8x2iTt@cy!&9ma*#xzzvWBb}Z6svl2z2~WNK-H?RE#ijj`pWxA@o+!l z*1xY->oZhUkHP;zRX5qI+WSn|J@wsPTT*jX{$V{cj&W=bk}B>rEI+x#{_U+jaLkKd zy^Xj^jd4R{_{9h=(fv5CXs7;YUGb;a@w@sq;Q(<_>AC&&8Y`*w!5WyuAk)nZ>K2c# zC>s><-M#oGZ?rh!)TutwT4Z)XNpf9f%_J(?n65hicbL5Cf{zOG6#U&&% z>3&bz;ez09S?p*{s%&aJy8Y610|UB&znQgF?!V+12!9VDn) zl{;~ZER-FoAV&1B&N8|}(3j5HHsLwq{d?%wyFAX7Us&5|VZWaRz6{s1DUxMGq`mR!r44l~=^rHi9ueb8-5zbGGOn=8c zvbzXYZ5I#-P|M7KeArD8*nP&1_723JQrSEw-kG;zsNK)%E9CW5*YjYTU=C9~W@TU6 zr|-!kkT%_lX0;BE7m*f!1A*AM>~m$MS)W!tPGxbKzk2_KfKEZ!_-&n<8;>E0H?q^r zSkAp#c0Si12dY)MFjN3@W|X$J&G_#BVAp2vNTV7Ld^_d`(3ig&^Bcw{uhWoU^eaQ$ zUQRV+@kMm3C-Z`P_r|yL2cKrU2ML(tLCfN8Zyr5+!Y1Lvs{@G?4d7&%2ghH|zPspfAZ)g#`^?wYY3p zfx>)285LCBsM;m{Tl8h|r^dc=xK%|p*N_Wo1xI&w-qUaW1#6B8Ch%BqmdXQvBkQ%9`Fun3+b&%hC*6JL5QQbXzE@ZB@4(JJeLZkFu*0yc~?YyD85( ztj@UO@3^Cp{eS7^Q;#Au=?{YcMa{*7x8GOupGBa9Wi6#HdB|HBMuJR*B{j^Cexq86 z^{;(|xl%=f{iZvZ$yo&S+q8%{!9GJ;&u)agYwJDNN%Sr=eXFWFOSmH@zd&pT6|5Y9 zMwTCl*?Gg%*A+lXk8)!T-&N*9Uez$3Qj@v(bbgAU)V>~0c7pPB)l%nV^|MCX=7XB~ zVCxvzYboZEhWc6ZLW3P+K_IV#s)ZEj53j0STL3#IeBF|WpyMG0IZRNMf-oKa!x|m?0qf`T!uqP9S4MXSh znRSy+`*(-<-oAI+N46{B`1;vgv(W-<+?F~J#>s;hKi*Z8F6`5e>2v1I%QnnbGRFyVdVD)d++;m9a`T>`?t2 z4E1T+HoZLWgd52LGAb|mOUaeQkY97IrPloi4+Z1Ago2Jt^*=lEG>us$?{_5fqz7}B zdi|JEK7&Ift;K6qk@4LX;p_ZO%3ew8`3|=lF-L!@fzESZ#ZZe%TFN+$ z`9**jgCa4}f(M1WW3a8bS1{JJY|MnZ#27r!7F69~pz1*X(mkw($&ycsOiC3v)oIA> z7VCg`VMu8yS4E~mJ=vp^m(WdFX*!YZQyqi=*u9wdY~wJ!6Sz4jaZf-YW>1=7D+R@ zhN)$7>&B~NA~*=;U%ooikoq1kYLpE8fH~?>uCM=nIIV8bdTKhrU$?}FM}rmlCUWLE zq(bG!&(%=aI%nUjZ_hv4R2-1|$ymmTAtqg>y^eZFN)K7>pWEfPSi6vGxojjyA!1Ku zzF4CTawGk*&j~ye?%m45>SUyZWH&NE zy~^Y66LDkYysqMW?Go@t81ytkK>u9h0`HBNSgrThy4E|K`m6Q;hXrigqXk+o#0T^H z%`S9oqlJ!;l*M1-qX+xn=I45(9B3*eEaMwsdH&*xcf$kU^{5E_6`FD7b#nja#Phl9 zn8umrZ;sq?u$JI(0&b{XHGjvXFySbiZeK9f7~a}xR?}|Q;!i=WR!Yk7UY`SCJ10aY zB1J;JrIkIx$&4(t)*{IZ8?~??OEGo;Mp=z;*E(B_NiG~u_WWDqD*!kb5NW1HcyRRKsWQE1?EfvY`?mvc(!QFwr;<26^_9fCst~#}8KUS4~aLB9p zITJ4#gDDi7j_s27qr%?Wl>X@in0l0WQek4%#DFMxTXua+BCBzJ?@%3u!unpd?ha^- zp5w3*Z5*VA*V+-Om}RZMl1>bseKK#nQVr7HLXcK73C`IXBaKUzOMUe_Dr*qkMwd(v zW>)8tD9P6H^hF}k@Xb?wyi`of@dPO1O~f~TuRm#jX3o6@^yk(tcqor07M$2tMxbDs z<&^&i<^I1gW%$1^B|vg4R^urF8Q*C8tgnTXMQHub&xDoj1)`-O4zY0glAdAdYcTkG zO|5D#7vY1DA%hJPYmJ{F^aU)j*6sG*0KF~V5FtWc5JPL4P-W96{v)?0xBL5w=#?6X*dL| zySoJi(ijD;#Z^3tQip*O;)w@@`Fl%Z-iU;a#`3JdPRfV4)}4J~b{7K8N^*gGZhapi z5wCUVtLBE`i)duYxN7Sg175~w(fjior@ik+s@*(G+b_n994&<3+M+c9K`w4K(tc>3 z)Sq`7>g9+_XuXj-&^FJ2Gm-ois(X(SVXNH$=Irh=u&RnNU{iLSjtVo5i=Si)Pc&KK zI-6~XK3q6gGF@Ltch1=K(W`MHYPnC;uCZ*Sl!EQ-izjE<9rv%^ET6@d9$&U^GeF>$8cf49fhG>L^&|p3Rrp${L(+IViKRd zVc25~3>u9#V#6>oX>H8ZXC{Q-R@DODL(}ge2G3rqQWD%k(k#wtr?c^BJfpecSm)1P9ln>fH{j*x;ry)3{-mAo(`!U|W z2{$K-S-nV8i&u4q$3oEG&axEOizLZu&fAAAaw85gGAWpudUli^Ndy*G4P|%TV{WLf zm<7d~9{fi$yQ;d|Iv%=RBDY~#Z7bv4h$aiz+mxS+uBLUmGjIP-p6d&L$~>i8OA>Do~_Z9p<4fFy!oU~bKVJIqnUrf6Qr3(;cv`8km=%iv|`Qd72c z3c!UX5#&6jvROe-Io|IgZ{}2nOmpw5C@y$=@KaiTE|&$id8&k8G#rs_j5*lx90ZA7 zuMK`(ZNHCjucwQL`x!Dl(7AIxl)J5W)Q!X901o~|r`l(8LSGUwU4~U_qe$ZYt=Z73v{emD4d?xA-wHpw_@fmz zA2?+7lUR%GsZOb?R>+6FS2^)qJ*kWp7v1kyEVzCots|7b&PeWJYLnf5ImFo^RZ;Azy2!MaDCr}v$%(xK-LK(p zL{3;iKA+t%5tLJSOc?_2XiFTKe%gp{5)ow11{yFra4%?kX}GYH9yp0RMJ(&!%$b}o zD%Y}l*`M*2q%Jq~peMB=oBGXyxSO}HM$R(OQ(pUm$;eByW6Xcj{K2o%z>=UoscwFz z5v{qxpUaI0a=#sKw2-Q~U&hLJOcSrY8ZyrWhd^WfrW{rla8{Sz^yjL|{Ixx^VvK23 zyTr!GYRs6-yfbGF&32U`)XFKaf>}fYGG=;!RO;a?m=H$41r3>}d{sp)%>q%j_K(W0 z8qSx-M2QT>F__-k*^k=ZqaP7ezd}v3gs)Qny)n;hT&HFFTZ6QEzy0tR5IOdObZ!V} z)*BBMvpqB+b6=oj*hvjeQ&^L)`-Q{d#@2fFJz)ikY7O&=JlJj7EOB^Xy=iFud;Y=R zt=PuGGoOfy7NsEmAzHH#!bOii?a)uf=b~qXL|o5Wa+&y}(~BLTQGu8XcjA2>RiTV? z!1g6$tDP9?N8BGH;*>NKe170)aKg4~B3ngUF0({Y{hRwZn*@U$`JSh;ZBOZn8gN%94i7nSJm{Z4+hASk(?jgJOz$u@ z2(FP0z~IXG5k6iCWe4TGr@AF5Ei_-m)$s$nRq0<0-Wye}*GIO?d1X4iA)BfLHcny} z`(jpdN3kl;bz*zl%;g5kCx%l9VLC?Gfh+;9HExRp#WlAO&$)H6&QPL6nxr%h`}*zQStP8j0@;uxk6Ka^tp`Y*b@Z8zG^fV0(_**m2aQ{p!^T|Tu7hfX z3#XFfJdu0fbi{1bL6PS{(c&ey>)aKB zo?U!L&+jf;o({{cr(7D-HxYEk{l9kgmG6I*3jWW{RA)IU_Cm9$AA|ZYZmCrBof6UK zkb3N-ZP#o4Yuamf9Ly(BI;Lyc7H7;m8 z4tJw_!vh9VP~g(1v1%^uMQPu(FX^?alccWK=&`8=J2%il)(v0!JQ)Lz$NOq0m83#? zCoIs>*YiYxxha!{YBJ-Se+x!@7GE1%H&U7HiRrL^;937fmdO=d^d3zZ_GD1c&mL8K z(W+o-)sWjxD|1oRGbhpZEur=J$fh;GM>8O1P%Fzv$~vrp+z0m)#wAJJg?5vRkGbUuL1M0sm$TixXX+Zywp_EE#b9>0Y}mBGrA$iJ9Y90iek5_ zo-62EF?vMrQ!e2~Uxl$6$2lmdt+pz}xooot63x|GM)#(m@BPni8&2(o&QtN5*U!+m zB!mME_1K05D)%6SGtSwyvO_-C+UaLNv~jFsiA^U@P#frx8Cai?$xD;49}zoDPVqA0 zG#t1tP38|xuIZ*P>J|^<-?$Nu|5f-*zm($R^sY{VGuS*v#>#eBrEFM{?jRP>^85pe zbjKbid~0MCK`N=Bm~5jvHWPMYEEmt~Ii~-CGG#>HI?`kpc#_a=Gd_y+bt3EO&o;=L ziM3CjRk(QcAkQmAuWjgR$3(V%pRLvY-S349faj9B_fr>%ChTQBDUXXL zJ1m(h6Kzlfx}VUkT6p`%J?`pWUOHBF>Y%>ijFm)RBW>MJ#4H+%Q}zlZ>6(Yn&w+kI zAWfUO=*r@tHYK!aBA>4a+tWQ1;MNG*aqs!43sp@EwM z3JHepc+z?z?iGeUTT6#M(I2yKbArN_cAl zG^rGol*a5J0jZ%&wbx=GP^OOwLp6NG$orJetZ|VMP_q}5?>W3%3C`fa`MzMlDN$Mj zX%N_ebGEMz-<~{mU3;2=l-IklIzja8tdGE-KD&uLsGDmH`_9oSo(Zhb-RJ)SdZXcW z0pUc^cM+?}j@eqsKd)g3dA05)8D+uWfcR)SUv2z0Z>xn_?7*KB)%KapL+Uuf^rzhs z)mtcCp4s?Be8t>wBQtyjvVHO>uC8z3*lNWkq9 zC^(V1&GSc)iVrfxW4TIFZ0ZrprsZ@~DbLR~5)N0?oALDTi0-_Lhi!iG)IJ`O81YC} ztF(%F((aRe$6Pj+Pej}z`2Yi@!7R;E5zdn9&KVxIRAoeZ5GAguWM@2b)M@o81Wqm%+0tt|!*%`6Y zsvf|vC~~MNA$)uCUJSzQ`Spa*8I_n z!nK>@rR2ldDc%T9MR-0*_{}!3yd2ZB=4M^{ZQNdWJnK%WedxBXjLKr{=(e$?y1Y`ie8e$! zY##DAp3v|y z{3*y4N2%;e9$hrxN}2x^FbIMFBTUTt0=i6m_p=9Vh(a@~l^XcvgsQ?^mnLn0PhRN< z<_+6E?7$0?H##enr238=jB#WwMN3u_ay{ay?HN^zj^d@OmCcCe@q)q|>11yAt%?1m zJ`n;8ashx8cfH8Y)pavS*Yta3^J2XWW#XSP;5t*CkQ3^nL{^p&rI(4YuxB=NR66_f z;Xy5cF#oS$v?5Du%<~kvCpCdJV+w)v3)bO?!?%6qqt)gNMPIi)$PzHR0eo%Ao_3UY zd+5M_+4rQik<)9`YZbDYRp;x}+LAhM2?wv%5>C)-&JNmMx2^lH&D@4ovZL;;zSDNYT5^g28hv*w&k;}NUCAh6O#ifX1L55Bq%dBM+Meqq!g*p99 z;S*RwffpPpnZhBo>cMuLqn9%*lY(*ml#QwW$=MvuRVI0 z3Z9^GtVpVz4dmXR)t9Q?uZ*9upnB+mdiJT&KV^iqQr#EmDh9^G-yeq?==ej<#A*@8 zXU4Ppr5cS%(J5TIRbX{?(sjp_w4;<2Q^}wSpDb zz9KJVoH{cc!PO?VpdtHO!YoBWFj05{Bp5H)Hw5XZsj59v-t@?4>G&Bul{}J)oE)UE zkSr(rD2y8?HDm^?^H1M@-*pl` z`gbMhE}Swi?#N6{}{}@n=<16jC4NCr-xcQ ziybI;nL}cdVgksB1qC}fJ0xs9iRdp@D2^t^BaDfnqiK&D@KPp>m{_{2L|F$|@Mws2 zqIgg{WX+5WS3gSlhcaQp&qg~i#?enoR;;p!TLLBaxz87B*phQu3i^5Prl6bEK2SNM zipQ<(6gYfVeYQo)C$X(XEd8ZDWxznSBp&qpKR>6dw#C(Tr(%7LE!!-G;*zAC&t4hH zN`RchKbFB#A)}BSeX0!E>WXIhcQEbfs;l`tMu~p=2F2(s6U4RfGkzsC0|*^QA6sJSk2^OeX(2m9{> z!PJZg40ku;ox+Q!ADfry&JD*gXR|umaR`Y89JM=AP1NhDmeG5wiF>glSA!V9q)3Hk z*`Xx5_~Z6+`t`#p@9SavNPK8=(v$5pMtqDQ;HL|z&-)}(Mw%kxUu;&WAy zwBKnqDltvJnKRk7c;A`Y`_=0y+|BHRb=MhT`}uRrl)mC1w9>}zN#Zr&Lt(|3RCz7s z%gj944V7@{_R+qCY49K6Tjh_>Oc(lrDaoI7pS&@CDwN`0GM~3KFxR;Oi*^9po-v6o z)JH!{FFZ*#@$MZkPA19Y2rp_yMgwhNkn;*NJq#~6d8L$P)vek5=4+lBA&QS&k?{FG zqO0g=U;hf+c`b61^1N||RlH6jEuv?R?KkC}D09H;>z+q{Q<}#Y_Fo_VZQd1UEv$DP z)nG-JJdR(?veXJP=^lNm_Z?sV)=K0iNAcG*JzufvWtiB?$=Wk>b9vueJY`zWK-M{D zrl6TdmOWI6E7d3Bw`mZh%A-}phn@ojcfQ{2dF$g8Y2Pips;-CT7z{AfIT{znxaF!V zWtOiZy0ZafmY1tya3?F4(%7G+N>)P*P&%TSqSPnV)H_j$!KsA=^%wLR$jsh)u(G{& z_?$`2dqAO;GQ5?4Z8mTXh_#C*Wvj_27>!omj zUe3rZ`6d|q5~}S|^*T=xK_Y(R&)*R=l6)1f1r@4#(hx1lb_(?BAK+CLMDOT=#mQd@ zp$0KEif&=6tokuKqdD|j*SI@nUQ{V5ia%qV%*mpQ3{rz1&@v7R`a9^n{$VOUpkhV- zKno=|9Cx#6<>2Ua|7)e3>EW9yIp&`~*^~XKcqf}MoMq?vF?LbdvA&(>h1UK->-?z>i+f+zYcL-`Wb*l9OrNlUziTl zHAPCJj98|iDR?2;U~j__{GHsg z`oIGw<2zkJ51`MBy+5BGUMevjtRmELQU2TLp4c?Bk7f>|@HO-^6O3EEUplby6JSM9 zsxh37rYsZ`@AJ4g`-&5Na6lqQRpqCQZL8(i0UP2wh5CDe5|Dc&{1&mgsp5=hjX z2F?aQh8ZRMu5rO;Eapsyw~kaK9l&!6_R(=QXBBcyS*$vQsMrJQLsqtAm8#{mB_!IFqzW1EdwS#}mZZWR7!N_qThtx4iPT*jO(%fZHt z{}3QXh^MZ932feki-#zRp7HMHJ*M!eE>QxF+w$2};gFd{n3XWU3i;|;DTtadk78EM z{0uPM@Q}jed;aMOuvzQP%x~T;eo`uxX7G`amux-Uy)@GvjZe2++<#M}&^5)ZFLH9f zvi^gmF94s0mX_5HC3;@?g3bpi6mf8UP0MeqD{~;_gh%f2e81lKTh*zijmwuKC(=Zz z{m6Sm@r_7g# zn*IbQi8%EUTAE?#Qdna2Id;AD!r!`(--^`P8wGe<31HVu<MijaJqD|X(wZ(6o@xZ5rkZ95TMaJx!Grs5EXxU1C#w{s5;wa=*dgbh#5>U|VpnC?d60oH`ZKqq_L zQ9q7W$Wfh^T&|AKnM&qD0`+oq7I2KS9cObnA_&HMtvmYR5wie+1Z?8klFIqGceutC z?OaHVfDTO(?w$~j%BdG=@*vL|fjW{jfQ#47y;v@VFeXq}UeB>6tTn*hM!j5ZRFGUr zJ0qlbgdS5tCs~NUGQ?DCl8WJF!BmIMZ!#Zb!4U{WJ_7#Z?&~Q_y`qk*qmZ^6)Y0#R z7Qn$e_I;~}ja#m%BGUeE`}T`Zx1atyPq2=K=pQRQ7VbQMBRb$E&6~O)b*d{p2vWGrM&)P4e>W}~|bdz7B zjK4u11d8|7BxaNUZo|*LnT!WB=W@-Mv#t=($WPqq_>o!tIEh9OBQ)=Y*4wY@ibU4z zdd#~YTc#n4cb2H}$EyXJ-}?~T3(EDCA}_46NQ&T6?(q$_cu~bZ+xY;@*;J7$n{sKW z#8{Bjdz-mZ*`bAJgbT6%h&%%!mvkaZp(^R|)mwwoBxzx}Y{-fH&PGx$;o(P!uokzN zvVM5~xsH&~Z(a4T{ixxilBx$jM?yA9SCI}^fRE>Hw|tqR78P~n7TBVGw0RgKEB*il zH(9xQp6GR-aT_>!b|GW&SKEd99ch{Om9{-vf@u9raw815O5VU%ARxN&HYIbea8qvx z!DpB{%EqW_Q#aO)#QQnMT%wA=n0Vxr`0%~IA2O=WJXTYFJtt`nk85_Uh- zvjcuVdix@8td7wxB{{P>Wz(}MMq4?_I1NX-LqnJw?vqf~Wuy?J)Djl~Ymq?^80=Q* zaCyCjfq%n6nS|8mI{3=W>N*F>eY)Xcpn)!JLSE^{o@M3()K~+v1IDe#T^PE3%F9NR zg=IylyLqJgvDJhMpCJZb*<+MuB%PxjB~$H^QoG! zvp2(`fg?-=$s~Vk9N%lq-!0mwo+<@pdo^5T;;=C79|;rs1NO)c4rGYljhW}hC`Uo6 zwLd)=pEje|J|k(>1pf3!nk2)gFGmemD~=6p!`_zKZY8)zuf_>b_Rn`%x86T#f|Cz6 zUu%pY^I?7>c$n<8$0uH($3q;TY_Ppg%f+(`9)*n7Aj20-s@cXN>~*iVx^4=GG4fhf zCA|ZHZ-nhdIz2+$JUDQwP^l?8C zK=sW42DaZ+;wZa`((@aoOaZLpQF%vxuuu(=l7yE^y(hmvP)~c1+QG_i&pCXPp~`|H zWziTqkN&5HC9@LT!t4}c>nSj73^SsS={45-4QHc{nRB?%(0fTWWi`GkpK+ zv~J+D?-p>c`xVi&WJykt=?x&b=bf;;iEGj{-$i|3N0w`faxyMF&Xx%-LVl!~?Pko? zMYn^9S*^_YQaZ{AjdH=hi#H-?ajU$!r)YJr#>C zVryp8gVefIj})0M++HYY&B+6sf;)hgJ#M+lMhhJH;9oZIP^lF%i8|Q*lgQQUQcF`6 z0%T!1{x`sUlRY*&axJ>5HI4&#PHQXkp%C~xJJqDy#-^88hQ4kLhP|)S*<7`p?19S^ zwN68GK$yOfH@SU!S>ZRyvIBs&&8ey8SKwS9{``9-z!*0t9fQB3ZS{vu&S?LI)Mi>c z=HTZX5~s|QPVcg@Ze>{P$U`IWUn=)%$EA&#C$&kdf37LL4whT0$yLZ}j2>hBZwy9; zv;?>~B~(0Ay}o3~G43aRFFMZi39gU+@a~gE^#^bVd^BztppS7*zLt?%n@1k&tU>g% z$2`9%r^p+XvgKNrRTW1UR@?3jgs7A_SaCA_bomU3SGN(C+vbV4fPvW9E49UvFta>T zzv8paIfw^YEM}{#8C3O#$U=Q;nY`s6mN6>I3zg}^M%03)oY_9_j0`r-BjsX#Xmdb~ zcxE)MbwNhtUG>8m>HuFNB}nBS^DlmYhR5!(PAPPP)Vji3J7Hw~%KYM{d+SNYZC1pp zIAWLRF#i176ZCls;8p^O8V)Ib*E{k)K<#A}wtmicvG}-xSI66bxjzG+-U}C>%Ucl& z%hKq{C6yjaNh<+VsrPE5>a+%>c-Vg*zNJF~|B)A!B1yotiCRF$Rpn-jCH!OhI*0v} z{SAMi>|}1XAhZddR!cl?s&DxaKBLdY2!lZKLPm9l!hSn7ST-V>IQsCx%~KZUSpd(# ztWDjQKiBKS8y0EmEJuxLDxSH@6-*h!o0Gq@S23Wh$j2T`X{_emwXZDNY&jOyU%~g>MNo>kCIy~AU!TR8UzY!Mr47;3&<$G?;;$deV zVmh9{Eaa1INq_R?Pf&Fk)Z~cA=!89`@9NHGX)VyoOs*7^D|ls9SVX(((L_=p-!QMF zI_{#70_b6_>L!^a@xCKNZO~?FKQiT75dFFr)!sgMetgcoODDinln6~&>e33!&Bp21 z`ZHh3$b?D2W6Mj%HgN)JIXoiRQD$7Ws*m=yd^)^B;OS_$3_I*4?{!Dp`CV6$-Z{Bb zh!g7A6!qWW@9_oV|EQMmkFlL*koQiyPh>4) zB6uko{iX+we!W}tO=w-F`!gYb%k3nTG};26!>6nGzvYjE^(0A#hG# zVi|HsB_$M|Jn?R;+VFAAtR^Y?Sf)!Utefq9z9Zp~5ZRYWSFrt)4zddsrtReI;`ddQ z8LEi1vGf{Ebul^WQ_Ap^GKNPMkhxJYh?@*JX|Z@UmYgmoty8NdPbSV@UfF(xPyW+k z=9U3X7QqM&ae06*U!zV8GYVv)s~?>={ttsT>)H8ohL}IDc-_Tq7}4hOBUtITgg}^i zlmnapeEW-sm;VMWsG)k-`Aq5GlM`oR97A41BLl*ftc<&c*`<9qRz5rLl%|m;PlHE+O}(MHXU@2|7Buo z8iZS5MN7hkho5s7D~Nm&mY7q0B{FPot5&QX<;^Rl8SRZd{|G6^5<#%g85uBOaq7vm zwWm#<>}nVrs^PK`v1fU`d~heGca@vbz8u&8xZ?-(`7={`Ex8mF=Yik6UV^wV%lK&Vb%D3Fh zl!JP$C6N79HD398{vUt0rt~8d0dOlEV9>3{WD0)Bho={7Q5SHJa@|G#RHByp6~({# zaVN{N7WDiinig*SK83@t82r+Mq@+^C$_`$q> zl#t&iv7Z(OcxI>yvK8d-)cnxd$o_`xf{tWnHgD_FFw5n&OC6uchj<`S@}ENgNVG~h zL7*6oE3)L7hgBC36UtaC5F09w);j#!OR$M$;Cm%ZO65X{2OibydT#J~O-`df=^3AAs~sN4+6rdZ5!yJqJjsGxho` zAal28yV^}57I5~bcOENfq-Raj*--*;(A2c{iIMjzDS6Tu*#2sY`=hBvo^sjbso&7Q zHaX75xh6K6Smi~P*#}l~PH~8$pyG%2vWNDe`n54U*03$jp@O=!+MpqK{bDqDa%0b# zD2D`8ItL|51iRU&h$V<>ox^6@;AOq34n9CdbvML(E)%in&+|E)-1N_81CnJcP752k zXG~!hpK7UcytyZhqVK>_g;zQe*jGzVX?D;!WRo}k;?>hOUy9q4qGLVZeQz&z$g~*F z=Wlj|V?XNLU8DwuGR76yN$aWa_Rc2E>1(NqC z7MEZOEr9X)PpYKU*gjc+dcnL#{Jts>myna)V?Xd}nPGTC(~5R+}F z{b=?@=HhFVTxFGPTk>_$>$h7I$~@0slP9Tke^iu|e((U#0`DR4ulqPL0x!2pBiD^8RJJLycKy|Jd4wf89Y$VuRL}o~AMlPup-D)D<^Uno4kZ?!3 zgy2nK;wfU@K|#YtJ~Ef)Y)N$`afQ{GmMteV2S4c1<#_uFYa3#@4ncNs>8I~&9~REE zh8dX&j664*+keIQUwI++x4bwO0jNUkNELe{{JE-}Rk&UkyB12l1xuAbs~s$p5IIS% zTW&`o@boWzXgI-T(RtaA(e<~_!r2$TWfi#?@^J%~sK*Ty$G<(#eB7!V(jEY;*hqNS zTU8)86r&1+T}BsL7kzjV+B=*@3p;K7VYZdXnyMn_3=rH|e3B#BFco_iVm0@RZ+(D| zs&OBw0yTt^l(Ur0o9uT}G?3$j#Jd~HO>P8LOqFq~SQ#kCJQrW{n5`ZvV-X3Yq)m|+o?eSBdo z-MP7@s0ou0E5bZ`g1+vTqaqxh>7VxbKQ!AWWi$?O@