From f92aeba403ec22e2a3219550b51191ae82bac000 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 22 Jun 2024 21:02:04 +0200 Subject: [PATCH] feat: test integration connection (#669) * feat: add test integration for pi-hole * refactor: test integration for pi-hole * fix: multiple secrets of same type could be used for integration creation * fix: remove integration test connection test and add mock for test-connection function * fix: add missing onUpdateFn to mysql integration secrets * fix: format issues * feat: add home assistant test connection * fix: deepsource issues * test: add system integration tests for test connection * fix: add before all for pulling home assistant image * test: add unit tests for handleTestConnectionResponseAsync * test: add unit test for testConnectionAsync * test: add mroe tests to integration-test-connection * fix: deepsource issues * fix: deepsource issue * chore: address pull request feedback --- .../secrets/integration-secret-card.tsx} | 2 +- .../secrets/integration-secret-icons.ts} | 0 .../secrets/integration-secret-inputs.tsx} | 4 +- .../_integration-test-connection.tsx | 129 --------- .../edit/[id]/_integration-edit-form.tsx | 78 +++--- .../new/_integration-new-form.tsx | 69 +++-- packages/api/src/root.ts | 2 +- .../integration-router.ts} | 119 +++----- .../integration-test-connection.ts | 95 +++++++ .../integration-router.spec.ts} | 204 +------------- .../integration-test-connection.spec.ts | 253 ++++++++++++++++++ packages/api/src/trpc.ts | 2 + packages/common/src/error.ts | 13 + packages/common/src/url.ts | 6 +- packages/db/schema/mysql.ts | 4 +- packages/db/schema/sqlite.ts | 4 +- packages/integrations/package.json | 4 +- packages/integrations/src/base/creator.ts | 16 ++ packages/integrations/src/base/integration.ts | 108 +++++++- .../src/base/test-connection-error.ts | 26 ++ packages/integrations/src/client.ts | 1 + .../homeassistant-integration.ts | 79 ++++-- packages/integrations/src/index.ts | 5 + .../src/pi-hole/pi-hole-integration.ts | 23 +- packages/integrations/test/base.spec.ts | 249 +++++++++++++++++ .../integrations/test/home-assistant.spec.ts | 81 ++++++ packages/integrations/test/pi-hole.spec.ts | 30 +++ .../test/volumes/home-assistant-config.zip | Bin 0 -> 97693 bytes packages/translation/src/lang/en.ts | 51 +++- pnpm-lock.yaml | 31 ++- 30 files changed, 1138 insertions(+), 550 deletions(-) rename apps/nextjs/src/app/[locale]/manage/integrations/{_integration-secret-card.tsx => _components/secrets/integration-secret-card.tsx} (97%) rename apps/nextjs/src/app/[locale]/manage/integrations/{_integration-secret-icons.ts => _components/secrets/integration-secret-icons.ts} (100%) rename apps/nextjs/src/app/[locale]/manage/integrations/{_integration-secret-inputs.tsx => _components/secrets/integration-secret-inputs.tsx} (92%) delete mode 100644 apps/nextjs/src/app/[locale]/manage/integrations/_integration-test-connection.tsx rename packages/api/src/router/{integration.ts => integration/integration-router.ts} (61%) create mode 100644 packages/api/src/router/integration/integration-test-connection.ts rename packages/api/src/router/test/{integration.spec.ts => integration/integration-router.spec.ts} (60%) create mode 100644 packages/api/src/router/test/integration/integration-test-connection.spec.ts create mode 100644 packages/integrations/src/base/creator.ts create mode 100644 packages/integrations/src/base/test-connection-error.ts create mode 100644 packages/integrations/src/client.ts create mode 100644 packages/integrations/test/base.spec.ts create mode 100644 packages/integrations/test/home-assistant.spec.ts create mode 100644 packages/integrations/test/volumes/home-assistant-config.zip diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_integration-secret-card.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx similarity index 97% rename from apps/nextjs/src/app/[locale]/manage/integrations/_integration-secret-card.tsx rename to apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx index b23fd148a..f90b1d40f 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/_integration-secret-card.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx @@ -12,7 +12,7 @@ import type { RouterOutputs } from "@homarr/api"; import { integrationSecretKindObject } from "@homarr/definitions"; import { useI18n } from "@homarr/translation/client"; -import { integrationSecretIcons } from "./_integration-secret-icons"; +import { integrationSecretIcons } from "./integration-secret-icons"; dayjs.extend(relativeTime); diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_integration-secret-icons.ts b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-icons.ts similarity index 100% rename from apps/nextjs/src/app/[locale]/manage/integrations/_integration-secret-icons.ts rename to apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-icons.ts diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_integration-secret-inputs.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-inputs.tsx similarity index 92% rename from apps/nextjs/src/app/[locale]/manage/integrations/_integration-secret-inputs.tsx rename to apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-inputs.tsx index f73e8516b..45f5b3156 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/_integration-secret-inputs.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-inputs.tsx @@ -7,7 +7,7 @@ import { integrationSecretKindObject } from "@homarr/definitions"; import type { IntegrationSecretKind } from "@homarr/definitions"; import { useI18n } from "@homarr/translation/client"; -import { integrationSecretIcons } from "./_integration-secret-icons"; +import { integrationSecretIcons } from "./integration-secret-icons"; interface IntegrationSecretInputProps { withAsterisk?: boolean; @@ -50,7 +50,7 @@ const PrivateSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => } /> diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_integration-test-connection.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/_integration-test-connection.tsx deleted file mode 100644 index 2ae512edd..000000000 --- a/apps/nextjs/src/app/[locale]/manage/integrations/_integration-test-connection.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import { useRef, useState } from "react"; -import { Alert, Anchor, Group, Loader } from "@mantine/core"; -import { IconCheck, IconInfoCircle, IconX } from "@tabler/icons-react"; - -import type { RouterInputs } from "@homarr/api"; -import { clientApi } from "@homarr/api/client"; -import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useI18n, useScopedI18n } from "@homarr/translation/client"; - -interface UseTestConnectionDirtyProps { - defaultDirty: boolean; - initialFormValue: { - url: string; - secrets: { kind: string; value: string | null }[]; - }; -} - -export const useTestConnectionDirty = ({ defaultDirty, initialFormValue }: UseTestConnectionDirtyProps) => { - const [isDirty, setIsDirty] = useState(defaultDirty); - const prevFormValueRef = useRef(initialFormValue); - - return { - onValuesChange: (values: typeof initialFormValue) => { - if (isDirty) return; - - // If relevant values changed, set dirty - if ( - prevFormValueRef.current.url !== values.url || - !prevFormValueRef.current.secrets - .map((secret) => secret.value) - .every((secretValue, index) => values.secrets[index]?.value === secretValue) - ) { - setIsDirty(true); - return; - } - - // If relevant values changed back to last tested, set not dirty - setIsDirty(false); - }, - isDirty, - removeDirty: () => { - prevFormValueRef.current = initialFormValue; - setIsDirty(false); - }, - }; -}; - -interface TestConnectionProps { - isDirty: boolean; - removeDirty: () => void; - integration: RouterInputs["integration"]["testConnection"] & { name: string }; -} - -export const TestConnection = ({ integration, removeDirty, isDirty }: TestConnectionProps) => { - const t = useScopedI18n("integration.testConnection"); - const { mutateAsync, ...mutation } = clientApi.integration.testConnection.useMutation(); - - return ( - - { - await mutateAsync(integration, { - onSuccess: () => { - removeDirty(); - showSuccessNotification({ - title: t("notification.success.title"), - message: t("notification.success.message"), - }); - }, - onError: (error) => { - if (error.data?.zodError?.fieldErrors.url) { - showErrorNotification({ - title: t("notification.invalidUrl.title"), - message: t("notification.invalidUrl.message"), - }); - return; - } - - if (error.message === "SECRETS_NOT_DEFINED") { - showErrorNotification({ - title: t("notification.notAllSecretsProvided.title"), - message: t("notification.notAllSecretsProvided.message"), - }); - return; - } - - showErrorNotification({ - title: t("notification.commonError.title"), - message: t("notification.commonError.message"), - }); - }, - }); - }} - > - {t("action")} - - - - ); -}; - -interface TestConnectionIconProps { - isDirty: boolean; - isPending: boolean; - isSuccess: boolean; - isError: boolean; - size: number; -} - -const TestConnectionIcon = ({ isDirty, isPending, isSuccess, isError, size }: TestConnectionIconProps) => { - if (isPending) return ; - if (isDirty) return null; - if (isSuccess) return ; - if (isError) return ; - return null; -}; - -export const TestConnectionNoticeAlert = () => { - const t = useI18n(); - return ( - }> - {t("integration.testConnection.alertNotice")} - - ); -}; diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/_integration-edit-form.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/_integration-edit-form.tsx index c93826e92..847479ea3 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/_integration-edit-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/edit/[id]/_integration-edit-form.tsx @@ -8,6 +8,7 @@ import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { getAllSecretKindOptions, getDefaultSecretKinds } from "@homarr/definitions"; import { useZodForm } from "@homarr/form"; +import { convertIntegrationTestConnectionError } from "@homarr/integrations/client"; import { useConfirmModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; @@ -15,9 +16,8 @@ import type { z } from "@homarr/validation"; import { validation } from "@homarr/validation"; import { revalidatePathActionAsync } from "~/app/revalidatePathAction"; -import { SecretCard } from "../../_integration-secret-card"; -import { IntegrationSecretInput } from "../../_integration-secret-inputs"; -import { TestConnection, TestConnectionNoticeAlert, useTestConnectionDirty } from "../../_integration-test-connection"; +import { SecretCard } from "../../_components/secrets/integration-secret-card"; +import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs"; interface EditIntegrationForm { integration: RouterOutputs["integration"]["byId"]; @@ -30,30 +30,23 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => { getAllSecretKindOptions(integration.kind).find((secretKinds) => integration.secrets.every((secret) => secretKinds.includes(secret.kind)), ) ?? getDefaultSecretKinds(integration.kind); - const initialFormValues = { - name: integration.name, - url: integration.url, - secrets: secretsKinds.map((kind) => ({ - kind, - value: integration.secrets.find((secret) => secret.kind === kind)?.value ?? "", - })), - }; - const { isDirty, onValuesChange, removeDirty } = useTestConnectionDirty({ - defaultDirty: true, - initialFormValue: initialFormValues, - }); const router = useRouter(); const form = useZodForm(validation.integration.update.omit({ id: true }), { - initialValues: initialFormValues, - onValuesChange, + initialValues: { + name: integration.name, + url: integration.url, + secrets: secretsKinds.map((kind) => ({ + kind, + value: integration.secrets.find((secret) => secret.kind === kind)?.value ?? "", + })), + }, }); const { mutateAsync, isPending } = clientApi.integration.update.useMutation(); const secretsMap = new Map(integration.secrets.map((secret) => [secret.kind, secret])); const handleSubmitAsync = async (values: FormType) => { - if (isDirty) return; await mutateAsync( { id: integration.id, @@ -71,7 +64,19 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => { }); void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations")); }, - onError: () => { + onError: (error) => { + const testConnectionError = convertIntegrationTestConnectionError(error.data?.error); + + if (testConnectionError) { + showErrorNotification({ + title: t(`integration.testConnection.notification.${testConnectionError.key}.title`), + message: testConnectionError.message + ? testConnectionError.message + : t(`integration.testConnection.notification.${testConnectionError.key}.message`), + }); + return; + } + showErrorNotification({ title: t("integration.page.edit.notification.error.title"), message: t("integration.page.edit.notification.error.message"), @@ -84,8 +89,6 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => { return (
void handleSubmitAsync(values))}> - - @@ -98,18 +101,18 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion secret={secretsMap.get(kind)!} onCancel={() => - new Promise((res) => { + new Promise((resolve) => { // When nothing changed, just close the secret card if ((form.values.secrets[index]?.value ?? "") === (secretsMap.get(kind)?.value ?? "")) { - return res(true); + return resolve(true); } openConfirmModal({ title: t("integration.secrets.reset.title"), children: t("integration.secrets.reset.message"), - onCancel: () => res(false), + onCancel: () => resolve(false), onConfirm: () => { form.setFieldValue(`secrets.${index}.value`, secretsMap.get(kind)?.value ?? ""); - res(true); + resolve(true); }, }); }) @@ -126,24 +129,13 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => { - - - - - - + + + diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx index 524b2efb9..d7ab653e1 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx @@ -10,13 +10,13 @@ import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions import { getAllSecretKindOptions } from "@homarr/definitions"; import type { UseFormReturnType } from "@homarr/form"; import { useZodForm } from "@homarr/form"; +import { convertIntegrationTestConnectionError } from "@homarr/integrations/client"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; import type { z } from "@homarr/validation"; import { validation } from "@homarr/validation"; -import { IntegrationSecretInput } from "../_integration-secret-inputs"; -import { TestConnection, TestConnectionNoticeAlert, useTestConnectionDirty } from "../_integration-test-connection"; +import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs"; import { revalidatePathActionAsync } from "../../../../revalidatePathAction"; interface NewIntegrationFormProps { @@ -28,27 +28,20 @@ interface NewIntegrationFormProps { export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => { const t = useI18n(); const secretKinds = getAllSecretKindOptions(searchParams.kind); - const initialFormValues = { - name: searchParams.name ?? "", - url: searchParams.url ?? "", - secrets: secretKinds[0].map((kind) => ({ - kind, - value: "", - })), - }; - const { isDirty, onValuesChange, removeDirty } = useTestConnectionDirty({ - defaultDirty: true, - initialFormValue: initialFormValues, - }); const router = useRouter(); const form = useZodForm(validation.integration.create.omit({ kind: true }), { - initialValues: initialFormValues, - onValuesChange, + initialValues: { + name: searchParams.name ?? "", + url: searchParams.url ?? "", + secrets: secretKinds[0].map((kind) => ({ + kind, + value: "", + })), + }, }); const { mutateAsync, isPending } = clientApi.integration.create.useMutation(); const handleSubmitAsync = async (values: FormType) => { - if (isDirty) return; await mutateAsync( { kind: searchParams.kind, @@ -62,7 +55,19 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => }); void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations")); }, - onError: () => { + onError: (error) => { + const testConnectionError = convertIntegrationTestConnectionError(error.data?.error); + + if (testConnectionError) { + showErrorNotification({ + title: t(`integration.testConnection.notification.${testConnectionError.key}.title`), + message: testConnectionError.message + ? testConnectionError.message + : t(`integration.testConnection.notification.${testConnectionError.key}.message`), + }); + return; + } + showErrorNotification({ title: t("integration.page.create.notification.error.title"), message: t("integration.page.create.notification.error.message"), @@ -75,8 +80,6 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => return (
void handleSubmitAsync(value))}> - - @@ -95,25 +98,13 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => - - - - - - - + + + diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index 239be6f09..ce71e53b8 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -4,7 +4,7 @@ import { dockerRouter } from "./router/docker/docker-router"; import { groupRouter } from "./router/group"; import { homeRouter } from "./router/home"; import { iconsRouter } from "./router/icons"; -import { integrationRouter } from "./router/integration"; +import { integrationRouter } from "./router/integration/integration-router"; import { inviteRouter } from "./router/invite"; import { locationRouter } from "./router/location"; import { logRouter } from "./router/log"; diff --git a/packages/api/src/router/integration.ts b/packages/api/src/router/integration/integration-router.ts similarity index 61% rename from packages/api/src/router/integration.ts rename to packages/api/src/router/integration/integration-router.ts index c7b333673..611c288d1 100644 --- a/packages/api/src/router/integration.ts +++ b/packages/api/src/router/integration/integration-router.ts @@ -5,10 +5,11 @@ import type { Database } from "@homarr/db"; import { and, createId, eq } from "@homarr/db"; import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite"; import type { IntegrationSecretKind } from "@homarr/definitions"; -import { getAllSecretKindOptions, integrationKinds, integrationSecretKindObject } from "@homarr/definitions"; +import { integrationKinds, integrationSecretKindObject } from "@homarr/definitions"; import { validation } from "@homarr/validation"; -import { createTRPCRouter, publicProcedure } from "../trpc"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; +import { testConnectionAsync } from "./integration-test-connection"; export const integrationRouter = createTRPCRouter({ all: publicProcedure.query(async ({ ctx }) => { @@ -60,6 +61,14 @@ export const integrationRouter = createTRPCRouter({ }; }), create: publicProcedure.input(validation.integration.create).mutation(async ({ ctx, input }) => { + await testConnectionAsync({ + id: "new", + name: input.name, + url: input.url, + kind: input.kind, + secrets: input.secrets, + }); + const integrationId = createId(); await ctx.db.insert(integrations).values({ id: integrationId, @@ -68,13 +77,14 @@ export const integrationRouter = createTRPCRouter({ kind: input.kind, }); - for (const secret of input.secrets) { - await ctx.db.insert(integrationSecrets).values({ - kind: secret.kind, - value: encryptSecret(secret.value), - updatedAt: new Date(), - integrationId, - }); + if (input.secrets.length >= 1) { + await ctx.db.insert(integrationSecrets).values( + input.secrets.map((secret) => ({ + kind: secret.kind, + value: encryptSecret(secret.value), + integrationId, + })), + ); } }), update: publicProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => { @@ -92,6 +102,17 @@ export const integrationRouter = createTRPCRouter({ }); } + await testConnectionAsync( + { + id: input.id, + name: input.name, + url: input.url, + kind: integration.kind, + secrets: input.secrets, + }, + integration.secrets, + ); + await ctx.db .update(integrations) .set({ @@ -100,15 +121,14 @@ export const integrationRouter = createTRPCRouter({ }) .where(eq(integrations.id, input.id)); - const decryptedSecrets = integration.secrets.map((secret) => ({ - ...secret, - value: decryptSecret(secret.value), - })); - const changedSecrets = input.secrets.filter( (secret): secret is { kind: IntegrationSecretKind; value: string } => secret.value !== null && // only update secrets that have a value - !decryptedSecrets.find((dSecret) => dSecret.kind === secret.kind && dSecret.value === secret.value), + !integration.secrets.find( + // Checked above + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (dbSecret) => dbSecret.kind === secret.kind && dbSecret.value === encryptSecret(secret.value!), + ), ); if (changedSecrets.length > 0) { @@ -118,7 +138,7 @@ export const integrationRouter = createTRPCRouter({ value: changedSecret.value, kind: changedSecret.kind, }; - if (!decryptedSecrets.some((secret) => secret.kind === changedSecret.kind)) { + if (!integration.secrets.some((secret) => secret.kind === changedSecret.kind)) { await addSecretAsync(ctx.db, secretInput); } else { await updateSecretAsync(ctx.db, secretInput); @@ -140,71 +160,6 @@ export const integrationRouter = createTRPCRouter({ await ctx.db.delete(integrations).where(eq(integrations.id, input.id)); }), - testConnection: publicProcedure.input(validation.integration.testConnection).mutation(async ({ ctx, input }) => { - const secrets = input.secrets.filter((secret): secret is { kind: IntegrationSecretKind; value: string } => - Boolean(secret.value), - ); - - // Find any matching secret kinds - let secretKinds = getAllSecretKindOptions(input.kind).find((secretKinds) => - secretKinds.every((secretKind) => secrets.some((secret) => secret.kind === secretKind)), - ); - - if (!secretKinds && input.id === null) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "SECRETS_NOT_DEFINED", - }); - } - - if (!secretKinds && input.id !== null) { - const integration = await ctx.db.query.integrations.findFirst({ - where: eq(integrations.id, input.id), - with: { - secrets: true, - }, - }); - if (!integration) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "SECRETS_NOT_DEFINED", - }); - } - const decryptedSecrets = integration.secrets.map((secret) => ({ - ...secret, - value: decryptSecret(secret.value), - })); - - // Add secrets that are not defined in the input from the database - for (const dbSecret of decryptedSecrets) { - if (!secrets.find((secret) => secret.kind === dbSecret.kind)) { - secrets.push({ - kind: dbSecret.kind, - value: dbSecret.value, - }); - } - } - - secretKinds = getAllSecretKindOptions(input.kind).find((secretKinds) => - secretKinds.every((secretKind) => secrets.some((secret) => secret.kind === secretKind)), - ); - - if (!secretKinds) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "SECRETS_NOT_DEFINED", - }); - } - } - - // TODO: actually test the connection - // Probably by calling a function on the integration class - // getIntegration(input.kind).testConnection(secrets) - // getIntegration(kind: IntegrationKind): Integration - // interface Integration { - // testConnection(): Promise; - // } - }), }); interface UpdateSecretInput { @@ -217,7 +172,6 @@ const updateSecretAsync = async (db: Database, input: UpdateSecretInput) => { .update(integrationSecrets) .set({ value: encryptSecret(input.value), - updatedAt: new Date(), }) .where(and(eq(integrationSecrets.integrationId, input.integrationId), eq(integrationSecrets.kind, input.kind))); }; @@ -231,7 +185,6 @@ const addSecretAsync = async (db: Database, input: AddSecretInput) => { await db.insert(integrationSecrets).values({ kind: input.kind, value: encryptSecret(input.value), - updatedAt: new Date(), integrationId: input.integrationId, }); }; diff --git a/packages/api/src/router/integration/integration-test-connection.ts b/packages/api/src/router/integration/integration-test-connection.ts new file mode 100644 index 000000000..ba8ffa2dd --- /dev/null +++ b/packages/api/src/router/integration/integration-test-connection.ts @@ -0,0 +1,95 @@ +import { decryptSecret } from "@homarr/common"; +import type { Integration } from "@homarr/db/schema/sqlite"; +import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; +import { getAllSecretKindOptions } from "@homarr/definitions"; +import { integrationCreatorByKind, IntegrationTestConnectionError } from "@homarr/integrations"; + +type FormIntegration = Integration & { + secrets: { + kind: IntegrationSecretKind; + value: string | null; + }[]; +}; + +export const testConnectionAsync = async ( + integration: FormIntegration, + dbSecrets: { + kind: IntegrationSecretKind; + value: `${string}.${string}`; + }[] = [], +) => { + const formSecrets = integration.secrets + .filter((secret) => secret.value !== null) + .map((secret) => ({ + ...secret, + // We ensured above that the value is not null + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + value: secret.value!, + source: "form" as const, + })); + + const decryptedDbSecrets = dbSecrets.map((secret) => ({ + ...secret, + value: decryptSecret(secret.value), + source: "db" as const, + })); + + const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets]; + const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets); + + const filteredSecrets = secretKinds.map((kind) => { + const secrets = sourcedSecrets.filter((secret) => secret.kind === kind); + // Will never be undefined because of the check before + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (secrets.length === 1) return secrets[0]!; + + // There will always be a matching secret because of the getSecretKindOption function + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return secrets.find((secret) => secret.source === "form") ?? secrets[0]!; + }); + + const integrationInstance = integrationCreatorByKind(integration.kind, { + id: integration.id, + name: integration.name, + url: integration.url, + decryptedSecrets: filteredSecrets, + }); + + await integrationInstance.testConnectionAsync(); +}; + +interface SourcedIntegrationSecret { + kind: IntegrationSecretKind; + value: string; + source: "db" | "form"; +} + +const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedIntegrationSecret[]) => { + const matchingSecretKindOptions = getAllSecretKindOptions(kind).filter((secretKinds) => + secretKinds.every((kind) => sourcedSecrets.some((secret) => secret.kind === kind)), + ); + + if (matchingSecretKindOptions.length === 0) { + throw new IntegrationTestConnectionError("secretNotDefined"); + } + + if (matchingSecretKindOptions.length === 1) { + // Will never be undefined because of the check above + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return matchingSecretKindOptions[0]!; + } + + const onlyFormSecretsKindOptions = matchingSecretKindOptions.filter((secretKinds) => + sourcedSecrets.filter((secret) => secretKinds.includes(secret.kind)).every((secret) => secret.source === "form"), + ); + + if (onlyFormSecretsKindOptions.length >= 1) { + // Will never be undefined because of the check above + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return onlyFormSecretsKindOptions[0]!; + } + + // Will never be undefined because of the check above + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return matchingSecretKindOptions[0]!; +}; diff --git a/packages/api/src/router/test/integration.spec.ts b/packages/api/src/router/test/integration/integration-router.spec.ts similarity index 60% rename from packages/api/src/router/test/integration.spec.ts rename to packages/api/src/router/test/integration/integration-router.spec.ts index 5bbbcc4b4..711cfcb20 100644 --- a/packages/api/src/router/test/integration.spec.ts +++ b/packages/api/src/router/test/integration/integration-router.spec.ts @@ -7,12 +7,14 @@ import { createId } from "@homarr/db"; import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite"; import { createDb } from "@homarr/db/test"; -import type { RouterInputs } from "../.."; -import { integrationRouter } from "../integration"; -import { expectToBeDefined } from "./helper"; +import { integrationRouter } from "../../integration/integration-router"; +import { expectToBeDefined } from "../helper"; // Mock the auth module to return an empty session vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); +vi.mock("../../integration/integration-test-connection", () => ({ + testConnectionAsync: async () => await Promise.resolve(undefined), +})); describe("all should return all integrations", () => { it("should return all integrations", async () => { @@ -290,199 +292,3 @@ describe("delete should delete an integration", () => { expect(dbSecrets.length).toBe(0); }); }); - -describe("testConnection should test the connection to an integration", () => { - it.each([ - [ - "nzbGet" as const, - [ - { kind: "username" as const, value: null }, - { kind: "password" as const, value: "Password123!" }, - ], - ], - [ - "nzbGet" as const, - [ - { kind: "username" as const, value: "exampleUser" }, - { kind: "password" as const, value: null }, - ], - ], - ["sabNzbd" as const, [{ kind: "apiKey" as const, value: null }]], - [ - "sabNzbd" as const, - [ - { kind: "username" as const, value: "exampleUser" }, - { kind: "password" as const, value: "Password123!" }, - ], - ], - ])("should fail when a required secret is missing when creating %s integration", async (kind, secrets) => { - const db = createDb(); - const caller = integrationRouter.createCaller({ - db, - session: null, - }); - - const input: RouterInputs["integration"]["testConnection"] = { - id: null, - kind, - url: `http://${kind}.local`, - secrets, - }; - - const actAsync = async () => await caller.testConnection(input); - await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED"); - }); - - it.each([ - [ - "nzbGet" as const, - [ - { kind: "username" as const, value: "exampleUser" }, - { kind: "password" as const, value: "Password123!" }, - ], - ], - ["sabNzbd" as const, [{ kind: "apiKey" as const, value: "1234567890" }]], - ])( - "should be successful when all required secrets are defined for creation of %s integration", - async (kind, secrets) => { - const db = createDb(); - const caller = integrationRouter.createCaller({ - db, - session: null, - }); - - const input: RouterInputs["integration"]["testConnection"] = { - id: null, - kind, - url: `http://${kind}.local`, - secrets, - }; - - const actAsync = async () => await caller.testConnection(input); - await expect(actAsync()).resolves.toBeUndefined(); - }, - ); - - it("should be successful when all required secrets are defined for updating an nzbGet integration", async () => { - const db = createDb(); - const caller = integrationRouter.createCaller({ - db, - session: null, - }); - - const input: RouterInputs["integration"]["testConnection"] = { - id: createId(), - kind: "nzbGet", - url: "http://nzbGet.local", - secrets: [ - { kind: "username", value: "exampleUser" }, - { kind: "password", value: "Password123!" }, - ], - }; - - const actAsync = async () => await caller.testConnection(input); - await expect(actAsync()).resolves.toBeUndefined(); - }); - - it("should be successful when overriding one of the secrets for an existing nzbGet integration", async () => { - const db = createDb(); - const caller = integrationRouter.createCaller({ - db, - session: null, - }); - - const integrationId = createId(); - await db.insert(integrations).values({ - id: integrationId, - name: "NZBGet", - kind: "nzbGet", - url: "http://nzbGet.local", - }); - - await db.insert(integrationSecrets).values([ - { - kind: "username", - value: encryptSecret("exampleUser"), - integrationId, - updatedAt: new Date(), - }, - { - kind: "password", - value: encryptSecret("Password123!"), - integrationId, - updatedAt: new Date(), - }, - ]); - - const input: RouterInputs["integration"]["testConnection"] = { - id: integrationId, - kind: "nzbGet", - url: "http://nzbGet.local", - secrets: [ - { kind: "username", value: "newUser" }, - { kind: "password", value: null }, - ], - }; - - const actAsync = async () => await caller.testConnection(input); - await expect(actAsync()).resolves.toBeUndefined(); - }); - - it("should fail when a required secret is missing for an existing nzbGet integration", async () => { - const db = createDb(); - const caller = integrationRouter.createCaller({ - db, - session: null, - }); - - const integrationId = createId(); - await db.insert(integrations).values({ - id: integrationId, - name: "NZBGet", - kind: "nzbGet", - url: "http://nzbGet.local", - }); - - await db.insert(integrationSecrets).values([ - { - kind: "username", - value: encryptSecret("exampleUser"), - integrationId, - updatedAt: new Date(), - }, - ]); - - const input: RouterInputs["integration"]["testConnection"] = { - id: integrationId, - kind: "nzbGet", - url: "http://nzbGet.local", - secrets: [ - { kind: "username", value: "newUser" }, - { kind: "apiKey", value: "1234567890" }, - ], - }; - - const actAsync = async () => await caller.testConnection(input); - await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED"); - }); - - it("should fail when the updating integration does not exist", async () => { - const db = createDb(); - const caller = integrationRouter.createCaller({ - db, - session: null, - }); - - const actAsync = async () => - await caller.testConnection({ - id: createId(), - kind: "nzbGet", - url: "http://nzbGet.local", - secrets: [ - { kind: "username", value: null }, - { kind: "password", value: "Password123!" }, - ], - }); - await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED"); - }); -}); diff --git a/packages/api/src/router/test/integration/integration-test-connection.spec.ts b/packages/api/src/router/test/integration/integration-test-connection.spec.ts new file mode 100644 index 000000000..5118f526c --- /dev/null +++ b/packages/api/src/router/test/integration/integration-test-connection.spec.ts @@ -0,0 +1,253 @@ +import { describe, expect, test, vi } from "vitest"; + +import * as homarrDefinitions from "@homarr/definitions"; +import * as homarrIntegrations from "@homarr/integrations"; + +import { testConnectionAsync } from "../../integration/integration-test-connection"; + +vi.mock("@homarr/common", async (importActual) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importActual(); + + return { + ...actual, + decryptSecret: (value: string) => value.split(".")[0], + }; +}); + +describe("testConnectionAsync should run test connection of integration", () => { + test("with input of only form secrets matching api key kind it should use form apiKey", async () => { + // Arrange + const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind"); + const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); + factorySpy.mockReturnValue({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegration); + optionsSpy.mockReturnValue([["apiKey"]]); + + const integration = { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + kind: "piHole" as const, + secrets: [ + { + kind: "apiKey" as const, + value: "secret", + }, + ], + }; + + // Act + await testConnectionAsync(integration); + + // Assert + expect(factorySpy).toHaveBeenCalledWith("piHole", { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + decryptedSecrets: [ + expect.objectContaining({ + kind: "apiKey", + value: "secret", + }), + ], + }); + }); + + test("with input of only null form secrets and the required db secrets matching api key kind it should use db apiKey", async () => { + // Arrange + const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind"); + const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); + factorySpy.mockReturnValue({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegration); + optionsSpy.mockReturnValue([["apiKey"]]); + + const integration = { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + kind: "piHole" as const, + secrets: [ + { + kind: "apiKey" as const, + value: null, + }, + ], + }; + + const dbSecrets = [ + { + kind: "apiKey" as const, + value: "dbSecret.encrypted" as const, + }, + ]; + + // Act + await testConnectionAsync(integration, dbSecrets); + + // Assert + expect(factorySpy).toHaveBeenCalledWith("piHole", { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + decryptedSecrets: [ + expect.objectContaining({ + kind: "apiKey", + value: "dbSecret", + }), + ], + }); + }); + + test("with input of form and db secrets matching api key kind it should use form apiKey", async () => { + // Arrange + const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind"); + const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); + factorySpy.mockReturnValue({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegration); + optionsSpy.mockReturnValue([["apiKey"]]); + + const integration = { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + kind: "piHole" as const, + secrets: [ + { + kind: "apiKey" as const, + value: "secret", + }, + ], + }; + + const dbSecrets = [ + { + kind: "apiKey" as const, + value: "dbSecret.encrypted" as const, + }, + ]; + + // Act + await testConnectionAsync(integration, dbSecrets); + + // Assert + expect(factorySpy).toHaveBeenCalledWith("piHole", { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + decryptedSecrets: [ + expect.objectContaining({ + kind: "apiKey", + value: "secret", + }), + ], + }); + }); + + test("with input of form apiKey and db secrets for username and password it should use form apiKey when both is allowed", async () => { + // Arrange + const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind"); + const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); + factorySpy.mockReturnValue({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegration); + optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]); + + const integration = { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + kind: "piHole" as const, + secrets: [ + { + kind: "apiKey" as const, + value: "secret", + }, + ], + }; + + const dbSecrets = [ + { + kind: "username" as const, + value: "dbUsername.encrypted" as const, + }, + { + kind: "password" as const, + value: "dbPassword.encrypted" as const, + }, + ]; + + // Act + await testConnectionAsync(integration, dbSecrets); + + // Assert + expect(factorySpy).toHaveBeenCalledWith("piHole", { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + decryptedSecrets: [ + expect.objectContaining({ + kind: "apiKey", + value: "secret", + }), + ], + }); + }); + + test("with input of null form apiKey and db secrets for username and password it should use db username and password when both is allowed", async () => { + // Arrange + const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind"); + const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); + factorySpy.mockReturnValue({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegration); + optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]); + + const integration = { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + kind: "piHole" as const, + secrets: [ + { + kind: "apiKey" as const, + value: null, + }, + ], + }; + + const dbSecrets = [ + { + kind: "username" as const, + value: "dbUsername.encrypted" as const, + }, + { + kind: "password" as const, + value: "dbPassword.encrypted" as const, + }, + ]; + + // Act + await testConnectionAsync(integration, dbSecrets); + + // Assert + expect(factorySpy).toHaveBeenCalledWith("piHole", { + id: "new", + name: "Pi Hole", + url: "http://pi.hole", + decryptedSecrets: [ + expect.objectContaining({ + kind: "username", + value: "dbUsername", + }), + expect.objectContaining({ + kind: "password", + value: "dbPassword", + }), + ], + }); + }); +}); diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 0248720d3..3969b204a 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -10,6 +10,7 @@ import { initTRPC, TRPCError } from "@trpc/server"; import superjson from "superjson"; import type { Session } from "@homarr/auth"; +import { FlattenError } from "@homarr/common"; import { db } from "@homarr/db"; import type { GroupPermissionKey } from "@homarr/definitions"; import { logger } from "@homarr/log"; @@ -52,6 +53,7 @@ const t = initTRPC.context().create({ data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, + error: error.cause instanceof FlattenError ? error.cause.flatten() : null, }, }), }); diff --git a/packages/common/src/error.ts b/packages/common/src/error.ts index ddc5f93ea..96adb35e0 100644 --- a/packages/common/src/error.ts +++ b/packages/common/src/error.ts @@ -9,3 +9,16 @@ export const extractErrorMessage = (error: unknown) => { return "Unknown error"; }; + +export abstract class FlattenError extends Error { + constructor( + message: string, + private flattenResult: Record, + ) { + super(message); + } + + public flatten(): Record { + return this.flattenResult; + } +} diff --git a/packages/common/src/url.ts b/packages/common/src/url.ts index f99fe099c..5c46343ff 100644 --- a/packages/common/src/url.ts +++ b/packages/common/src/url.ts @@ -1,5 +1,9 @@ export const appendPath = (url: URL | string, path: string) => { const newUrl = new URL(url); - newUrl.pathname += path; + newUrl.pathname = removeTrailingSlash(newUrl.pathname) + path; return newUrl; }; + +const removeTrailingSlash = (path: string) => { + return path.at(-1) === "/" ? path.substring(0, path.length - 1) : path; +}; diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index d53a9ec30..22692da00 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -141,7 +141,9 @@ export const integrationSecrets = mysqlTable( { kind: varchar("kind", { length: 16 }).$type().notNull(), value: text("value").$type<`${string}.${string}`>().notNull(), - updatedAt: timestamp("updated_at").notNull(), + updatedAt: timestamp("updated_at") + .$onUpdateFn(() => new Date()) + .notNull(), integrationId: varchar("integration_id", { length: 64 }) .notNull() .references(() => integrations.id, { onDelete: "cascade" }), diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index f7c3c87e5..ff913f264 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -144,7 +144,9 @@ export const integrationSecrets = sqliteTable( { kind: text("kind").$type().notNull(), value: text("value").$type<`${string}.${string}`>().notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .$onUpdateFn(() => new Date()) + .notNull(), integrationId: text("integration_id") .notNull() .references(() => integrations.id, { onDelete: "cascade" }), diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 939b278e8..87b4f5ed9 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -4,6 +4,7 @@ "version": "0.1.0", "exports": { ".": "./index.ts", + "./client": "./src/client.ts", "./types": "./src/types.ts" }, "typesVersions": { @@ -25,7 +26,8 @@ "@homarr/definitions": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@homarr/common": "workspace:^0.1.0" + "@homarr/common": "workspace:^0.1.0", + "@homarr/translation": "workspace:^0.1.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts new file mode 100644 index 000000000..50aee2be8 --- /dev/null +++ b/packages/integrations/src/base/creator.ts @@ -0,0 +1,16 @@ +import type { IntegrationKind } from "@homarr/definitions"; + +import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration"; +import { PiHoleIntegration } from "../pi-hole/pi-hole-integration"; +import type { IntegrationInput } from "./integration"; + +export const integrationCreatorByKind = (kind: IntegrationKind, integration: IntegrationInput) => { + switch (kind) { + case "piHole": + return new PiHoleIntegration(integration); + case "homeAssistant": + return new HomeAssistantIntegration(integration); + default: + throw new Error(`Unknown integration kind ${kind}`); + } +}; diff --git a/packages/integrations/src/base/integration.ts b/packages/integrations/src/base/integration.ts index e8612736a..69b346e23 100644 --- a/packages/integrations/src/base/integration.ts +++ b/packages/integrations/src/base/integration.ts @@ -1,16 +1,25 @@ +import { extractErrorMessage } from "@homarr/common"; import type { IntegrationSecretKind } from "@homarr/definitions"; +import { logger } from "@homarr/log"; +import type { TranslationObject } from "@homarr/translation"; +import { z } from "@homarr/validation"; +import { IntegrationTestConnectionError } from "./test-connection-error"; import type { IntegrationSecret } from "./types"; +const causeSchema = z.object({ + code: z.string(), +}); + +export interface IntegrationInput { + id: string; + name: string; + url: string; + decryptedSecrets: IntegrationSecret[]; +} + export abstract class Integration { - constructor( - protected integration: { - id: string; - name: string; - url: string; - decryptedSecrets: IntegrationSecret[]; - }, - ) {} + constructor(protected integration: IntegrationInput) {} protected getSecretValue(kind: IntegrationSecretKind) { const secret = this.integration.decryptedSecrets.find((secret) => secret.kind === kind); @@ -19,4 +28,87 @@ export abstract class Integration { } return secret.value; } + + /** + * Test the connection to the integration + * @throws {IntegrationTestConnectionError} if the connection fails + */ + public abstract testConnectionAsync(): Promise; + + protected async handleTestConnectionResponseAsync({ + queryFunctionAsync, + handleResponseAsync, + }: { + queryFunctionAsync: () => Promise; + handleResponseAsync?: (response: Response) => Promise; + }): Promise { + const response = await queryFunctionAsync().catch((error) => { + if (error instanceof Error) { + const cause = causeSchema.safeParse(error.cause); + if (!cause.success) { + logger.error("Failed to test connection", error); + throw new IntegrationTestConnectionError("commonError", extractErrorMessage(error)); + } + + if (cause.data.code === "ENOTFOUND") { + logger.error("Failed to test connection: Domain not found"); + throw new IntegrationTestConnectionError("domainNotFound"); + } + + if (cause.data.code === "ECONNREFUSED") { + logger.error("Failed to test connection: Connection refused"); + throw new IntegrationTestConnectionError("connectionRefused"); + } + + if (cause.data.code === "ECONNABORTED") { + logger.error("Failed to test connection: Connection aborted"); + throw new IntegrationTestConnectionError("connectionAborted"); + } + } + + logger.error("Failed to test connection", error); + + throw new IntegrationTestConnectionError("commonError", extractErrorMessage(error)); + }); + + if (response.status >= 400) { + logger.error(`Failed to test connection with status code ${response.status}`); + + throwErrorByStatusCode(response.status); + } + + await handleResponseAsync?.(response); + } } + +export interface TestConnectionError { + key: Exclude; + message?: string; +} +export type TestConnectionResult = + | { + success: false; + error: TestConnectionError; + } + | { + success: true; + }; + +const throwErrorByStatusCode = (statusCode: number) => { + switch (statusCode) { + case 400: + throw new IntegrationTestConnectionError("badRequest"); + case 401: + throw new IntegrationTestConnectionError("unauthorized"); + case 403: + throw new IntegrationTestConnectionError("forbidden"); + case 404: + throw new IntegrationTestConnectionError("notFound"); + case 500: + throw new IntegrationTestConnectionError("internalServerError"); + case 503: + throw new IntegrationTestConnectionError("serviceUnavailable"); + default: + throw new IntegrationTestConnectionError("commonError"); + } +}; diff --git a/packages/integrations/src/base/test-connection-error.ts b/packages/integrations/src/base/test-connection-error.ts new file mode 100644 index 000000000..8a8067f0e --- /dev/null +++ b/packages/integrations/src/base/test-connection-error.ts @@ -0,0 +1,26 @@ +import { FlattenError } from "@homarr/common"; +import { z } from "@homarr/validation"; + +import type { TestConnectionError } from "./integration"; + +export class IntegrationTestConnectionError extends FlattenError { + constructor( + public key: TestConnectionError["key"], + public detailMessage?: string, + ) { + super("Checking integration connection failed", { key, message: detailMessage }); + } +} + +const schema = z.object({ + key: z.custom((value) => z.string().parse(value)), + message: z.string().optional(), +}); +export const convertIntegrationTestConnectionError = (error: unknown) => { + const result = schema.safeParse(error); + if (!result.success) { + return; + } + + return result.data; +}; diff --git a/packages/integrations/src/client.ts b/packages/integrations/src/client.ts new file mode 100644 index 000000000..d62df98bb --- /dev/null +++ b/packages/integrations/src/client.ts @@ -0,0 +1 @@ +export { convertIntegrationTestConnectionError } from "./base/test-connection-error"; diff --git a/packages/integrations/src/homeassistant/homeassistant-integration.ts b/packages/integrations/src/homeassistant/homeassistant-integration.ts index 267b3b957..47f10ce3e 100644 --- a/packages/integrations/src/homeassistant/homeassistant-integration.ts +++ b/packages/integrations/src/homeassistant/homeassistant-integration.ts @@ -5,13 +5,9 @@ import { Integration } from "../base/integration"; import { entityStateSchema } from "./homeassistant-types"; export class HomeAssistantIntegration extends Integration { - async getEntityStateAsync(entityId: string) { + public async getEntityStateAsync(entityId: string) { try { - const response = await fetch(appendPath(this.integration.url, `/states/${entityId}`), { - headers: { - Authorization: `Bearer ${this.getSecretValue("apiKey")}`, - }, - }); + const response = await this.getAsync(`/api/states/${entityId}`); const body = (await response.json()) as unknown; if (!response.ok) { logger.warn(`Response did not indicate success`); @@ -29,17 +25,12 @@ export class HomeAssistantIntegration extends Integration { } } - async triggerAutomationAsync(entityId: string) { + public async triggerAutomationAsync(entityId: string) { try { - const response = await fetch(appendPath(this.integration.url, "/services/automation/trigger"), { - headers: { - Authorization: `Bearer ${this.getSecretValue("apiKey")}`, - }, - body: JSON.stringify({ - entity_id: entityId, - }), - method: "POST", + const response = await this.postAsync("/api/services/automation/trigger", { + entity_id: entityId, }); + return response.ok; } catch (err) { logger.error(`Failed to fetch from '${this.integration.url}': ${err as string}`); @@ -53,21 +44,61 @@ export class HomeAssistantIntegration extends Integration { * @param entityId - The ID of the entity to toggle. * @returns A boolean indicating whether the toggle action was successful. */ - async triggerToggleAsync(entityId: string) { + public async triggerToggleAsync(entityId: string) { try { - const response = await fetch(appendPath(this.integration.url, "/services/homeassistant/toggle"), { - headers: { - Authorization: `Bearer ${this.getSecretValue("apiKey")}`, - }, - body: JSON.stringify({ - entity_id: entityId, - }), - method: "POST", + const response = await this.postAsync("/api/services/homeassistant/toggle", { + entity_id: entityId, }); + return response.ok; } catch (err) { logger.error(`Failed to fetch from '${this.integration.url}': ${err as string}`); return false; } } + + public async testConnectionAsync(): Promise { + await super.handleTestConnectionResponseAsync({ + queryFunctionAsync: async () => { + return await this.getAsync("/api/config"); + }, + }); + } + + /** + * Makes a GET request to the Home Assistant API. + * It includes the authorization header with the API key. + * @param path full path to the API endpoint + * @returns the response from the API + */ + private async getAsync(path: `/api/${string}`) { + return await fetch(appendPath(this.integration.url, path), { + headers: this.getAuthHeaders(), + }); + } + + /** + * Makes a POST request to the Home Assistant API. + * It includes the authorization header with the API key. + * @param path full path to the API endpoint + * @param body the body of the request + * @returns the response from the API + */ + private async postAsync(path: `/api/${string}`, body: Record) { + return await fetch(appendPath(this.integration.url, path), { + headers: this.getAuthHeaders(), + body: JSON.stringify(body), + method: "POST", + }); + } + + /** + * Returns the headers required for authorization. + * @returns the authorization headers + */ + private getAuthHeaders() { + return { + Authorization: `Bearer ${this.getSecretValue("apiKey")}`, + }; + } } diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index 1804d2124..fd241bae3 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -1,2 +1,7 @@ +// General integrations export { PiHoleIntegration } from "./pi-hole/pi-hole-integration"; export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration"; + +// Helpers +export { IntegrationTestConnectionError } from "./base/test-connection-error"; +export { integrationCreatorByKind } from "./base/creator"; diff --git a/packages/integrations/src/pi-hole/pi-hole-integration.ts b/packages/integrations/src/pi-hole/pi-hole-integration.ts index 4b309c07b..c0c61a26c 100644 --- a/packages/integrations/src/pi-hole/pi-hole-integration.ts +++ b/packages/integrations/src/pi-hole/pi-hole-integration.ts @@ -1,10 +1,11 @@ import { Integration } from "../base/integration"; +import { IntegrationTestConnectionError } from "../base/test-connection-error"; import type { DnsHoleSummaryIntegration } from "../interfaces/dns-hole-summary/dns-hole-summary-integration"; import type { DnsHoleSummary } from "../interfaces/dns-hole-summary/dns-hole-summary-types"; import { summaryResponseSchema } from "./pi-hole-types"; export class PiHoleIntegration extends Integration implements DnsHoleSummaryIntegration { - async getSummaryAsync(): Promise { + public async getSummaryAsync(): Promise { const apiKey = super.getSecretValue("apiKey"); const response = await fetch(`${this.integration.url}/admin/api.php?summaryRaw&auth=${apiKey}`); if (!response.ok) { @@ -28,4 +29,24 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte dnsQueriesToday: result.data.dns_queries_today, }; } + + public async testConnectionAsync(): Promise { + const apiKey = super.getSecretValue("apiKey"); + + await super.handleTestConnectionResponseAsync({ + queryFunctionAsync: async () => { + return await fetch(`${this.integration.url}/admin/api.php?status&auth=${apiKey}`); + }, + handleResponseAsync: async (response) => { + try { + const result = (await response.json()) as unknown; + if (typeof result === "object" && result !== null && "status" in result) return; + } catch (error) { + throw new IntegrationTestConnectionError("invalidJson"); + } + + throw new IntegrationTestConnectionError("invalidCredentials"); + }, + }); + } } diff --git a/packages/integrations/test/base.spec.ts b/packages/integrations/test/base.spec.ts new file mode 100644 index 000000000..9ef1f3861 --- /dev/null +++ b/packages/integrations/test/base.spec.ts @@ -0,0 +1,249 @@ +import { describe, expect, test } from "vitest"; + +import { IntegrationTestConnectionError } from "../src"; +import { Integration } from "../src/base/integration"; + +type HandleResponseProps = Parameters[0]; + +class BaseIntegrationMock extends Integration { + public async fakeTestConnectionAsync(props: HandleResponseProps): Promise { + await super.handleTestConnectionResponseAsync(props); + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async testConnectionAsync(): Promise {} +} + +describe("Base integration", () => { + describe("handleTestConnectionResponseAsync", () => { + test("With no cause error should throw IntegrationTestConnectionError with key commonError", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const errorMessage = "The error message"; + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.reject(new Error(errorMessage)); + }, + }; + + // Act + const actPromise = integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actPromise).rejects.toHaveProperty("key", "commonError"); + await expect(actPromise).rejects.toHaveProperty("detailMessage", errorMessage); + }); + + test("With cause ENOTFOUND should throw IntegrationTestConnectionError with key domainNotFound", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.reject(new Error("Error", { cause: { code: "ENOTFOUND" } })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "domainNotFound"); + }); + + test("With cause ENOTFOUND should throw IntegrationTestConnectionError with key connectionRefused", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.reject(new Error("Error", { cause: { code: "ECONNREFUSED" } })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "connectionRefused"); + }); + + test("With cause ENOTFOUND should throw IntegrationTestConnectionError with key connectionAborted", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.reject(new Error("Error", { cause: { code: "ECONNABORTED" } })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "connectionAborted"); + }); + + test("With not handled cause error should throw IntegrationTestConnectionError with key commonError", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const errorMessage = "The error message"; + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.reject(new Error(errorMessage)); + }, + }; + + // Act + const actPromise = integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actPromise).rejects.toHaveProperty("key", "commonError"); + await expect(actPromise).rejects.toHaveProperty("detailMessage", errorMessage); + }); + + test("With response status code 400 should throw IntegrationTestConnectionError with key badRequest", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.resolve(new Response(null, { status: 400 })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "badRequest"); + }); + + test("With response status code 401 should throw IntegrationTestConnectionError with key unauthorized", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.resolve(new Response(null, { status: 401 })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "unauthorized"); + }); + + test("With response status code 403 should throw IntegrationTestConnectionError with key forbidden", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.resolve(new Response(null, { status: 403 })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "forbidden"); + }); + + test("With response status code 404 should throw IntegrationTestConnectionError with key notFound", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.resolve(new Response(null, { status: 404 })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "notFound"); + }); + + test("With response status code 500 should throw IntegrationTestConnectionError with key internalServerError", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.resolve(new Response(null, { status: 500 })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "internalServerError"); + }); + + test("With response status code 503 should throw IntegrationTestConnectionError with key serviceUnavailable", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.resolve(new Response(null, { status: 503 })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "serviceUnavailable"); + }); + + test("With response status code 418 (or any other unhandled code) should throw IntegrationTestConnectionError with key commonError", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.resolve(new Response(null, { status: 418 })); + }, + }; + + // Act + const actAsync = async () => await integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actAsync()).rejects.toHaveProperty("key", "commonError"); + }); + + test("Errors from handleResponseAsync should be thrown", async () => { + // Arrange + const integration = new BaseIntegrationMock({ id: "id", name: "name", url: "url", decryptedSecrets: [] }); + + const errorMessage = "The error message"; + const props: HandleResponseProps = { + async queryFunctionAsync() { + return await Promise.resolve(new Response(null, { status: 200 })); + }, + async handleResponseAsync() { + return await Promise.reject(new IntegrationTestConnectionError("commonError", errorMessage)); + }, + }; + + // Act + const actPromise = integration.fakeTestConnectionAsync(props); + + // Assert + await expect(actPromise).rejects.toHaveProperty("key", "commonError"); + await expect(actPromise).rejects.toHaveProperty("detailMessage", errorMessage); + }); + }); +}); diff --git a/packages/integrations/test/home-assistant.spec.ts b/packages/integrations/test/home-assistant.spec.ts new file mode 100644 index 000000000..76d8446e6 --- /dev/null +++ b/packages/integrations/test/home-assistant.spec.ts @@ -0,0 +1,81 @@ +import type { StartedTestContainer } from "testcontainers"; +import { GenericContainer, getContainerRuntimeClient, ImageName, Wait } from "testcontainers"; +import { beforeAll, describe, expect, test } from "vitest"; + +import { HomeAssistantIntegration, IntegrationTestConnectionError } from "../src"; + +const DEFAULT_API_KEY = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkNjQwY2VjNDFjOGU0NGM5YmRlNWQ4ZmFjMjUzYWViZiIsImlhdCI6MTcxODQ3MTE1MSwiZXhwIjoyMDMzODMxMTUxfQ.uQCZ5FZTokipa6N27DtFhLHkwYEXU1LZr0fsVTryL2Q"; +const IMAGE_NAME = "ghcr.io/home-assistant/home-assistant:stable"; + +describe("Home Assistant integration", () => { + beforeAll(async () => { + const containerRuntimeClient = await getContainerRuntimeClient(); + await containerRuntimeClient.image.pull(ImageName.fromString(IMAGE_NAME)); + }, 100_000); + + test("Test connection should work", async () => { + // Arrange + const startedContainer = await prepareHomeAssistantContainerAsync(); + const homeAssistantIntegration = createHomeAssistantIntegration(startedContainer); + + // Act + const actAsync = async () => await homeAssistantIntegration.testConnectionAsync(); + + // Assert + await expect(actAsync()).resolves.not.toThrow(); + + // Cleanup + await startedContainer.stop(); + }, 20_000); // Timeout of 20 seconds + test("Test connection should fail with wrong credentials", async () => { + // Arrange + const startedContainer = await prepareHomeAssistantContainerAsync(); + const homeAssistantIntegration = createHomeAssistantIntegration(startedContainer, "wrong-api-key"); + + // Act + const actAsync = async () => await homeAssistantIntegration.testConnectionAsync(); + + // Assert + await expect(actAsync()).rejects.toThrow(IntegrationTestConnectionError); + + // Cleanup + await startedContainer.stop(); + }, 20_000); // Timeout of 20 seconds +}); + +const prepareHomeAssistantContainerAsync = async () => { + const homeAssistantContainer = createHomeAssistantContainer(); + const startedContainer = await homeAssistantContainer.start(); + + await startedContainer.exec(["unzip", "-o", "/tmp/config.zip", "-d", "/config"]); + await startedContainer.restart(); + return startedContainer; +}; + +const createHomeAssistantContainer = () => { + return new GenericContainer(IMAGE_NAME) + .withCopyFilesToContainer([ + { + source: __dirname + "/volumes/home-assistant-config.zip", + target: "/tmp/config.zip", + }, + ]) + .withPrivilegedMode() + .withExposedPorts(8123) + .withWaitStrategy(Wait.forHttp("/", 8123)); +}; + +const createHomeAssistantIntegration = (container: StartedTestContainer, apiKeyOverride?: string) => { + return new HomeAssistantIntegration({ + id: "1", + decryptedSecrets: [ + { + kind: "apiKey", + value: apiKeyOverride ?? DEFAULT_API_KEY, + }, + ], + name: "Home assistant", + url: `http://${container.getHost()}:${container.getMappedPort(8123)}`, + }); +}; diff --git a/packages/integrations/test/pi-hole.spec.ts b/packages/integrations/test/pi-hole.spec.ts index 829f8f891..23f8ef425 100644 --- a/packages/integrations/test/pi-hole.spec.ts +++ b/packages/integrations/test/pi-hole.spec.ts @@ -25,6 +25,36 @@ describe("Pi-hole integration", () => { // Cleanup await piholeContainer.stop(); }, 20_000); // Timeout of 20 seconds + + test("testConnectionAsync should not throw", async () => { + // Arrange + const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegration(piholeContainer, DEFAULT_API_KEY); + + // Act + const actAsync = async () => await piHoleIntegration.testConnectionAsync(); + + // Assert + await expect(actAsync()).resolves.not.toThrow(); + + // Cleanup + await piholeContainer.stop(); + }, 20_000); // Timeout of 20 seconds + + test("testConnectionAsync should throw with wrong credentials", async () => { + // Arrange + const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegration(piholeContainer, "wrong-api-key"); + + // Act + const actAsync = async () => await piHoleIntegration.testConnectionAsync(); + + // Assert + await expect(actAsync()).rejects.toThrow(); + + // Cleanup + await piholeContainer.stop(); + }, 20_000); // Timeout of 20 seconds }); const createPiHoleContainer = (password: string) => { diff --git a/packages/integrations/test/volumes/home-assistant-config.zip b/packages/integrations/test/volumes/home-assistant-config.zip new file mode 100644 index 0000000000000000000000000000000000000000..e66c5c76b9184eaed62318730b6a7f111899f726 GIT binary patch literal 97693 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
    )uye zt7{HYnFZzG2Raa>jmy}{Oo%KzH;n+Mp+CZ~(ZCzlpM1%H>S7aV`1T0}BG*VrP}Kyw z+Zq_)X9(7bClz2%4bqAPH}sE|ankFW6)uz?9A%aeN;puPAnb!27|iBzgkca6p0}pm zjfpjF7noO_ff&YB(Z$GNTW1*8qQ&U&dkPl_5(d*x`<;C%wNovdpoh|1GIsHsJBQ9S0qc+ZV9lWDzJ+Ozp7x96D&-`~ekcR*jr!~52M%Al zfAn!H?53F=1bf`QSx)`G3oW+r%RaXH53ASl`q>1NzqK40HR&zs=IM|bzNtN_QfBap zLoQ~UGA%M4{~~9Lq*wggDxcIdcXMb=1N~nVlHS;{HhVFx!1(@bKS6}7oZR$ZPHlu? z2_glxY+Zt15m;5{i3x=4SJzysHH(XeVfFor=*Vu?UgMi$T!cz8=)81o+8TedYR(gz z{b^Nj8n#iR8HNRn+PQR9?)ts^1ow5o#FQL#-`rtQRcW^kqssnEJx@p6*0!a-E#vPd z$FPtiw{^$ADScq40}t3CUFnS~8-)9K>=8W*JMy1Ko|%E090(V(O0hb1xf8~eKc^!7 z?=DitR%u&p{YFl%*I$lbhg5LuN4WNYQZph`5k}?pBX%#73Ns>~L5r+P&mn#{8SL0j zj)XxrN$0f+>oAitHKNi%i;0CNbPSVnJ))jLi;U9Ku7p`O;YZ{;GBInqy`h~)b)%J^ zb{8Sdva>o*^NBYuOVTE}n6{kj+lrKyHoHt|-v|$eGLA;xYeqG* z*xF+g{`PdVfi=c1xrDF5iSFc>3ayJ?dJ2=37ShAH>t9uB>LPSUiN`SSVQ5CRwV!QN z9E~J4#N5ENwGUuWdPJ6FjH1=tIPV|<_H+z@GsdpEeB4I)`PJ%Sb)=Tw8AZc{^f4KR zHhqUX{DJSh7&urwVL;B6yU z^qLKap-xCa?V_4FWbKy<{_K^fjCL_<8C!eIh3B8`bsCOF9ynAot}lDbhZ9jFjMa%+ z>-P;eD3<^GGtHs(5Rc50#eK~^zp5d4D^)hw<;(p(_#WGioD|SIfZ3>WbX-4D^iJY7 z{h^!#(`BzlGUf0~sE9#yldS!AOsLgCjtA~dV&oOHUXTKHQUD18R=()E&(^b~QNMDi z1=UCp+JZ!6-1LsvvA{Q~8lwNg2U zo*s1)?0pgRhmjwZ2H8c*(U@(KlV|1NXy3f>n7mVhmn1xx|4kPr^e{B9U=Rz~>Yzeh zosYf|@!G3=NRx<1*?zO9n@O?sou1?BeMdm!$#WcsNv$XSs+Viz!mf>r61?%^<|S?%ZnUt`tAUZ^Y3D1-|xwD?V0 zcj-yV>lN+KJ{dMVRPcI~)sn#x$+xJcW|rvrUsh1Wf|KxwhVgOL{eXtBd&N_e8gT}fWM zCUj&&Mve*^FH{sYyfQ1KP}tOG;F_Mtra1A||yhE7H_G)~MA5F@=t zy++p;7L8A*c8*2F3T;m9K0X{&@~@Cbh{A_;^?vN(`FC_H9B;8}#&#aX`tykbKF+&&dw)g%8hekyPpuSUiDE=5j2 zLtO`ZRhWt4Tvv7PbcR}c_vMex5Sn|HU$=awJKOi?t13=*;f?y$xa{R@DZYN@-HvqR zIm!3zw7ZKQ2HL91=_+^@=6AcBd-%Pyg_O!Ig&)&zy@@@p0ItfLa1}j`?R&D~m&l__ zxZDK}p>yb+^umwd8!x+ZeIMV8`7M_b{^1vqmo!%Sby#xN{VLt`o7--Jr@G$cS*m2E zQn&v$^WdOzw|C>kQS${SM_~``$4HIyEs&?y5NRsaM+S*(v_EhmMT?mbzkJDduyukYKJYQ(MRRY5kE|lnkm5HkxXaX2^H;vF3XY1#?^T zwEUX>A(QW5!~vLKHi5|dQVeJ*z>Ed_XCYW1=jIw$D@5piHNf9 z{Qig-d2tbNP>>sn16sW0XQ5GsR8;QAlY8M*FqfxKZxq@57+%lGjJUruF-5JEELnFWv??Z9v15{h>k9o#5ok~JC-xB zG$_~RO;B6FZ#?floq)g*JqC=QOi*eA_s+oUp>~F)oEQx4^T3%$I=yiA=M8NE7#YvU zwc&LvAHKkXQAGk|Cm`t)7(eP;>}wRZiNg{QMOFWO>Hf1n-xzDyP;f&swt|YJVN38% zT?zkqwZzPHI@MRL1G@zY1%U+x^Z4|}$|#8|Hu}1&gg%2of`olK=kpJcnHReHK|Vfx zJBRx()su}R{>aJ2s&nws^0JC@a+#FIs{7~|iMhzsJFyVcDs!fmUi5VKcMcDdhV^A- zq{ONVX&L!Q={QZ_Bw2(GbaD;2=^@W3ka^E!ZpCICefh?745j?a$>3z=Oth+a`z89kruTS2aWd$XB*EZ;sJBBQ>B z;X)pv$Ub2-W}UvuNZY?P^KlCk_wo@FGa8qu>7=W4lD@iuKXV#HeQ$WYyX?=2njh2j(mc-$dYE#Ai?NO1+UQe9dwW;Ko zc3SX{-Mrk-`JGr=y9*#RckqvW|**?k*0+_c3DU|A&a8^TW`5;9VryfWJTvCPM=t(}^4|NHM|XC=k0G zBSBIeag4&`I3+6NaJ_+l(iwovv8?)mGIcR&8~+9yLmwXM>{|cKSHN(uj!7n;D`1a7 zu7BGr?xMdST!7;T4;f%d0LL33-GJH!#9xme1MC4#o^6|&ba4xSJg2w|I_G)c(|i|+ zHx)1abXrkq%>CB>%qB7FS&WX;U&^|D*1Q^m;-z9{y0xrno>4^Cb*S@Q4m@ec;=c(y8b+11}rZS#C$1D)#A&vtlj z(FqY-d_A8AY>(Lm8xPpmE&kNU))~*TxoSM;3AfLScjmfGN7-6o9URlyS`i%_&DmN( z9UR%&T2UPwVcA*_mD0mg%CHq3GvE`-usFIz0@+>|?v4sPj|f-$0v=X;SNpr}<(Te{ zz1*yb?v8QXk8IscIBRlKt}cHa+{&Tc9pSiHN9IWPm&?{-=;ABnruua=bL7zW>82%1 z%=YOJL`q;BFvN3|Ut_Mm>)o^URdu!Tw;ke~-88Ki1i`?7e-#!$NnC?ONGSirfFX#8 zKlwyR3L+vydUbkr)-36(Mv|RRL94K(bwh)4Bv_q#%Vbw=cZi(_G47eKdo54*m9A_a zag5X1_+*Q5&Su3nWtK-WZ8l|fhcan4Wx$7%?Da)t@ytc_MPf0`q!2LT(QdE4lt*C5^xZMA=gq^VT!Z$Y3R?ocI;ftC}zaDjevveZ^pINA;|`7j2tq+|Nr9iUQZc0v&6 ziY4yl-8I|c*z7|v8ob@zE!n|-Yxrx7AUxK!?TLcb*iXz1c~tOSmwDASeyD?wY~1PG z?dvlbtVguwO-Cy3FnLT``vW6Qk0zs=84oZj$^E61`O({5YXme(*b)BC*lTY#SyAms zkfYyS%>TKtcK#RZQ;@5AtC;JO{fD4HT}SYEF2r;FecS5Lx93OT3&ZQqMTeMqbEKyq zXC@tc@$B%{?K*dh8FvJJU}J;vJ)z{awac>a=nT z(qy4-M=A2nzd}8XNN_pFx!%cI(+tOf5u|XSvmpLHvKl;E76H<-FKp`dW6sG$#Q<|u z5SW~0#-_d}VVfDX@U)+m+v$zB0LR*k_^?Ip*Nl%FWH8NF#rp;<>3ai~8u*yg zedr>&tYO`Z{pXD^!RT({c{U9X-)Dc;8&I5|BDXTh`?Id-dEM}Mba zau<&3y2**dUTO};=mHmg$7fTn+gJO2mjG8y%a;!N_oVT;xxA(sqRv5^-9}@j#Pe!4 z0fs-m^0wmpL$`7>uK-?5`Mz8w-hrP(JEVl+ehpj4jNUr$Q6jg5=;^FxjeM$a%*Gx> zeS`nVCjU;z5w}|VxRZy{4$`xAAH3d9v~&DnZNYVfthwp3rfk{;%|sr5YZLkqf@JyJ z$2PRdXBhWbW{Tr<24oDud)@Km5&X;>QpPr}Ns>D)zwz~c65tboz&x{p+wz#%L(#3Z zi-sA3eoydN(c~%kvvkcOfWIS#mawr%VupfLPuc#kiHeyYHL{b zBiXB2toa;`tINmg?tTs_9mjb1M83IIgBPhkubEtC$FJ2jKW$A>Pth1>Y9ccE2WLZ7 zj2I3g&Ho>nKd-83&4=a$MQPuXN-{U7 z{nl)c)am~FedwaA%b(wKJgiX=||= z#eu)WcYky4D{m}NCYX*GK?W~-ZT&I5@B}t#-MQHhLMV0g#zItDy|IY$d#Py$*X7W} zf=s#GtZe=M`sC6_2mJlva*F0f*5|C1o0Pq$?r6>XID%{G7l5@jPq6ecUkkh5wt6?DqbF-zS|@SnS82BSQ^u zhxQJ|?Kd;*+$C;~z#nAahrO*^rPxNE`9nTLybtUc+bX+BvK%on6uPhDn5;o0hju@N z!>ElzRvaN|-It4iC)ti>A+guThw~#Pd+_V;6NrfFvfu(e!zee&{YTN*6+3opF-W79J5vhY zCEAlmUJAV>27f}t6)l%LV8Op0-JGx0h(jylJV&DvRGjaV6kI>YkQ9-izyc!-Jc{i} zK)wi`Mo3&3tWik$LCOM4A6g=m=pfxnl=&+LmLICAU&%0=K{}NX6>3c=j6vaqUIZpE z$a^EBgwC~0TK7yTrG2r8&h@j0x=2VlvCPn_$Q8JuI5=p|L`|9{N0YSbKK0+c)>LKO za@(YnWtCB~XIEYf5FxD$*b7X=s=kqq1P~?@5|^R7U18rYuVG36|n08s#+mQKiwO1XCF0 zdBMCq#k91dp;;7=V%cr0NWYr4r1MZ!S6Bm%Ca9cPe^860_9wy2W?*;u0? ziE?;?%oqVDM;0KLkOZt~+ZYfIUG>;Uu2F@>_~3CqnS9jkGT^<Ukv5#Je4nLn3OGB$c!?Ou`XJni?gjP)S%>(O{Bo--pJb@D3a9C*DiHxo##uQWa`appijh3Kby~azL5{qha{q{$l(Q1sbh6I=&_K!OI!- zzD4N#9s48o!)gcHl0d!hv}R}nf<5h3R$XePGN5ux1$rCTs;fgg9GPb%Uq@X%LVnA{ zgN?C2*Gm5+-jmOL!lM-_rAQ`))D2}HQWb>ZpS3e=ez&)YaRu)l!QHFs`1^geDmyd7T?T!K`fX(qHLC6kucqohknlj{0bdIX4nCMeZfM%?PK6HHyQI zDrwAsr-XAD@Aq~xzKD#NndOcC4bFUHjjAZW&+s2IIoF|AEZU5yyQfiEzT0=ZrKJhq z7ro1nPGOv-tu0Q_w=$ujX8Vs$=F9P;1gEB<@@IV}Q-&YNPgJHcS-p=fARS?+BZe{; zv4lUHN-1niYw8gQ$??F zhTgd1#bok@=2zB^v8-VzBx&+XwnsOIAcF7%l-+%I{r&SpF8SLa{e1AEyK3Ze=SpmN zqPw;M^CybQ!Ufp;F=EGP#+Da;K5)@)Z3U!@R^r+ziVy6Ta|yDB$|?sEXSUW6cesMr z>BopAwRJ=Mu-j3?h_FnQUf-sezAhtKd5cE~-*6GlzLF)eboRJH1sUpCuFFLnKMY6I zx6-ZhKGbcyH%Fw*uB}4;xcuQ{lXN;VMVDCVLi>gV`oQ(g3mkcSyIEWT+iHG%@rCU# z7TU-n${u{*jHM~xvZ*C)V~h0PviQ1RBoXU6dh6l&MS<}wYvQwo7;iiVlMH?EfeUf<-YYq6hQSIcJH=<8;eqT543?bd~+6gDSM?(n&JlhxdVgy2GKx)64q zf1>@y2(j_q?^A32UZ;q^@;h0K^gjdq9Ud`T;bodH)^wk+BS=gy-zv1Wk#A)*U;d6FN_&J{ z9{#MGN~DznXL^)a0m~wOD1p35>}1>@mQenX<9^+}1P7rPxl$B0m?fdV42qb9aL7g> zKz+KkXfj~g--zGI{*ya^nOpH!-XCc*O8qhUW9L^xA0qs*=c6o7>g@n^r<~T5P0;GV zcXs!Q8vyX}n`s<}I}qtDZK{86#$O zk~Spg16=j!l?WAOR%*PeTS{5VxQg&|H!W*G4~a+wG9tU)D>5=xD6dIc^0ey=&J-H;zc5u2L>kkL`*BZ8d0~p_63Tc zhD18`28yr#sXT$=V}2@kpm@7*8KoM~KQ3GW5(S`t8aV?b5`QWV3MW&l0Eu8>va&A1 z#Klb@qAvKf!f6PkQ|q4-gyp`x-(y=^jS6UC0kNWqGR3(;2$*fL{Fa~wCEzYWG5|bK z0%;m1Qr6M~jaKH6fLa;?z3k*b?T#D+KXh{e_x=O{fg1KUAUsn}AiEC^pr053+>VbA zYLLQ>)w^tp8Xzj&3|FWq7Q^6c!rNLHDQ-EsMIPnaEuYdv`j}ItT zH*$hZ>resDpc-47DM3|?sFP6;8b25hn&$(O1|_Dfjm;IH$1xbYHUx?+4g*oaN672t z!^|D0g36s|M#?3k0m|)M%L#(uhq?1HCAfruLH415dMfT+iv(%L5cq+00A~{~FjdZD zF2NvZZvP^#fm#JNIHV4+Qlk#gMM4(vWJM-Wlq@cA6q^g=?AkF<0OBfW?dz5a0X%qs zyLLNBCy#}6U^IL$aUa(1-aQ>EP8~>V(=c-Gmy3CTG9LQR1qp&H3?u-x#2*z%hh&Od z1{N^Qf(E}GW{PVD7C4|2hM|F@m|cWy^;bd6Wspn(Q4@!lr;Q7IfB~YD7X*3_0t@K+ z&C~k>)_^l?8gOS7AiI3z-qTbzfghZj5L5xf;89Y%C?=|Z6wn(8@lpF+!Zt2*vT0)D zNeS*7h{aYIham1d8OE0OLpq=@3y&xwIH50uDh7e?^v)c8wz}Jf|HmZszwgtc>hFxR z0`pM@;L8;$_~>R8PNt}^jtXx?a&>v~mTc&&YQAb-pG_|pciRq!Px}87qAxQ-Xi^6QXl5VPscy3o!T=YU~(pp?zx4LZd z`ln^Rb*AT7CS@&JqxGJh#z?XkL*U|PE6u7dw;fT6&@NdB&SHbc&m&b7BNsxpt@g9b5E?!k=YIl4=mE7?uVa|% zVXQc@aJ6aVT3;#85D?IIO`kfwF;;s_0H*vE#4>9EtlFp=)S~)?7PI8 znLy!}Ua43?;i^OPacqnhbr7}m_sYBS_xV$H?bP5xVKf>8l^AYfZp$SG`lwq0cax8EDGHDwsKyaR1V- zR766?xUVIsFPO#w+oJsVp-}C^c)=HC#&Xex3f=DTyCBA{8=+o1lFLWBbB5@oF z213mi^dzscwV0hedY@0#_vHn`JxK#;4&OCkH!>qF$Nc!u#|ddcd*!z4>(@dd_Zxb? zOTTUo-)k2g{L+ollHzUl+u|l;jHy(DN5t3tjlnFrq50|^I~;c<8IkU4Epexi;~B>e zbzxiLr2&qyIMnYEdq^`M{F?=M3m`Hw3?na-( zEn~xX@Y9cB97|%?O!=rZ=HDX{xH)b~MQ-oLmy1;`PpW|Xh1-=<@m0D4qb^){cFYkv zZ-?RAE;;HW0OTmxHSWlQXY?>;iP})Pv5!>M^#li%*A3>#iE_D)?Glo8Mjwwt=$=N< zi>Yvd7Vlp3qpd}A0Wijpi?II!zsKcC0mizk|ihWKjz}+6RLKb_9$^ zk-!U597a!z{6EXQf9eVU6#1re1jwmwp0}O?P#3ks%%$Nn`fkVZF@rVi?iDB7(I2`afK7|Bw6Z`*H4l)>$A{?NKXI zV2e1QGjAZrZ1)DXAquMdco-BExcsc!-(NmnIzW{}z0Nn79}ri8+Phq>VAFv=DKTHr zaUdWCNLQhogQAgy{&#uP|Ml4n{MH2~^kX1D=JR75pL!s8KUN(wLi#bd*a6kKEhS(J zwiqUF>tHodz@}q0u;+BjpJyqZHNs1W)cjSDn`X@(%6I@mt8cy47bYDL@;}SNx7?NM z+sP|fc2-Ciupal;2QT+6cfg8V?PLQ=3f1GqpiP{I8wlNU6}N;uL8&%QTsq(G+F#r~ zP4~ETuAiet&t=@gVt5ZXR9>EQ1SMsNGq+M`P}g$=rhD*pMR2!R*eC}xUrvYNdE?2} zUM8z<0oqdF_g>r^=vLt7KxqRM4%p=%Di$dRLUJJSu0b&C-~ZXYkdQ@@t}hzkpS(57 z+VOv;MQ7VWTFjCf?ICSU)r!aEWZ(U+xO^PAU*hKBt=tC`eIf5A+ zpp!shPaxMv?MB(gi^t!_3+Js)OXhndBMW~eyeHTq7l;s6qo@2GiOBdX7eOVWEb1#t z`^S1PWsu7VB3|%;sIb4IUuusjPw*wVHY-<4fsEpp4ALO9$A{Aw&<9&LP%Iz|;5&*v zg5p%sTG2)}jqYJp_>COx3cibZbrxaR5`A2PHrC|TF=v!qaj3>Lk{~|bF$$lIQxz{Y z)(nW#%9yffu%2@B=(T9zl!%@KLOi8KXxP%qWz@dF>Ke+loOIHV4BkAIa(g|%v={Sp z+})2OY8%U6QF_8^U<(gfdlMG7FbVqW77koII3!YvtTIQXiY(KV+Mq`^B1S?cT9$8& z(mpDQl2&3W6m>@Ao*L~IUDazw?wqTOVA+E8I6G@MNyr+C&g>e)hfqP95`}XR5MIB> z*fn59I`sx}Hv1<}1q6kA)888jH;Qv2Zz{h&!K{sW9^3=d-sUB3hwq^rr|aM1df9B# z4Ms7HOJSc?9YBH8LjjIXnzP*ic^ague`fNe(9z`3Xf_T zB@MNaOI=^(xQ{67+Ja>pod#RFJHTl3-n|5yVP)Se@U(P~BocPx^yIb$5fa_A zonrMHlNr{4ZS~_W%4p@60xuRh9MHEp7m& zXUk@}pa;zGFKU-D<}Du+acTn;MdL7|4I{hxm?q8v^oZU$AErCIaI_{oIkxX+$u%WY z2)|bKMKJ`7Xu`635qT;xwCt)+P1{&!X9QT`s{X>bz|cs?Dn7IO>CYfPWkQ5QXS0#v zr^k%c{_d&xrx&;kyq%{dnJ5a2FHPvlfY@14j;EsX^0wYo3m(E%nke^S>fPmJVqF#H zvx+e=VP8`1cl&tm?gKq9qt&pxgjmN&-bz(9S+Vk`vDRrr)9}eIle9tZ9j_2yv=9y5 ziV3VyxTA#smMY=B^!i_m^<5(wWE3S)N;@IkCOI|Mm@%m#T^sW7xDb6c3_NUG=`gd56dZ+dT%uy(mxCED`f%Vl;&)n-RP ze$VS}f??!9rZbabYjKPKR4p>B_NtQrG$#s3&3C;8#ay$9Waig0XMLv_-DDO@%J64+ zeA6ZgFhnlBx4p$)vojdx^?~F3W#=s&8JRg=AsmorWkO(s4|HtxzcD3#78wwQdf1{? zkBlZhW<-_&Ih^q2larH}x=DP3;$)?bH^VE6?0)gSDmGnVGC5jTaLfG%bHvgEC5s)G zh9yYxz#FenFRpgEhTdHCU)r(YVQqS2C$U zWi)W_gjB;F1y4eHbNYiZ=2zc|Xam=5*>hX2Zg`N+MTQ#ibAGtm3X6$ZZ!fg&Zuqbj z{d2+EsBiXHt_q*CJZ%Rfy_>@)f9oPvCN5*-8L@`hk@#4iRb)z^qw=UV60IjmWyM~W zde*QHMTHkJ>EJ5-czSf?=ds!g3+*rcl(StpsOIofqA)! z7j2bN&4Owa-r8g-e93r;)$!|vpM_PVh$P`86QqeGyrhErR=0k)MvRndk%1>+rAmzg zg=j;6q4vp*$QctcMMGe}QL~ZlkkFwZVK<=t&~T7xP-IX^U_oBlU%X$OUyOf5tT0|E zJK<&q-VgdZ^*taxuJ7R(kVr@hza2t5*58!*Ll@uG84w;J(9Eoo-l&>GG=@x^~?D1ik2Ez@S#M>9HIYmtj;X+n9*0y&;abOnC)Y z)Jga2R;^{8uGIF7RAOgnZ|jz9Xg6`5fI7BPzo0e3Xb;Yw<0JlN@!tPaln@_hHzcR8 z7fWk$vK6j6$BnYhm*jyoZ(qBBfoA}V*}gK4qWQUEqvpemiRj*BQZfVAd&Jxr&s=5n zSl@IXI~DE%tCsF1`@s`&HOl$6q@61$+QyRMH?^#|ig&f+%>kZ(0uO9~Rm#3RMWYdMX2wF*NHL11zZ=M)%bgP|ok~wrFeeHSJrs_W~`*5vH^zNp3iVHx?@2*g19UwwT`c69!l z(!lP)JbJkO$YePhl@7AoQ(36t{WeyCb(S73BavFJ%inX+Nturh*KZItp@oV2R(mLE zHSO-y9-d87Biik%1@gMzES)ZW7{3QvkNKsn36DZLpxoXDG-(4FV0xP zwg4XWqds=Tb*>Tfp)1B|6y<{S@bW<27j`*r>7#~)n(ejuA=eSJ`p zqwatAD=KvYYiKIrNtA`>^>du<-MiCb#&=!vbiWqs+o!>uaLNpE4SC&DI>Tfw*9kKe zpS!ch9vD9H?T#&SqdcawYy;FV3ipcJ0|s7xG)LSl4DRwSnj@-!!Yumbm~Yu93@!bB z-YbO!&3!^u&bX;dk&P`@EIj3kB>Ue{i_zR`mWcO`PTr0Kd1OK6 z__*H8&(rH`dM~qdU-UQ_$5XO<6cL>o@)bSR^d)miz>r+}9T=RxFPj}Yri2xjOUGL_ zEZBZ3&o0U>!NmS38l2-GZfqMiMQgQNp+&61Z@voNKJemP6wqvqo9zD~bg8ad}S@YWw?Q&U%=cEhOWN z99L$f@y_)rE-IH?NsE%n`hsfRVUj9zq%NJi=ZCex)f4>34OdlXQm*hkHa^yR2XKo1 zWnB`oG1|=RjU4$f?j$u0sXqZtCFt4c@xofVW< zCa-VEqJap*W&3JS49)AX`?`Zt$68xsCw_#>_%b{^X5O}IZY+)}nKU!s;9H)nd|+M( zx@*KJnhEn8QZ%cpPl42#pQky(cVoLPS9Z8ZK@*QZ9Wv6hafiryvgPOg>XVY)x2;Yv zX@RdZSFowT2DolGUhDGL18^}wMi+%Q7}yN%BgbHR1lafB1vjW@B0nqq|^ zZ=kWa=%2-z(>?A+E0Sj{j{n7(E%;3-p}Tr3D*5IRt~7zyw|swK@!&Z5XiR&IAEV=5 z_FFu@&#r^jlV$+ItT9^aF_-KxTQtv5yyb&4OptmQ_st2p{i5X1bb@Q3H4xA_z#U=ieUvg-q{TGw?xx+ z9_BlY%ccYbe7es`>>d5}v?{q0&`|pbxSy_mn7O(hAD7K2K|vPWRHKG--@VBSlyPw9vzQM0$SB{S%M4p1D;57XIwaAu`_;$BK3t`;3KOY7s zA?;@aeS_Z+^EE#X5MLxD^%(vL`rk{Yhn+mR>STuvM~+R0GT-Oc-l}@ziGxR!fihP} zcBEhiN4``lZ6YinHz0&MSvEm*xlSZn7kY+)%Cfv3?ZA{y?ZQC}c zZJX1!r)?W=+qP}n{rB#@_uu_C<5U*nR7IXBL`I#=e3T_?OVdYwUx}p+VE!KNRj^Dx zAzPXHp8N26Z~ipk67P*o%HlG#YGN!CRNRfI@` z9tt!ws5@c)!|(@dA<`;5s(@)#pA?x1nS+RvVv{1B$eON6;!ad(72+1*_Pf?C+fY$g z+Y7NjiTU`VOHmMwhJs2I2?_SmdI6c0K4v>Trzr=g;k8C z*+Qarpj6#Bip;Z+%x!74@WMO8dwMeN`qeJy^UHnt{iPzp=DJS*~_IzAlsN$Ho6dEoS3eFDFgPNR#ps~KlV|>m2M;c!oSFb_0lBO zE8h-#I<4UccBj~s!X|~?k9qPh3wWHa^EPWHE%-v$>n-kJwYpg+>>^&R(FY^tGx)~9 z)ALE({aMH2sOxujF;banig~~A6abB)vQ@6=U>>#|6Rxndrtkc}nzcMq2Cf6)T!qH8 zm@NuvO5v!q&?!L+A{Bv50u?YA?4!IE0YMVnMt@bIARLtumWN6~a0TE=vl(wTm`mds zp5lBwxC~hInRf6=!@?PJ@JTc5wSqj&6k2Agu^E}-{9ULFU~QowBo;h0=P!YI0YPiT zBt)1uw>+LiwGzW@u3{!=JoaL&rX_5P0><6tP+cwZ1F-}cFdjIHfPk^TJCZ|yA1+j@6h8)-jvWCg2wi3z z1n_ca9566ui+EkzuGG`4OrPjkyi8y6Nnu*B^|&|_G-kAT?~{Y*3lx&WpJ#`0gzXNe z1qQNRtBZV@KJaDXJe2Bu{E4BR@)JRsuQN_d8ID9|@yTd{Qoc6a++Ow-jnk|=WzdCX z?lyO0Kcq>!rk)vjpDx|8^0&YnYTWx;L~1_`#g9(*L`?o^uEnTP;a&bG1mnAtZmY~3 znVizf?RrQX0u6M`+06vKMk9OnaWBUnXJd-Q?+=QPPo!}%%~ElOL~|a#iBp($FK6u9 z#;8LV>+L_M69>)F{|@9*QZsW<@dggQq+bp3(mUPWv!o%(mao5QnEs9ibzH_mYUBV` z2k^dbfKVI0UqcDPp{CysF9rWyS9s4Lh^%pMgo#(D?R-)r@cRuALmK?4wv=e$Tyk3k&0Mq8zV-k^U z3NGOAnFeIOO?|Z!4lLGJa4)mAYUsI$t`5_tb$T~6_&AlSh;fp!85>vdS9b199Hh*l z>$93VdXlDZf>AY)x8u8j>}s!$O*MymIAY&G?UcMqE0sZ1VYvpxI)QuCWvUW_=I?}|bhMjjW|ve=_C zS6UcNBLgN|r)1=$D^T+KCe|j#{JMI+TY0{nLbxywDbEZuSOvy+cX`?_^?#uAzzv_~fT2<7zvshNlj&*v;8>kyOv!>sFj zzu~B4BzS&LR|fe%ulU63zd zmuVDJ`5u23R9%yTAR{3uD=QQ0c9sog8+?yviZ>^F&q9D`y~KU=C0><3Uuc9fY|a9I zbNlpUkpXkLMV1D;SCGJD#7i?7Ik}2V!kaNP^xI4#d44!EF=EzAzV{Df1Tb_Z z>EpL|hE>VgsIlyfRIUCLfPB(PKO2k-N~@jv8}c$v--l*Np)Cw7j#M7&q>lGlG_mjG z_kBfR%$wO>KHCNf&mtjl&!4 zn5$n+Wg6zAdRE59_rQmJtNgm()~JilX>f~x>N%Hj1g+QQVxt(c4sC6{jrhMXJv|)p zYk%q)8_5uGJB{zXYF;~qhn3wdM0p51h(E_Fz;7g-&hF|b-wwPBA-GD%N6Cvnuike) zJ8A-+zXyl6Kf2VlWjZx5Igq@IGaKrLAsxDbiao-oN=i;$-h%ClpHH+h#N%d2AAqU~ zI=()_BK-e@dcP5#?0qyM`){_rH7pvkn1P}!n4$p*$FY z=D>S`iz@xp@G&Vi%chZ89+1MA%0`N`o)AN74nxAb!!By{7!1}>oSKULBRmqomPZ|( zL(>dxg~dtE_5T)J>;bMy+-;_aB~xq;=}an40lrO^;On&;<3DBhgXFGCJxX=~E~;J< zzdvjDmuZ^9w$trBD1~460JQrb;pM$h7 zll@$8R+&~64%O4;*;c|YeT=sICl}b#+@6Ct$OopLgYakZy&PN}dl3A2Cb>a-CixEm zkY2n1wA;fI)%}wuuwR4i0tzx29RM>4l#NaWVS9nGAaLOSpnz|^#>PcY3l>rM^0V!V z#HZVP*)aQQ_!}HUxfQ{u!S-JNqwN47$w5u*69O_zqjS~`30pr ze0m4<1&S|vd-%H--5pjhNcx6Y$KQOHhX>hw&m_x#cn69H>$xw#74>Pjdj*w!5Z*!8 zCWzFgh({Hl(k?iK3V7nK$#MWq)wAZgY1Z5kzF8fJu8r$j`pvI4f0TD0;U^T)ug z%(yaYhAb^|uv6!K)N z6^>@W@EF3}yo~myCssB|B*HQo6FYb!IB2msSYbGff$bdJDx^U>>OeRsaouEE_8Kb( zpep;LCIsvc0}qn|4|hZ3GGlOXA-l_fIH>T*{DO3Jrg1QVyD7EUi8uBqDexDMhQP;% z=CMJ$nThWeCiaJn1Hxy)CNx8hXclEz-s*_-0|J*^??L=_5f{t2HMg44CsFY ze!hU+C`5F4-s;`-1P%)Y4toGYxB}j2;&w>2>&+Gp;0^(T1P67yfZ520bucjB@t5oU z672QG3s`ppvcrVhU;}E6;jgVBa5#VJ<-7FnS_koJ0=7GZy;#Lw>4$7BVz2GMtktGo zDa#?$-yB&>=JOk0=(P|UabnbQq0Cs1ukRb&j_vtRT~{33sd_W z6i4=EfRvELJqTE%26E}tzp5&rNy~tmx=Jl@3&dsDd?v~zAU4zKGxChKr?eXOL<-qQ zqNhZE)|h3{TSY%EV}#i;Rfp{QuOo_Jr=+rx^ur7ERzQa#|Aif2AlaDeVdY)=tGh>$ zQmKsUT#|+9-?>fi=nVlP7$yQd{lvJD>x{T?%n=XAb{>hR#LYtpCPrP>Y*t&gDh64$ zA_?Pv4rQ4K<~*i47P!W^7LAL!^M1wKCEJzy)ww0pi{z*8CqWN-p5#8XZ}^*H>azhX z!Ocaq5~Q!;G$c+E2*0G?{t|x3f3Wh!F7J8c#`Gs|C%unh49fG7+b0(u+7)lhJj>>0 zJY^hZie*BidCN}zD@({cFz`0(G_GXBXS!mkWRxBjX32}iF^onw9 zE~q9vB9n0^>Uq*^CecadN?%K_7cI_#^lSE$hq}R;hLVQ<3Uv?;zY4gDysEWJO|oia z)0yXI;$bquHiEN-n}lnIu>Ne~|NbQWp&M=m>lnaT`S@V+>93M`h(g}-Wk&ufh+F1m??rDyZ~2+$Ke{E=q<9P1b|VGc3|`U5~A(?#bJpBwRr{e zEXI`D>5Q3+747|T)C?IN?=)ia{nCdUqxmtufN5v8Kif?+sn+x=C#E8${cwRxR<;X5 zm1-`e+Gm|VwMEPs{KWb}{$ja$Z%Gv|2YQuKSEXmz_)(9C-?7gu%$EL|f6DU397|Za zS84F`asLd{Vx~&XHF8L#Dt$m)=b|yRmXJf?H83zt@*OYxCC7eO>c=j(``vUKP0Dyq|?1!l75HCSYe=bbcT_&ssE zpsn+}0A+HQ?Yj?W^6280+5$t}C&*QhnbXf_3Y4X=_~XsloF!Y@&yN>NcK_3BQ-SZg z<5AFKrL!$`b>^XLAZxMqqNyHP*}cQz$z;kGbJ;S%s%b*Ze(bPbJqKLwz*fjnZgO8I zA~$KWEp3Kot4(%C$NA9R1-r|N-tCJsG4s)JrsY?;>Gj58tRt5_sC>ySn}H{l{ra~2 zZq@aJ$=2J*zWchB2a3HmzGq?!9i_3V^`wncjmVcIn`c(aiIQ%-&{tY^U^fFYivj%zq&_{oY zHS2oR{1T7n#b)!7_O)^n-|gzph#q^l+L-g9)$|wBa7YWkogHAE`*>ge+b* zd*RQF^w`Nwl{h!h%eJ{f^_HbW+ed_sjJ3kW7#k0B)i9@RUxiF3&LHbsx%yqQ%~w0x zvDHXe}>M`^1FP9El=OlQ0ms&~T%{gYrBKXvK5whi4d685Q=Y+%H)*lV;k~uuG;bm{W|7Sv(gu+1FK@mj^1tO#=N|6Uu3Z*_kFlMSMA}At24HY@^MB&D1 zUObxqkdngp<&=BOuJ4V)$9(-hl}>7&P!qZ3f?vmOd$CrXxe7+{{|BEuD6CoDeac*f?E&Lxdk z^d;9NZJU>(NUu1pXg4MXUvx)-nk>>(L`?b1Bk?1iN7h%&S0MIx%ioHTkONo~a7`fu zLMKVyG7@7+^gpSsLRqA-Nblj7BCK_3X`(X3xC2JCS8Z zumQnYKN;rFd&tP6y;?FOXvm7-8YBfGwb-bRA|fmZm3_eV&n3k=-Sem4>!EU55R3qBlYG>-5x>ONyy zakBA9=T+Zdw#f)UZ<3*e-dXS8)*3Wn@?{RVxZ)MY#x7+P5EGT~JZ#b!D-ZU}p`_141`H-KLw}hbHIaF^VT4OeNre4rppggQh8?b^ z7?3}VDMA~ZfkRuILJc%hKj10r_#VXJenMcgamMbCR{C&JWRC!b>(4FX@aqsmyRaNAGxLXDh6Z~#xF63; zbigPsa%)Ej&?-DJmJy0zjScc|^9cX3f!ZArh)_=IImU@<*2p|J?9lW-Wmsk#%;5$l z`27^3Frjk&35<>D>)^r_t-s-D)ya**H{$&TJDBl74=-^+OehRm>vkF8mRTTfD7~ z7Dynj69Vi56?l8}HxRM+lv0UxAga&eLl%V->@{hRCGiozx9f}-s=N;#E3ci+3}k1!`oWQ5^`XjW`sGgVPRr8-pOkHEN8;uBD*HuDp(pb)p)?vII#f>V zk}yS6-`nq%GK-3J##F2H^<_#BL8BH7zxt$INTIq_O493N_0gBMk1U zHbi1kHUH%)vuCRms{8&M*Q-B3bzRf^nQuAFZn7xz4JbN}yF<~PpEpXZ>Ux3OaxnJV z_m$*5vWYZX9z4!mqx)nb4|dvO&U|OzvfGG&^g?n8I&vGe+5YGWp{A+X)``2+yZ$!v z2CgV`u7CRPt5k%1sRa#TNMtTA=hK1vFv++pX2Pq8GCF)&?cdQfvT6+;hJ8J({sysW4yuVauLu{Z4t zID_c6slkzw+{r?Nsob>LtVB}Mo!%PhH5T7wZ=+ zuF&gYF7+fUXl${nRxOs(7Km0g*5+2J)|(6p|GoVGXCIW;w5MP7O#oJAgyOF;QMD)> zZwhXOE(S$IFd}uDBs!7ajcN44v*~E{!g;CT^}_KeTrBcWmbp-Mu#TR<$RA*tZEl3( zj{`9yZ4zgM>h1$DBPU=x6%P1=F(PB=d4}^wn8V}A6(6(d+*9zyN~lg?vtr)9>c7|d zsloJjsDGdav7S)gN#9w#{$2vQhp|`SD`R!XhH9NOz_p9I2Duvf`vm@H%l@~_r+(re z{N3u1;&g*|BVb`9gEtc3!)T)S5)i}SqPG%Y!%t97u%&_fPQh*9NdQK+z73Gb9gbh{ zIYdA)ZU86P8)J}{WM>N&XghFzG_aOHg5dyd0AgQn)sDErj@nH>fdlnR9c+OI%#6dY zSs5^kXy6?Eo%0wN_sX?AP>cug*m7RyKf`*Wo%FqQ5Er~p2wp*5Ltg`WCewBL4f?g> ztELx5Ps~q19jTjxcINC2=PRl)Iq?6_M@T9$2jf#BMd`g9qbPhhK}&qKfq{MNfS~^* z(w_wWliIY1N{F$NNIa>4J&9(bV7e$~qd>YwW}{^2j=!O0wHpV27Qx<3oAA`->#mDo zXD>g2`xap6S^WQ*<2M2o2R1%m89}I-@Ia)zTQDppH}Qx(>))Z#wv|wOjaYN<77YKY zRgJ^Ba5CKf5(pq=;3B~j^cv9g-Xj#`gJLPc+;#?8tN?|+;PPA(VlyFuuow`sT<0<9 z<>7Im3F_yOd(gBT;*i=y?}H!Z-Ga2No?qZU!Mr1QMQrxv7_--wtS(+ayK;J^=}zFA zJlEYD=Kat5`yWwH>7hRL{bXr8RoRV`fILmH9;D{IF$T@!y)gjhWiK?OxkWn-2Pdjz ziQ|+kdj7eC%qOt^K4IHAnLf6`5OnA8v7k@59XT#_^0kQ8^p?w(dAz%U?n({0yppY^uGtoJ z?`Rg)pMUQL->3ohixwV0i^YFIBD@VL<>q8+!K6;a$&Ccxq6Z^j+f?xuFh|R#t$S4}kc2`3steR= z^KB-TJZ1E_raLu*VW3v36HyV};LcN$A6F+t)S`e-^4Mvu%S^rq@-k9VeaK_em73Qqtm13;D3A$#%I)k)5pR7;fsixz1O;k69d+c;~GPnuEcFpJAi%0GnMOl7YO zrzRXBc5za(v-A|>1#=e(&5^BTTs4%bkwVzVRZ4IFSCMN?6PPUtZ(hLhG)(WLTkez{ z&#+;Q0J+M_T%JF6c1I5|L&YGJRMH7`3v@&lD*mbzQB#(am(G+4{jfgl5=v`smbiKO zT-%r;_FKg^;(A~25+>rc-o36cZJWNJpPGN@y8+~m5bW`~Di=ZL(ivUMMYKxPRz^9y znd6X61#3s?OoDAAHZ1HsQ&fGGShk~>&7TPL1$!Z#Lb11 z9zDV zb}W|9t3YGvaL{L5S1D6vMTPc(9}Qs;^yk|2^`{d0_IuwIu&Xgyb_{j>3ogJ{`gj?f z*uY@=rDzLyH^8)7r~`D1actb>s~H$5sQ!JDn~#QvqO~nYd3QJ;#(xl;TsDRs$tT(% z7iNC)L#ty({OseGvH>)Lztiwgv_&Tvk1LzgujAW1-|=k#V&+oUz`+IX7E9FpUg^VH z6@GTlr-=p^j(_?)S2YYGRilQqj|1eRDuWRi5WSD1L#V~ROGW>ciO}|rJ-=k>>v4*_ zdGPXsyCKv?4O}vV5gL$ql9yl2a^sz&_@-~BAumol0}aB$OoW5JW9WDNl}T4Z?Q!EP zu0ul^s=@3m5Rcz{5@Gi20|+MR?3=6FYZej*ORrZth;UvxE}DOk452rwOv&U z0Vzg{EyzXG?&)(u1OMaRAoVVCR-zMTf5Y|=iOr7JWObw`W;{6lhO1rLmTI%X5aurR zt2Xj+C^suUga(A$cn|ej(azcp^EIow1}@)9@qyz*K<(%?oh|Y3B!DJ+`y|k|5vbPk zEH8}9^TL{El( z#kVpQH@1Nbu@m^wg<;QjfA5|gE$v*>f9c?i(O=%Jc1%u*w;+8dsx_i2mUWe^;ZPau z&wx{-?Nnm$n%5OO{u0&9g7-acEJ8;|FTyyhN)OR7%K-Pift`iy8ROp2xE4BMueu-B z-0|$6A&o#i_qi}E`OAZ$cMN`Wyph;k^~i;B%WnOW80_R_xV=0Jx9Buz8bV3hUco74S3|=YQ#ivc=-I6kX;P zw~JWYwxKu0d2v$`@Ljjd&`1w-t~cGG(?j5@*4yED!^b1jil}~_@Z*cgtEeC>D@hR9 zEop9-;rb?6KekCn-5J#1As^sC(AAISxxnA2*UcZ&leRxQeUwMPqtTsLvY#v`b^r?p zY7X>H+7Xf6hhWBSd2JbIxokylnLBsJ;>MC|t!BlOLC%6$=BMgc=_{*SepMZ7XqEn) z{t7pohLzGSa$TVRkJgz>R&iTtz3gI z6P2OelrZg6iyZIcYl#U+^wpfhLs1++iqCUq0oWLFQ)B))IW=_@1w`_(AOxCq#;HXe zYIc43f?)Y_Xb`_;C0Jn3C$OOxN9g84krg@iIHx8#^*0y_HUi%_xdt6r319bXw(EPk zPe@L%fOkLOxYHmViKk&bIZ*XB)9H&s7RuR<<0TrUlMXB{r}5ij??U36myT~n5)JNV zmxDDIaWa?V_>4dm^l5RcES?+;-@5fCi>5Xy4!>4{&GOAj8GV2IgN^9s#3#_yAOfjU znfG>THVb!k?k-qhI5f!I4@eI+(CfYW=p7?Pou8SX=w`$xkh<51$bED(2=kh!|8g{1 z3hLwvH491)JrGgHGpJh0zJ1M6NGMOZAJEVZQ3C}~cHK~x?WwnuqD@D#@XOu#V-Szz zZE^Cq70|XVDmN|()o9J(K9p*ZkJRbu*aGV(lXD-}Ka2`$dk($~AH(E>h;akRmgNVT z3~WWL+03Yrgln|q;JV`^%<30MBzDKEK>1yMYjWGlNAtD*%-!Efm&B}>uw^37!0B4X*#N=j+5<~sTMXi{&} z(e6as5xB?!@9Sgy>}fnFv)Ahuo~ifS-uK(iX(ukcO}p%Ok8Ji4!%NG8=t$P_VxX=Z0I(^IUGjq)B7~nEcm#s@0wkHz4rZjz9U0!Q-ATJ z?M(%(m$~u3dhOeFBCNFGwFu~|CuVU>9?8&S`P6L$7!W7T_k!(UWCES+20LBlRk$9! z8?*^S+s(BimF8;a!xEh`9m)G#{uZUD4?NAMkJ~(;i%=Kp#bv6&bG~;L!AlHqkDS5> zd7hS>Wx_;iN#7RANEcK{xeP0gyRyNRYiBg6y-X4_%r9OqA6S#JOZ14nr}|1(#bi$E ziDZuJlIM#tzh0ZBPMVfEzv!2vH^aB!vt-!)7Wxffsimtwd3Oc-(%*RiEh_CH``ilP z295Wbtqm*Fw;A_MVd6F=@%|;DFrR8SGV1TU_Ia8x$SOQ~zj*4V*UdA?UzHu4yTW=0 zF-n!*#X^*yL#Aep1A(O`_}%|dMa8f5kGX! z2lFRVy@PIUme-}{_l92DB~3z!RqIK`m!d(smW%6m*>JylN<(LZek3|&XNS+s2EW{8 zMsE6AW83%apPti!NVL%pI}MY{HP+Qkr3bSlZNvVyCgN9_E#@ae7TQpk+ka|m9-%|0 z(r)94FN=AA*-9?PIVODP>^JL4O^-eOQpcxP8zx&RgR##$qN0AT5!4?aj0_32Nh=ci z6PMo=j5kp{InU0UZgvWpGSw04^#vMbP!>yS1GhPW@3C&g8ZE6Jt9o(A@nO7j;b?Uq z)u6~z?Q&a-uvAWc!pmn;=!_qB-_pz)E$73N&%AFRl`FbLsv`FtpY_EJS7co{!}skp zJSXjX<>7m7@7?J`?br5T%+^)&YN1)FRK%~knUxGnT?RP$1UFLB3yXr~;X7_s;R;RC z#^Tj4-)XhqZ%vp=q8vTebxgr7_ZPM;tH#+^nO}783jvku3rp1>9<#qv#}AbT zoqb!pRFKYNZ+Ur$itex4^|z$a=U;T0#|4(~+MK`RS4kdHx+vo*`d)PRhsvx{xo~r| z*)F7KOJ;{fA{F$WbKW=S_S&?ddq*AZ|)U^RD53KM4R$8)4azr)Sq>kDamS1$O%NVxHs_ATM!wAH{K6wvE0jn{>y)hoK#w z#BKcFcH?;D4R+Rk`&)yL_F4L(qn|H}RXy@Xi!izSs>}nV_CaeD#Ww_kiuWQ-YcnR= z?w03$dzd93A~5>C%VTn%_d$YPs#aB-At!cmkJV8Jd>>&3>6|$(xpMq`DoV{;X2yZu zclxD#`x;HM6Svc*s`L%-t7Tx|&f3+m-aWUzN^aMTQtrC`Wb&X#;$%U$25WPOCzsB@ zFC`Nf?5M)-es7oKqe)TZ2c!J|>`h;VH^~fz@K&aA{c|_YJ~~uPG$a)AeQ+2;WGJKp zlO#snH3XJ(0?6HqYv+x>iZ?1QuHmIUZbyQDG3VQCSJSVEQpfUQ&G>b~Pb==`c(XW@ zvZJ)wHPfQ$s`tk_yJP9O24P!qHC?x?)A~Jp z!S4g?tMk~|*pz$`!3MasYsdMNJ*q^-GxY&j%J^ld=ky)*3H#csf?!`0uu+eYp&N(0 zOu8v$IZ%!AV+UpawtY#+!hvr)ulPvxxrSfL|3=UEyW?h6%(C>2!xuMMOqvZgp}?*3F`Ewwp6#xmMde6w&$$*aso1p!@vR0(Y{^-Kt!oc?dz2;u=S zQ?3k=^hmr3!)oq@7z?WWsM>(p9@L)*1*9zzqFoUg5+*4Bo&oEKW_NTxoLqnD8$oxl zZV>umS_jZdeC?3z0Xw7nT9^q)r~b-1tO=;Z(XFwmk*l$%5sa~=u@6&dCdBlJ=%mM4 zk5VVS5=HYKxtuAnY!f`)Y(hMGJmzfQwXmk~#-C_07RwfXbJg=H*4$Qcvk3D`iC=U+ zL|s(8RGVbFblIfYbdM=qQeG0;(g;#gzb6x-61XIDOv+7)O2_)`e0P-{_fQJob(YW%Bc60DFs4_xxC(y1Eg z`OYCrQV@bB{b8R@fBMtMapYXaasIpgIJhynwwm(f|K||r!5-=ES#G)A0 zuDgQ;_0!hyJlsw{BjSeb40LsU*0~5X`@L|a1Zg)T+h2QeqjlrE0y2X#6I0l^FkCru zlbe0u0`s8{2~uKv=;d?xu>q9af!lGeY1|(!Es9Wl@6*X_ma^v6v5VLd2GdR2rX{#2 zXNY~#8pl`^S0e(`i#p+s%msSPTjY206cX zXVanuB0>spy|s2v(cDdJR4jMk9j4iGXwg%~hNKr(;4_u}+rPMtRjw%PNQ22$JuNU=6H(`Z%HxmorH}uq-TVW)YAqe`9k2fxzrK*x7Bvy6+_8e#`TP; z%e`edA%;%j%Y)QVJNfrZtZ;)P@_YU1$u%5jk7QU#>FHOVHujbC)yL2GY-C(_o6SU0 z@%gFE%0EIgh8vY`%QkfC{Q8&S1|1a7n3tb5nsiZr1ZK{S7iRwyZf1zGLpnHQ#BRSS zLy8~LY*@|_0P=M8#E!eIZ>+Vp5e0R6l!uK|Z}Iuzn#CE;7g-Lwnj@W(5nQ2i=6n|S zl>r$aE{i_MjT-iEKVW#unY6j@4h%Lze{x;4m4iaHyjF=@owsi|A=XMWlp)y95=P419l9!R3puY8PTaE*?~hXs@&HW%hhzaKkYz zxTFrD*SwBuI0`5e_Z`1rps$@UF-ri41bxzGE%B25)uPc@gBi$*V;4JoxhC4(wv7rv zURjOx00_eC`5r;Fuh$SUxzc9y_||{`q8m(y+n+O4u-HVV1ixotbTi+AqL}-h9(MBd zZTL7{pU*8Y<0aj|=qt<0nY%o<2P?aA0#ns=?-|x`iLTs7qJxyV)(IRP7`*Eb9}23W z^mx8z{0?B(05vU?flgBop3esz`ayee?SvZZZh*r&?No}UxD1Dx<%N0cx2&X6yh-?< zY2p*tGqv?8bDXG`!+ofG8$D=py*PBaFHf*o zsHUwrc^#%QL&PCm*uktIsl8(|Rp9T&je}Sw(X=+HD}Zyw&S2?D={CE?ei^O;!v(+Cf2*;SJCeSnR|Q$2rDaMPF|#;s&Pek>b_$Tr~Z z)80*5>-ThAO`I;Zpo6 z@r2)J>>62ZMaD~87Ie?xn!>cCY5$>6_>Bdf38yRkUX|b@tQhS&&|=DrCh?JEpNnMq zT_nQ$Cx}DNhFlFv$_Ron#ew)h~3` zdUDvPc67yH0)@GEpkBS+x}Iv4X_ak}2=;#+t^ezM{bsMIYAum~C5w29%5Jw^HQ!%d zx4>DMob|~ZTo#H|XWv?o1MNT(z2&b>EW~EyR{$u+a1gBg@D==*fI{p5ZiQG1sigqh z@B$Uwl|TxZmw?Og0tMWaXbPC!fGXTYiNOG-SgV7sUM>;EZa5nOw85@kEfGZrcpDM4 zqB8-?0*Xk8C6Y)-$2>4av?kC@j`Db7Q0pI#0Xj&l!i))_)}k;OjiC`iG3F6)jV!9k zSPP?=Gyp=4Bx*^gupq?hq+#%?{uZW!Mj?VmTG6W{qG$(=Mi^NyZ@^eU!4IDDQ4!M?^6Nnr5$53<3JUL51WP^oS&~K*#{Nfa0e}6iF;?KvY1H z0anjh9{&aG#95Af5A5_K)G@H;PxmYTRoun!6ChG?)_M*Dwef4&jIvA_1gisUCXQ0% zf0D;}Vn$>!LD;KlDhO7=21*>I@vf3$NJUa8NFrrPB4CQ;q|ky%k?Es?G0|dZffUim z5rk5{A|&C&lz*TKWTbflDgL5F5lZC@I4F>WGJ*;w?L&zpg)kJ8gwJ8jBZNP&PnA{et0KoCf=hvZK<#`NN(KoZFi z5`@hOLI9?S1i%F7#oe@BX*+>D*vlw&K|BZgX;s5+B0(27{&?=Bmo^IoYDW^ zYT|z>jAwq86Ul7tH~LS$z$afisYXKqvRd0Zs+j52Rj-#&!U_eClZ&opQpmy`hwc9M z|KindYno#mx&$bX;KF~C#pp$MFmwMxegfqUVH@t=@el#^jc}JiGZvjNzJuA1-X^<7 zs&O!<3g(p5CkK`MpRWEubcVJHBR%kr6d*%6AK_3$eIj=8OH+=Y5Njhh zeO$)uAr0h%VHTJhz_MfiqeDP{@>&Y+Pk`T1s7g&0uPZ5c)jugAv;^J0b^JzUV?O6HJBo4s0) zcI5s3uKsT=2>S862&zs6te4up1ZFW#(I^56A`3v4()Yqd5r9S(3lLiYJTR;PabqDu zoNJ;&cmlYZ!&Y@_F|ZjzVV7#r$gBhz|1nU)sx_b;=>w>m!^+fdc-Q+|sWe8P7y@7- zSMaASK&W^0b{a`*aa|o8Q8b455X1=sivIRlisk(+a$rH$=zDUQL(+hQ=fHqM>qS6V z0e-Cw3UU&_x9$tzFhE}K|HV#-iS57vKVnFU%MOZB&x=a{TAvRr-sKNY6XK-V>)jk? ztkTAA+?3EvtyZ@SWmc3qNt@w-gn1 z*9rKQ%zi*v6%^6X!38~0VIzsDG!$$M#N){ffWa%nx#|e1#AsGJjAD2PL=w}C3M6S( z0%r{(B8t<@r6-ulzRb3{r&f=u^!0qLKey~Zw>;gxZ#Una1 z`bVEh8c`6MpyEEaECICajS6fhy;9Ao&+eejT-s9>*B??I6-Rk7L&!?doqEy(cprZZA zgJ|~|_xpQ+1;Ke$Y`OJAbN88#Horx2GW(TSn>#?p!##*b{&KC|jk+Sk>TpW7g8zAD zOf$K01kX$^=gg@d8qF}-t~#a(P=0|s2wxq-634SC(V=#5roN>>eI3iQ)EVHCNgadP z-Z!BIY0SPDW~37OKJU4<6Ub-!soQv$Od#%3U6G6#r2eHMPJPWu`#$*#T98BZigw~$TT=l23@uF$r+dleb`;37PB8kjwtd>XfcVh^`S#B6A zp8TpXBjwbi3pZd(kc`-QX67JmpNKo z@0~KqqzNN!0;)lPFO@>t1Y(OLZrJ%6|bkMM-j8@^ez**!dWrX-U1FW*Lnl29;BC`j`r-{-_#LG8(Dkue=8Z zqa*|=98wu*6`rI}DcwOyWHjy{`_wvb`7g03~bcvXwPKT#=BAm+j zM2HFRBbx^lZEY9iUL)0xw3-W0gV@KT;6w2PYNm@M;I)*TBU zXM_=XAO1IciGwff%EqH44_oYv!Bx6fb5xkr9sy>xCwpJ)jJQy~7pqsF#2K-+(vyEG z^}t)LI%+aW%~<}MZKWsc%CUyZU9VfD`cG$)dX$Z|hERJ^N36NjgLAd=$kZe;BQ+t$ zN>7Nbv@NMp{obfvZquboswRQeN>4m>0YQ_X%cn>hIN|U^sM}l3O&)(Dn~q} zye*tY@m@!%{D{+}F2g)geNi<^g;voepNlDqM&X{?q&s8spT;EfC{K_Ne8;bUP;<^q zm`X(HpqAL_@MZoJfh@_LAeJzU!WlAVLgvqs%mU?AoF!0Bc6RfELIV(F!0;H*8o!Zdf&qEAK?3ja zV3*uLjq-joThMu3dxHCK0Z$jOYlVmwHuf`DT>>400j`(OYekS2GBg&>ZZlJx0^Ba( zmn+~bR1l5AEEXo_Gf&w9>n>n1;UKo_z>TuME!@3kmM#UfMg5y?VAl%ZEi^bR?w9iN zs|19#0wG#};9)@FZ4j`LV9p3Y&M@K5aKPAEk)(2gC_eKaoZ6MYb zU>EvP7rH1dP!VT_uJY_;1#nm)Y)>H8@{midg=gO21ONAa20)khn4#9>uOeyFVJwzUw`~)A|8Hy#&6|?S#rw$ zBU%XJQVzO{s#iZP2&VQ;k)KiNCe=^kulLr|BS{@$KDc!(-_XCZdX3&tC}yhj=IW;P zq3N=G73q}7pgffNEOHo?x1vzIO>tM;_=yPzJBUjpDuL7vwOi~|tfU-CxpiREYVtC2 z6KWV{m~7ac-^(zBWFF1}hLeG1Ro}8=*PJrgV6l|ITA2Lc(;hfgsC+_!El!ebpG?1y z6KO`OD)~2e{P{53vDH~}jkHKgci?!>=Qdb_hz?0S+~@ym>?^>c+Pe3L?gjzrP(Vrn zX#@cYL0Vdn7?>f48M>5`xHL#Ng3{exA|28wEl5c>!vA1=SMGPe|MNRM`aLRL*Tt*d z_rN8Q?2g3DQW@H^e3;61y`%_H4`SxfpooLN+9#UMbn4bSYY<_+&6_hO888k+qa) z{*!{KTs>-30a*ViF8Mf#Z6HDbKdDR1lKmA>KOtJmKn_Kz5iE3#$CJLY^ktyb9X zE)C4fPG_MfzoR%$ItrrS4y|=t1vh?)A zHGPs%=S`{nm%1i=##@l{Zy%{k2OZBw8Ut{Sl>??uVGovh4I_4DUs+N#J+gQiOdY3#(#79@g z5|UX}g`XT+QvQx`UW-XYQGLjuZDbP-vcFswkVC;+_IA6oCjOo^Peys1e4INAFu#~rR zIc|+|GCppMvh-62qFBn$POJpq4{jSlY_@}?6$7PqG`8+b5gOE7^gn-a)O|4JL-|Woh z*W1E#;Tw2`d`_|&H%9TIY_$|CYUy<^q;NB`)H8RaqNz87(G}c97!Ibit#PKO4Ow*bu=VepXKbHdX-c+QZwgy`DB&6l$4Hiw;=O8)1Ai-XvPqFNu0 z9wN9OB(H?*%!is$Y&EQQIMq52d?Xp-tYOPYbP~HugOJszD8!m6-(7r9q3m0;Q$yZs z%*5xMU0%%4I&fGCeYvf^;!udG+&8P1x@Dj1>?WqT!v$IE(yLoD&Y#iq9}v^oTn_Fk z%qE%bwQzf6cjWhr015U}L&J{Z!%H|sUEi-Wy z-ovtoobV(HioE%FdRSu0cPtzI%T_l*x5JluxTH|VhUOT1R<{*6lIJmH_P(kxdjY9o zR(dN?R*>E=7&dz2ulNPF8ns^$2v9g)TRi~igb z?W#+{N}@{qO5#gWRQW`C&mq-Dg0{zlDn>O!u3yvrXKKEpx0~*)+cmlm`MCJ;lj?$| ziiy1poMyzxvw_rV>?{)?4qSX7_1HnUzrX_o#Z#oFbruE78&lgDtTg%kRiGUHX-`cA zkLl?8T13oAF^c9m@NJZNtU)qp-i}N{eBH`bNZO>-o(4(X^Fw>2_*a>RP{_Nja7CV4 z^8r;q1D`vE{~To?Cb2(!+km$n{kc5-JFx(%gD5XdC4xEFlZ}D-ibE{wZl47qtHQ~! zwru4yM!^Y;mY^BTT{k=JGeP{8{uIpR{kh`Zb#2t;Z`9oxwG7*vL1~)g_&qNI=8Nqu z`1~U?lZlQU7mAx3nEV$sRjGrFg<)~u1&Ley4l&`=lb{WBR4i*EdkY_WR%)-o1Sue+ zP%jtRcBLOH;h-kB36j?EuiQP-Vf79o#FwJ+7IG%QT^NKsq{kp12jM#ioFQPsOPyYt z$8f@34=d0sWPg8=1iR{^Y;t5}W&En8Y-*dC0|{7fveanpZR$JXHof!Gafx`%lEp|A znEar#X7uN@zyzS+cTBG{1UQu-O`ucf9*sSmTMlk6} zOiIp&367{P!(hJc^UWv`Qe9xA7IzYV6|*J6<3cZ=ft#{aFYmvcixKm7+F(A5bw?6E z{=NnjOhw&a;C%;TymOx(-Y#Ov_kLn7rcL7-)o6B-L?;XW5?(417)95`!rAuI;eUAj z)$Vd(n_mDyANs5UMHE{!?aWol(be$*Uy`y?Pq+NzuDG}^A56CI(+N}GLJB-)tQIw$IprNe1w z?w$tW4q^LNUoUMamHQO?`{f4qAbN8Vy!bbybhNc8jBJ4a2z7xaU{3Mh7x-RerS}zl zSe*Y2nl3oDDuS+VbO2$G)9DQPrLyUK>m z;@|YYn%P)Qre!(GboUe?WxJ79^494XET9SsW;HW$np?p&5C19w5%io1-ebQ{Gc&zk zB1H0WzA%_7DkU`~Ci->i1O9=`yCa`IlwPWAmcJICre_jMErLQ^J}?Wu{sMf3mSdL{ zmsg-zR4LNYvnRD+_*;#`(}2m*Qj228Vc`6+l0q356k2Gj6PZhc=@WClL$heP2ZwVC z)aKRDo@0twyzM;C^2;$9jzV1S#6*f8vbldHhcA9`#-1goFz!AT*ieVK4F8^_&dV+5 zArdan_V6;aGZZEERVJCpwn|z@*Hp>P+?ls34Y@~bBhzK4W;nQ2?fH7C{vdZ@#+=F%UBO= zrX(A`U?<+!tw(5;I0XjZ)F&QqX}oi~GxGbQ&;b9d+sekrGr`HiBeoHMJu!aO1N%QB zd~ec8mBctE-}K`y=9~a04=#UII&K7ISz?~WJTu)YvCfYR0e{zVJ2c!#cGA|+x_D23 z%Zun?JM?~R$`OF}h$A3~tmO7hOVSP^840hJcqANR8mb`UjRwn&hT_DKE(>Ck5P5w| zJ~Rcs+S|8pKKuNn_q1+Sl0G)*@IrO3T?DgCNZ2@ zdgqJFh*!9jJfSVgYp$ z+Gt9Us8g0rpR}3-UKiQGtbz#caG}K|9Ya$nWxPE!&6> zwRc)A-1VCB+Y|r9aPKp8+~83Mh$M)^<gGfaC^xNR2q7~xg7Kfe|rA4bqNi=$GD zJ(N1(64QlV+?W5dqjbui%*{U~^K8OebLYf1^|x7m*=K)6qh*A3F17|#5nqcYDo#%C z@WcbzQ=Gi{=@U+r6yO2LBcwNhU)Dtve!xo}&YqZ&GRoL;VZMYsW|2LH)D(fEowg|e zN8&C2F2hBr(N9XdZO`@G8|SD;pT0PQP0iDcD{Xc*b{UyYpmEk-<6Al52ehCO#rFPE zW$i~dq%WoTBObh=oe6VV=o7^0d^bKyeHbTZ>NIKXm`qmo*M^{*KaJTVEAHuj{*wDv zlzPp}#gtY&iOr%MQCX!#9A0?O8+4q;;yX$qW6}3b9dw@D=%t8SeX;8C^9Ih!zNpwE z{-&(H1Yr}60yRT){@FfaEZ$8P8T9ugcKTNYLr?GyN3H+doj1=^~ zZUoOAb&`{KQO2VW0!!#Mid+$8OD4NWLXT&d{Xfu_hU7E!d_d;$@1>9GLS0~pXMP<_ zk7Wv4eYhYeG8`lwA=56C8};}NZiGDj1(tZT)Asl=Ph-HPSJ)!cagj5gf%nP+kLRM;7GW9`B0Ep+&VjqV%1Y68x&6??G)}dDB=&6On>pSDoh;B`=U*ucCFGO(hn!l(yze^=> zhlHO#CgWJ)+|s)+p0v=hVJlwJ9p-G{D2aI=lMqw7VQYn<*Ku0fpD&cBp#0J4tD10B z=-W&P_f3Th`3FoaUAhamDTFJsshP32y?*dGT1_v7 z*}tb{*kmuJl3()nXI7lL5GE_P>p!xzuRN{pH7(aQ#}zf(oQ@j2ll(NRpR(!Mll_}78Um{p1u|=d6@+I`p@erxr)nea7PJLjtkZd@bW9`*ompDlC8cjIr_o zuS|Pfjt*mG{W|GA3ybW42#Z$%L_HrLS;0)p1C07rh0#xnl@|GSn5Ag5u$^n%?1EsE zTu!#UYE}jFc9`1dto-qY!3+L-k*KworoIynE9Q>rcIF!}uS#)w(mF0TQN?!PiE)y% z@1uyNA5$diotT*w>uXdj-+qk*+QxA$z3nlLP-lS(!RmEtF9-QJucg zBe{4Q~L?S_m~x?Ox#qJ+Yf6B(PR!5jE9 zRNZsyBnccA`x^wR$7U?)C3ovHCCa2kF!}Nxxq0m_PGRwFKFpUhpAK5GV4~hWk2EQ8 zSvwMvnrteq+(T2p6)fFyDZ@`FIdj1zrpUdhUGEvU65C?ovw0>Y^R^f z;aG9y+ejhj;C+3Cw7e=SMjCvwuM2Al3vb?vJ3nkUQDvy#(kW7tf}ERr8aN<{Pkqi! zc*(+^kAGuZy5)VYdp1c~r3O)sEjWFB$8wrIP`kd4am%e<@Qt^e*B7+Y-2ymr2%U;1 zb4~Ew)fs_q9;73E#Wcp0o3hfPgGhVvhZ}T-*yUR(jP*Wl*xVnKjl7tZNW1bG`A_Q} zwY#$Sxi1{>(qaVK_c0GoducP(_)bAm<=ERghlll_t8c2u@Sb5*ALuvpV3c5N+&&BT z=nXAKvEtS1UoMvK?Vz?ITyHVfVb1Hj!|QjUxHVk#70uO~HrwqhdaZUH??6`kh|<}R z^GzwLp@eeLitx*D+b%Ht6*bd~TOuEB0^jXqGU;OFiQd1REQdp^luIImywkMJc`kH; zO4ot0bE6)ae4%S1M0P<7ru+$Cpu+)1mY;k1bSr0kRC$zOv}&YbO~xTmSRwhH@F
    `l|hRw%Bc1MP65QBZF&slqd^u)lXTIU&lQ5$aAQaI|COkG}ndKwx*)y)8>6jE`e_uvG6u^<{>!6)nUurSn% z?@4!rT2_i${)lv`JQ?#}2eV_-W8C?v+F{y9dICDJ+UB|}+8+zr@*4_h3i|RL3ZCW% zeJS}O(nHom+2a<&*rVBl{i{w&e=DunC?aY2ycKI9Qf=4omgjILu@Yf4%sZ}K*;TF; zi;-A-6$y(aKvwXfFry0JR0~!qkrH7D%-htLWQj3}{ss{%VM9NbB#{?tl`$ei4Nh$2lv4df#PB(GMm`~mMPsooI|e9kRlA?YW5ON2RQumJ6p{`8iJA?nto zOu-Ro(tCy_)m%FIcZ6wua`^W}@x>*iTjuBMnemTOgXfC(c*eP%a7Jrvvas+1d{{h~As zbmoRrynhQxlpC&nkQNJ?cE!HifZ-1osKuGvd~YOwR*3A|iK9}80}>4CSJDm!@oF}I$}lE0R*P06ITaIpBNQgVk+{zRQeWm?QQxtKG^h7^ol-+o z1u-Vo2M7pRVaG<6&+wgQ-Z4pRPEi-m>rxlalw!dIquV-0?+Ybe%%S0TW=Nich`j0O zCALx8F+5*xHxm%jz~U;hfJ@UtloPwt>DB%Z|m)}tM}dYSd- ze_sLv*Lp8BTV{&sNlCHt+?J+FpRES!I4b9IzK<~)DE;E+vX0JxsJCqi(@(q<`KVjh zlajQoNIee^br8|7taqIep5kkGE)@RuEK4w%Z8cOntS4nvsmhJIvfgWE%FOg|Y=@H~ z(z1-jWjHBbS8H~>%o@-7mf@a~!kDA0=(m7KA?omQa@YSEI7N&?x;-aUN>}nxZFEGz9E1qTh)><@D$0m-j}u?*rH7F#^q07BUZK z?rIM%IStIjZv;ELp4+B+=2vAqR+M@4WS)P+IX>uhVVEH=$4WRG({MSxFX5zTKxG?n zNL*%={#swJl(#JW-ld~-iWk2;X+*3xgwrx>PNpVk*=Tl|9oGqU91w-Sg zPKv<8ptiP7jdA>V%)IJzFR4~~J%i|~1ji)lSy%1>sxNiRw=xr)eJWIojx}Go%&@w_ z;a%a&Gr3dY%(KWmt>K1^ChqyutHWIO^;OvCn28>Fedp<3@~ilD>*KK+XLweaO|P50HmpG^nx>TSw0hXZ*#%yI>`nx}jbPdBd`o^ct6W2p?CC|UyUqg!x^S(YlU)jz z0BRP2n7r=WtD*Zj_8Phzo8hbUV7lb)fA;I`6n`nDLqMwe66W zA!0MTI$!;CS+A<`{uknyzS#EL>piD7%Eo>i6@|pja$3YpxJ)J&0$!D>$9@`W*yEx$8OE_|SAh+@zN|{@%Ssz`h@)s;86KfuOI78W6 zK1_;K{Br30>Z=~=rhn8Dz&=sLFsMQ^Q@3#oOF(6NUN56GyS6GWdt;9<1J}!3g z4e9tMV%e-uoAm@P+9#=cXYZ3K#;drzB=bd~j>h^M-is$q5ATh|t#$bY`gGj;4i66- z4Z1IF0_H(eqNH!Gb)8R1aMQ@O$0rF2U-LXekM4grN)QI0XB>7YKwo9jB8?`$Xnplb z=>8!g>8>vsix%N0skzo9o8nySbgSH{+M>yFB6#tQP?3pSCv~Rm-~3aHdHj~`PbuIjuaz^7pu>FI~osZG!4vn$ClN#Ro*TMWZ=6Z9D! zw_T5O3)?1nM8Z=N=~@~cDbFKBk`o@PF;YNyuA0RFQRJm5Eg3T>sIT0O-nzx@#rNiX zkfzYyLHAN7wl6$rbh=Spz5@Kx9geHSBu4&No1wFYVx(CNL;uaB)Rtz4sv%c}vmlzh zw(Ur^W4r7Y9>eFC>e>q0sUMNu{`&H09!6|w;p0y-4{fYiW0Y&jbt_1pUMTQC;$oAN z>7HTzsOULmv|ZncM;w9fzZ%*Q<-#{<7)2n?D3+r0*3L(`;uZZ2i26+G6qFU&ObbAIFBrbZ#lM}s$gV^tyvyG3PMdd?Yf-mr}ov313$VscvBtTx`{4@D?>QUwmC zC3A62j#a;q=8p?ec^lJrIalm2Fcwel6_J@AYoswid6Zymv@f#J;mI)d%&Oo<(9ZKT zJIws7OP5O}3K_rRTZzLX6&Yo@ZA8SfZj|k!aE<-<4Gx5iA0^?o948|>bI@&GD$}x2EKYIX;>0Myz5q*iGrDvS+cn^!LuVO z?#r+*(H>7$5`1QQx(W+$eMy|aPALZB9qB@4UnzXlc&!QU1si|k-h0nVevq}o#(C7v z{)F3i@wIxrL^=s5g+L&)w|vcFLlJUHN^IR-os!#7_|ecVGv3mMD=qg) z&zo1(t^*t7;S3g_9VAr=+*?zMO^+D^nIq-?*mYgrLEq7n<5d<4Pi|D8Lhk+6kt(*# z8r^-oCF)*cP~TP0KUZ1u-O6buI_WuK(}^Yf0Bs~k3UnYfMS-eay>TisB0o#BPTrNh|R{y&r?vIXe zD9rwPPs|^beB9wpZa|N8p#A6xHi3hG^}HWag57o(SK8@?A3b7GY<92$K@_U?*OcZq z+wsx5WF1YXo0CyGhOa35*Fl~rqEEGi%no)cLsf%lt08$Wt2ga=?&fc4w@Et9&%E;_ zbL{@cQc2NCVOe>nD)3HHVv&Vim2X@=&1tQ0o$g;vi4=ENS6;YeGTiFB?G92=Ktd)3 z{jIrnm{B<Y6Ir_{Q~hPUmd{LaQm5hFDeKWhYteb zUE{L{M){l1!5;D_Gz)+pk3%Bb00(IbbWe%b(I*5t$x}*G>Thc7T3AXYThLp<(@#$i&vh3~K%-!<9a+ z#N7J@02%|3p8A?7MA-k%UCugOFQ zoL-m7!2xReC%zoOQx)f-X8|}xKp4?$_@Y1YW^h{@d$5fumjeO}H#9Y}H!?I4;^#5C zYr=DvPXNp(WMU#@W^5$HXToP}%qw6fWWx97NLN9#OjR3|3XJd=P?_c0NQESSNP5jT zxxmgaTLjqjYDS>;|Co|1^*b|JhAaWHS3tY2ek9;6|4|=eZx7=#`bYR&P3cF6Bqd`2 zn*cO2pj~Z0BbgI8{SV&C))8!FWCG^0HiG@B*OdkL_ktJR0x(g)g4g}4_1`D};~^l% zwnlK%YyNemOe}1;Knh?o0NPdByxM*WlX>G0Vg4b*l^PnUTRAYGcOlRKWB%BFA_;*I z{*M~AHa`Z2+FT2gt1!E@dFmVv!1jSg@Du-Q=#oDPU|=}H*5*$OT#eo;z^JYPfUJQ= zcMWVq@yF4*=2>3nHf7c7YL++<}2!P4| zVG5*`U^uLVp6EFU+`0G;sJy}2X9>a=we+1~w1mtqDYo_)1Kd}8+G2qrl z##Ug%ANoU0emK`Z6#rS(G%PT`iUfwZT55kQ{&N-w7y;k-vG_Q^{|*2DGu)L4BA7s+ zD>XU)vnX>}+5Qou|Be5>6oNok_?CagbKU#fSpUrc1k5bHl# zuF}ryN9M2c<&i+JJ=}k=oB`|0zsbSrWCYZ-|J$8^7lA*m`WdJ>t`)=o8~8h0{{*7L suYi9R$G?&PULIfB@~0!+y+Zy`EUPG>0r3n1-3I