From 7e3d885b9b7f621c16d32f2799a742a91296da4c Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 3 Aug 2025 21:43:01 -0400 Subject: [PATCH 01/18] add etsy auth provider --- .../navbar/account-button/provider-icon.tsx | 2 + src/env.js | 4 ++ src/server/auth/custom-providers/etsy.ts | 65 +++++++++++++++++++ src/server/auth/providers.ts | 16 ++++- 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/server/auth/custom-providers/etsy.ts diff --git a/src/app/_components/navbar/account-button/provider-icon.tsx b/src/app/_components/navbar/account-button/provider-icon.tsx index 57c10e8c..a94ba0b1 100644 --- a/src/app/_components/navbar/account-button/provider-icon.tsx +++ b/src/app/_components/navbar/account-button/provider-icon.tsx @@ -7,6 +7,7 @@ import { SiX, SiSpotify, SiStrava, + SiEtsy, } from "@icons-pack/react-simple-icons"; interface Props { @@ -40,6 +41,7 @@ export const AuthProviderIcon: React.FC = ({ provider, className }) => { Notion: SiNotion, Spotify: SiSpotify, Strava: SiStrava, + Etsy: SiEtsy, }[provider] ?? null; return Icon ? : null; diff --git a/src/env.js b/src/env.js index 927ed7f9..55ec3477 100644 --- a/src/env.js +++ b/src/env.js @@ -39,6 +39,10 @@ const createAuthSchema = () => { authSchema.AUTH_SPOTIFY_SECRET = z.string(); } + if (process.env.AUTH_ETSY_ID) { + authSchema.AUTH_ETSY_ID = z.string(); + } + return authSchema; }; diff --git a/src/server/auth/custom-providers/etsy.ts b/src/server/auth/custom-providers/etsy.ts new file mode 100644 index 00000000..13d032bc --- /dev/null +++ b/src/server/auth/custom-providers/etsy.ts @@ -0,0 +1,65 @@ +import { env } from "@/env"; +import type { OAuth2Config, OAuthUserConfig } from "@auth/core/providers"; + +export interface EtsyProfile { + user_id: number; // The numeric ID of a user. Also a valid shop ID. + primary_email?: string | null; // The user's primary email address (nullable). + first_name?: string | null; // The user's first name (nullable). + last_name?: string | null; // The user's last name (nullable). + image_url_75x75?: string | null; // The user's avatar URL (nullable). +} + +export default function EtsyProvider

( + options: OAuthUserConfig

, +): OAuth2Config

{ + return { + id: "etsy", + name: "Etsy", + type: "oauth", + clientId: options.clientId, + clientSecret: options.clientId, + authorization: { + url: "https://www.etsy.com/oauth/connect", + params: { + scope: "email_r", + state: Math.random().toString(36).substring(2, 15), + }, + }, + token: { + url: "https://openapi.etsy.com/v3/public/oauth/token", + params: { + client_id: env.AUTH_ETSY_ID, + }, + }, + client: { token_endpoint_auth_method: "none" }, + userinfo: { + url: "https://openapi.etsy.com/v3/application/users/me", + async request({ tokens }: { tokens: { access_token: string } }) { + const userId = parseInt(tokens.access_token.split(".")[0]!); + + const response = await fetch( + `https://api.etsy.com/v3/application/users/${userId}`, + { + headers: { + "x-api-key": env.AUTH_ETSY_ID, + Authorization: `Bearer ${tokens.access_token}`, + }, + }, + ); + + const user = (await response.json()) as EtsyProfile; + + return user; + }, + }, + profile(profile) { + return { + id: profile.user_id.toString(), + name: profile.first_name, + email: profile.primary_email, + image: profile.image_url_75x75, + }; + }, + options, + }; +} diff --git a/src/server/auth/providers.ts b/src/server/auth/providers.ts index 1530a1c7..b7ca8702 100644 --- a/src/server/auth/providers.ts +++ b/src/server/auth/providers.ts @@ -15,13 +15,17 @@ import SpotifyProvider, { import StravaProvider, { type StravaProfile } from "next-auth/providers/strava"; import CredentialsProvider from "next-auth/providers/credentials"; +import type { EtsyProfile } from "./custom-providers/etsy"; + +import { IS_DEVELOPMENT } from "@/lib/constants"; +import { db } from "../db"; + import type { CredentialInput, CredentialsConfig, OAuthConfig, } from "next-auth/providers"; -import { IS_DEVELOPMENT } from "@/lib/constants"; -import { db } from "../db"; +import EtsyProvider from "./custom-providers/etsy"; export const providers: ( | OAuthConfig @@ -31,6 +35,7 @@ export const providers: ( | OAuthConfig | OAuthConfig | OAuthConfig + | OAuthConfig | CredentialsConfig> )[] = [ ...("AUTH_DISCORD_ID" in env && "AUTH_DISCORD_SECRET" in env @@ -121,6 +126,13 @@ export const providers: ( }), ] : []), + ...("AUTH_ETSY_ID" in env + ? [ + EtsyProvider({ + clientId: env.AUTH_ETSY_ID, + }), + ] + : []), ...(IS_DEVELOPMENT ? [ CredentialsProvider({ From 09fb27ddd61d2bd21653fbfeffec72a5eac40a72 Mon Sep 17 00:00:00 2001 From: Derrick Chen Date: Sun, 10 Aug 2025 23:25:20 -0400 Subject: [PATCH 02/18] etsy api getlisting server ts setup --- src/toolkits/toolkits/Etsy/base.ts | 20 +++++++++++ .../toolkits/Etsy/tools/getListing/base.ts | 17 +++++++++ .../toolkits/Etsy/tools/getListing/server.ts | 35 +++++++++++++++++++ src/toolkits/toolkits/Etsy/tools/tools.ts | 3 ++ src/toolkits/toolkits/shared.ts | 1 + 5 files changed, 76 insertions(+) create mode 100644 src/toolkits/toolkits/Etsy/base.ts create mode 100644 src/toolkits/toolkits/Etsy/tools/getListing/base.ts create mode 100644 src/toolkits/toolkits/Etsy/tools/getListing/server.ts create mode 100644 src/toolkits/toolkits/Etsy/tools/tools.ts diff --git a/src/toolkits/toolkits/Etsy/base.ts b/src/toolkits/toolkits/Etsy/base.ts new file mode 100644 index 00000000..77b90716 --- /dev/null +++ b/src/toolkits/toolkits/Etsy/base.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; +import type { ToolkitConfig } from "@/toolkits/types"; +import { EtsyTools } from "./tools/tools"; +import { getListing } from "./tools/getListing/base"; + + +export const etsyParameters = z.object({ + tokens: z.string().describe("Etsy authorization token"), + listingId: z.string().describe("The ID of the Etsy listing to query"), +}); + +export const baseEtsyToolkitConfig: ToolkitConfig< + EtsyTools, + typeof etsyParameters.shape +> = { + tools: { + [EtsyTools.getListing]: getListing + }, + parameters: etsyParameters, +} \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/base.ts b/src/toolkits/toolkits/Etsy/tools/getListing/base.ts new file mode 100644 index 00000000..e20cb45a --- /dev/null +++ b/src/toolkits/toolkits/Etsy/tools/getListing/base.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; +import { createBaseTool } from "@/toolkits/create-tool"; + +export const getListing = createBaseTool({ + description: "Get details about an Etsy listing", + inputSchema: z.object({ + tokens: z.object({access_tokens: z.string()}).describe("Etsy authorization token"), + listingId: z.string().describe("The ID of the Etsy listing to query"), + }), + outputSchema: z.object({ + title: z.string().describe("The title of the Etsy listing"), + description: z.string().describe("The description of the Etsy listing"), + price: z.number().describe("The price of the Etsy listing"), + currencyCode: z.string().describe("The currency code for the price"), + images: z.array(z.string()).describe("URLs of images for the Etsy listing"), + }), +}); \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts new file mode 100644 index 00000000..8fff406e --- /dev/null +++ b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts @@ -0,0 +1,35 @@ +import type { ServerToolConfig } from "@/toolkits/types"; +import type { getListing } from "./base"; + +export const getListingServerConfig: ServerToolConfig< + typeof getListing.inputSchema.shape + > = { + callback: async ({ tokens, listingId }: { tokens: { access_tokens: string }, listingId: string }) => { + const response = await fetch( + `https://openapi.etsy.com/v3/application/listings/${listingId}`, + { + headers: { + "x-api-key": process.env.AUTH_ETSY_ID || "", + Authorization: `Bearer ${tokens.access_tokens}` + }, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch listing: ${response.statusText}`); + } + + const data = await response.json(); + + return { + title: data.title, + description: data.description, + price: data.price, + currencyCode: data.currency_code, + images: data.images.map((image: any) => ({ + url: image.url_170x135, + fullUrl: image.url_fullxfull, + })), + }; + } +} \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/tools.ts b/src/toolkits/toolkits/Etsy/tools/tools.ts new file mode 100644 index 00000000..c2761dd5 --- /dev/null +++ b/src/toolkits/toolkits/Etsy/tools/tools.ts @@ -0,0 +1,3 @@ +export enum EtsyTools { + getListing = "get-listing", +} \ No newline at end of file diff --git a/src/toolkits/toolkits/shared.ts b/src/toolkits/toolkits/shared.ts index 076cdd1d..cfa5f002 100644 --- a/src/toolkits/toolkits/shared.ts +++ b/src/toolkits/toolkits/shared.ts @@ -30,6 +30,7 @@ export enum Toolkits { E2B = "e2b", Strava = "strava", Spotify = "spotify", + Etsy = "etsy", } export type ServerToolkitNames = { From 9efbd6c148010dcfa84ee4a6861479ffa75d1b4d Mon Sep 17 00:00:00 2001 From: Derrick Chen Date: Sun, 10 Aug 2025 23:35:56 -0400 Subject: [PATCH 03/18] getListing client ui components --- .../toolkits/Etsy/tools/getListing/client.tsx | 21 +++++++++++++++++++ .../toolkits/Etsy/tools/getListing/server.ts | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/toolkits/toolkits/Etsy/tools/getListing/client.tsx diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/client.tsx b/src/toolkits/toolkits/Etsy/tools/getListing/client.tsx new file mode 100644 index 00000000..c3922248 --- /dev/null +++ b/src/toolkits/toolkits/Etsy/tools/getListing/client.tsx @@ -0,0 +1,21 @@ +import type { ClientToolConfig} from "@/toolkits/types"; +import type { getListing } from "./base"; + +export const getListingClientConfig: ClientToolConfig< + typeof getListing.inputSchema.shape, + typeof getListing.outputSchema.shape +> = { + CallComponent: ({ args, isPartial }) => ( +

+ 🔍 + Searching for: {args.listingId} + {isPartial && ...} +
+ ), + ResultComponent: ({ args, result }) => ( +
+

Search Results for: {args.listingId}

+

{result.title}

+
+ ) +}; \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts index 8fff406e..1f7d1e07 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts @@ -31,5 +31,6 @@ export const getListingServerConfig: ServerToolConfig< fullUrl: image.url_fullxfull, })), }; - } + }, + message: (result) => 'Tool completed' } \ No newline at end of file From 25aa5d717673a7c87db0d4f8ef5b2030229efb3c Mon Sep 17 00:00:00 2001 From: Derrick Chen Date: Tue, 12 Aug 2025 23:41:06 -0400 Subject: [PATCH 04/18] finish client and server for general toolkit and add oauth wrapper --- public/icons/etsy.png | Bin 0 -> 47976 bytes src/toolkits/toolkits/Etsy/client.tsx | 34 +++++++++ src/toolkits/toolkits/Etsy/server.ts | 15 ++++ .../toolkits/Etsy/tools/getListing/client.tsx | 1 + src/toolkits/toolkits/Etsy/wrapper.tsx | 65 ++++++++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 public/icons/etsy.png create mode 100644 src/toolkits/toolkits/Etsy/client.tsx create mode 100644 src/toolkits/toolkits/Etsy/server.ts create mode 100644 src/toolkits/toolkits/Etsy/wrapper.tsx diff --git a/public/icons/etsy.png b/public/icons/etsy.png new file mode 100644 index 0000000000000000000000000000000000000000..5ff35c743962ac7c59b74af98834342f50ccee63 GIT binary patch literal 47976 zcmeFYRa97M)&+>WySoH;_u%dp9D;jrCj|Ep+}+*X-66QUy9WXckW^Jmb^rY|^D=qJ zN?7ZD-1{Bd=j?qH#6?7On*ab*h2P7m$gvYBz5II|5F!PTavXddoL7%NS~Mp+Gb3YK z!n+&>(%9;OWv5xgOE>~u_PJ$g$uM&9v3)Gy3|{9+$!q6o^?5ArDV(+J>2`p(=8~5` zICbYSX&tf>fA?VfxoYgYnhxo+#{udlogIGC>6*4Teou>@3-=}HkSH#of zGQJ=zw>RYj>9wP1N39WE&gud_ea(Hxb3l%w5nBSzQx1uOV8Us;$3w1b@tlo zpOYTbexyB(J%Qhkx$#Z$b$V6*e7=WzV*9!JICjT(p;L>GwOX5c`RwE&EE=rwg0 z^ep|rc5?e5`y*mu?6!^6Yx=p}>*hIUVfUfqDC8mQ`FZ~E3IF8wIU><(zHRaOXByRN zm!{7FG)|aN3I`qH*MD18&BWvy-wQbJXJkM?sUcihMxl-D1PqF z^X>9^GK)XfJ?UJnKDr-2w%y4-XlLM;dbVDlKjU4k5#I=oB42uW@Mt`d4_^8+Jv~=} z?^~;{UVEAGWP08{#my6#WT6T1AXWRO(${|SwZIz?rd`R6+b4R0|Cr40iQZ>@&wc=+ zzYqy(%*^?j3KFp4iN$JK#X6*Q7~;?*8o1{f69zGMJ9r zN;4fp=ztNu-nAb_CE;F7Hp#hPRWz~gjRb|`HFKLd`k5Qa2Jl%cm)As}95LFIHx)<5 z6r`FM?AUpcV_}uM){U;HhEO^l^Fc!nV)JFLCmntK^(Df+PFu+Ufvgt{Z3j;Auy+%P zaX;o9G9P-plqT za{7Si$C<`I4i?~Q#=L9p)L)xE7))!N!w?+wJk-*h3|w*f&WE0!vI6r{TaUrU?AlOv z*#d@e8)Y&a*iDm}jk7%J8^5Cv6zwUT!%Yn&UE^%_AquwB0)hfyWE) z<9NIW0)Q4hs`nZIjL)pGHUoiq(WlHZHk+4cHzDax?enM-A?H*RS_X6Gk~+Y#vSkbEhL;7!ZExq!UWg)-st~YC^83ZFEZ+r*8)#-RZd% zbqSI4vZdOO_3R*{gAM!W?yDA2{+8UIw!C}H2@m(}&X-hFeT6DE;Q@JFAPcr}J# zV07GuYKauSh&u`LL|c6L&KK}QRywFOHCUekTM|0sN{hBB;$|LzKu&_rndi`ZyX2w{ z3rH?j5q%*1pgPW;Xv z+jLS$Lk9QwlcN7me~}D<+v}~r-r{C2LOikVi<$0__1~g}P%Ak3Fi{?ROj_(MR@~e^ z=RdsKX^i>p-U-R$T?&+646Aq@{~BM-^NIW-0RLcHOC1gcV1}zUFO4Yv)F;;?>B$$% zI6{m?GL@70)RrX|0KPr<8kandWcxfn>3_$ifQIUb^r>glY&tMLU>pV`7CUf#DW=uR zgO-GCo4jl-Mt2qTvf|d=Ahc=o&296*jBUEm(U8i#r8kH;>){H&kn5HtSA zg3WAK!o%@PU?6iCE~Ov5j6Mt2JT0e;4#_5nT#VDT8oGc;(v7!~xJ`&qdKf0eh_K-E zl+OEjfv#8F?d^$OaMQf9q3N|rW7clNDaKeJGQ%~Nv2y@I->h1;z^%I9$BSS=bPEdH zNThO#X5zKL)6|?rkz}#V1#qzfAs5l#hXG;S zBPuWa>s~;a;4lniG5ztseUK{3KV+?}D!-LKoINdMTmbaoSN_t(dWn$7!nxyL5mYYX z{y8N$n@Daf4SS69SF9KbC_wuq{F$nbRXXJE*aMn>-*(}9q+cm3Uh3s64S@N?-$~27 zC{do^=koBOij2E8_x@Wx6wiZt*Ott~8hgR#LyE2{TGe0H*t%oO z5*V42W-v&*H&d!fL69jmITjg8YR>w)8_j#Gio&M{;q^^}pPp{m$MlM`EKT{h*|Yv< z4)Wx3Iir02jiUO_AEYn71r}^jgR|b7x8t;ZV1PBc1`_y;62$Sr1ej2rV{w$AZu8w3 zw(p?yRalW9XQgkjzeV%jc=Q&$J*!r(;O1A)gH#enNay{ilcccAfNpAJ-bc1~M<}j* ziI2z2gOkP{mGs#~X6Fo7V_wd{tnxWHs4;YGnKdx#_}C5=ibISD^El$u{C!Z5DkFd4@v(b`Yax<9V2 zJ^ygIt<3>5#N60WsK>dq4|()Xo9b*DpQu~8nTO}8?@sHo!wF)C=w>-yi79i*jGkIO z=v|JxIrmPN1dJ8oH(|v{eR30$4$MshMdzDCzwSy!SEx=;^NdWQ6J%&M2Qmcu%f-5LI0E-){^96?@-4qorpTYYN|>dKYewWf)r)|K@M>u?WNfDEG|^~+cvnmNEv z8sXzc!wy7PqZ`L%7PZw1!yLb|k2%xLLJVM~iBoF!R)WYACsMM<-P0)3RN?U>%;X{L zp(GYTNaJ(4*FbDi+KJf3X&ez6`eVq3T0yl|i3-YiV7KqyJBmgwv-HaiH={*DKr?Up zy%Y!ll$jbM8Ta!1atx>bKyRT^1Y8UPDcr>l8T8VU>WYr4pZt>qZoxOKB}-Q%!i=%B z+JS)}66=5yJ|G>-%_!@x2a8fvBbRhPZk(-cz-d-oU?L5^=M$ zVMw=JM8ISbJoh+CvjMG0Ir-^I)oC#djqnK*ZEjWPQ25}WcVL;OL}My<=7%m+>S3p3^KBndo?(1 zq~lXhKR9BVHXK^2wVQNLVkx#~pY6OplDZ7-R;DKXHE1kF9Dn0Bm3)df9&?;peh6@& z?u+8#-TTk#a{P$Du+ividN-II7W7qxC8nc51V9}cOU~J z-}CK)Y>0!13oIAQXc48(YK2h?-jS)t9Q(W~twiOCu!k2P?}L`%bu|Gz5M<%Sb+(Z`R#jEWsF0IQoN`dhvt#6rKRl^)1XRkTfiB9M>-JX=eA|&4phVym88b0$c z&C=^rOjSg9MtEiR0-CAywTZS(7y#5z8?0un?p2)eQ2Tv`05KZd=nZ|HpSknL{3G4mhf&!Hg8a{6n)w8WTn0&UvA@&!jr^uzUkOJ_R7#xhVk; zHp6Mt^kr?!)t6Q3D=fa(Xf+2f=V0X$s` znf7vGFl}h)lb%tB=MeJFj=I;9+?f#d4Yw*%<>kBG-0;>P&$=;5m6L(984p_T=qA&dOP^Pc>5ft@?rf$?Ma-j)LYYT zF=`CRWYz_i7k2ln_g^60nZm~+EghFI^Z(l%^UsdpU9yNUg}Sn0jqz&)V1Ze~8(%sk zBF$U7wH~nfnT1Nz5$||4#-~h*i6eK?YzOM4no4rk?50BQt|4_lSwX{@wlj z`)8E;K$s_n*sUOi`_pPyin=E*ziqqX3{StehXw{|*aykz))zWskl%74Cl7a(;pem!q% zYkPUxLmvJ4OHV=V47f?8l1UJ>L6Z}0_lt`g%?d`0h$Tp#%dZ6TzYatNc>=5`Wxh1hzxy$LsX&My z{oKr+|J#)x+G0;7%2BjKXgsW?m9mJ=i$WA5e2${i*siGi0Ls)=3Q%I96Ri^N9tA8PQwU6iBL zSgfjauZRk(Jo%%6aWb9n;t|+x@8uhk(O;X3JOEDoAME;8HV(sM>Ibf=UT1WrrB<_H z1()3s-iIWDp;|I!BJS(lHQ|Tt$fl3X@B8xh7nU7}2AL{Qf36d0P~NnWIuiY#sr7f! z`uGWtofNoZi1eOUB7%8NWtF`$7m-l6MRiliG@@!HluS&tAp~*{WTh!|ydYz!9Ez)O zv{`-f0N4kHhZ!F}Nx~Ou3%wDjpLihhpNrmqiIK#3;Bi$!CC6LC-g{$Sj(OChJn;8@ zt(uaMLe@MiFKG28U-`wkdHg-_`L|22p^47^qai8qm`KQBH~+>zc~DarMDpQ5Tg%Pz zx_UfLn++?3Zaz-pJ69W;l~h-*k)bUpF57ToM)2LlfO97Sq7y086sLq;p&%p@wY=}1 zbVkkv?re$OieTflN%pgNjT7dQLt)GHoQ_OIRY+Ure{u7Vrvj$7p=In^UR~W}QUJak zZc<^`+{OlW4DkkXPkvO4jbGm}&SyleQ0#vO(NKR6O{kmrkn8^#Ri|OR*wPw;22|$u zgf-&Xyk|KX-r}QekKt%zU$_{}@So#!;OGWE_3)$aR5=QZ6p1rssxj(na>XA5N=K9W z9*ZWiei{ju2T+np3%AzLFQ*lSTV*3Zt08=6uVfBq8cqq9U1vFtBQ$?7XYkWU7WsOT zhWv$K{?(YWrHP<-eVZ=3;`Ua`Vlad54UnEsb#DNvm86fPuT9NmQ^_;4gFO0SNuZn4=yX`5%e*=ghGHr&|PlhNf16pji7DQA@8xQ_yy~w7fD| z7&sL2D-GwvkK=<4vN*mlQxW{+>3(Ro2xbj*Q!+?&oNx0P?05ybf9BLbUm~18Hce@t zWA@~J7Km`%qS^d%Ct;>d*WrkxIHz- zJr+c3pYGv;h5|0(xZg(~@^bhdPFGzFl+y#*Aw%0}Ip@*R{S(xFdxJ@nRPbN(UA)#R zz1Ko(?Y^xFs`rwNcsqTEfu_pm7L-FEo9@8{3;-OdHJMXtn#`MPWs~UywgG_Yp@WiE zl|KG01jhGsB|LrW+{wym=?(E|mSEg*wnIW`Oj%$=r)uqcB`QOaC;fIQA=5 zOZskT69#sF(0V*wZNt{Jm$8%q9|8XizAK%j>4~=z( zv}5CQS?e3qBB1zP%2Jd%<2RCgA!Qy)Z(hGYAAq$juL^g8ekmud?2!C^cU9c21*Nc; zkW_3~0DRtD1P6bAiVY_Rt1}`Y-P4TUIi|_;5~!6$`m5dUcHOjN;ZV)@VwU}(vE>`a z)nq$mNFUO#M85VG;^iB8Y2+TsgCyPo>W&Xl&`MWWo&)if=`$A{2)LBEoh?{MAHU%} zCMO#SzVbfu^WkKbo7;qjrhZ5zv4j7cM07 zep#Pj+C$^pm@wbkr{qdHg0nOP$Tz^!jMfJ_Aqwkpq{mB#F^M&uN9U_Pix1Dkn@$W0HlKV^j93dVCItQ z5wzDHluQXkJNm4Q_5vs^{2lWB7h($2#sd4ZHiNJ)L6uuQIJn=&!V^BeXh9m{HD*e~@(b{Qj7#LPkABZR_*zjERfrGZWf zzzs*|#*gBkn{Ba|CY@V#IH@GA8)sv_As3J+ZDg9Z3!lPij|HJlxO8M~N}O zl;G&&U&o^b15<4Pz<-Xx)B7(+99~*|gwFS_XM0dq@)7sGWJu68O0%PTI9_I->T!wt znb=A~8S}$()cg7sbGMj4u~=|@^Bp9sj!Hwr9YvHm1E`N^kat?5DjZ=nNYD=k*-du{HI9l3-)%0Cqt2&sMW1*lRw%f|Eq3g;uj(?~PLe=AevY*OeAG^7zhu_U`i2 z6n|gA>HB+vuQk=#ARs#r-M+;_I4TV50SGr7WG^F$XPpFCw8DAG!A>AG4wr3gZ-yw z(=9jFxkH2*AEWEUyn}myc@P|LYVfZNb>B}Q+!=r(Pd9rdcl=;~nPrA!JJRRzvPGbD z`9a3NWHRz(W4FNN)SreR11liNqT3VGA`98H&~vMmaF_?RdbXH$v2??#MelVG*n=JQ zzK|EF)SoBe#~IYK5^Rb+1)?9fWk{-b_NY#NC*GbenNrx}R<()UVz9*}OF@v} zFq#JeQFC}VhFsr*NJIvUEfCnYRbR$9=%0Ik4QrpxD(kGK3I-6ELN)eovsWqDgEYF> zb}$Si@c^9rV~QqJQgO)9fxh`@JWeab;)b@oPMhWo_RrZ|#KW*@waO&G73T>~TmTq;a zR;q|7UevL`?!3?zJS5_=u9)oMWnw8V8so4A*S-^GFB6tY!+Q2>@7&_6l(ompvmQ6U znne_~Vq)0mz8ygdho)Y--)@J4{6X9(6*Bf-lwX&PUIUm`-*1JIy=WDvVeE>>)oR=f z|8OWM_hTL47s+RhJIo0+@eR*3gI4d?0zLWQ_`oZwZdqi3P~em%Uc*UWW%UP~pFWs( z{Q3%ex0Bh6)udO)Ibm|qVxWhyc=XNry_OtspRr-|JZ?_6HC~HyVOTXAnBkP8N4llc z0H~;8ZRo+}@2H?=}nM4K(25^-9qh`;UOznu3V%|nfe9N=EWx$1tVxLa(Yg(q8plJCojsC`z(SQ zzWj$GZX0NV)oVW$Z6;dT%{*NJ)r_BOYKouef2zfd?S<6;{L+XACC5^l=u+6hj2JwD zauimU&hlFPF}b7x`lvG-C1~BdVB9}ucGaM|8H)kmDY&v)rdaWekCZ+F(@iQEb%1>+ zZn3NmOsbQ#rTkiF3!oy4!ImwG{s#NeR8^rYvwPWTF3dg$$he4W$7JRtE+z;AuxK~^ zPziU@;DT0GX#&HeD{cD-itoSRjT;O7JWH+C2RZI1nIu>(WLhgL#=_sJ^4Msl77&4q zvWY2F8Yuh;HH_zT-%BT({27~$q2sj$GA73^w#XE>7#@cjdVhj>0(7zUHW7d2o`z|= z1LYE4a2Yx@R;*F>QtUTiT^!V>k1yl&$q^W7UcjDgoANDKO0aOrWpegAYW=nL@8=)4 zvJnv3jTh6)l0PEwk&3p7~SZ2Nn`VeAa6ZwdYtK^H**&n>T zhj^x~*}z!XZVEFh56h-LFuVDXg6X}nq?kD81sZEGDL^-xk3^^YfL9LEJe7c7 z(ut73h3h(=xo2OzLz}vW4iC&!D-9^Q-)eqMWWz)Cu!Fi?J{zD)uP?%CVu0=sQXDeZSLY& zH&U0lWNtp4f&EERvGYVvbnl}#OQVe>7-*affq+vN3nKTje597df9H{O2-h5W4OvMd zDU)GTWKZ_lSsqWt1v9U}0MR&70L09GpGF{6Po`Yr^P?pt0#&5;Uen@Ug$@o?U3QI` z5>L`sLfotN%|jtOZ%|}kQ}+OeL(SU=j@9RhUVq!xMbPn$tB7Y>PsZR*AN(BBP`oUU zgsY?iDvVDLbBdRxfE1)U6$#=tJ;UtcVW~jD@d1}GB0_k#&977XH^g2uZ)1#{o3$jHBmQAa&}$$v zy5VPPrv8Fp(`dlIsAdu2mtNa(;oSfp77tDJWl9MHkMAuRF-dIoFcM8A*u=?C)p>$4 zW*cCdJ@=I#Xacd%#~YmOk9bI3vSjK*_}LoasVK(6C#XR838k^BY1x>> zLC~KnKV{5u65Gw*=Y+Xe{Y25Sv8Topr`gsZN_iP8A8l<+e|zP_O{mO%lzb|PQk6E@ z?dy5$oFn+e>CjY7X@v|kH48LcTCW7M(s>tO{2(n3ecxCcrM4A+yR4Y*1%-w+tgJpZ zutyU-e#(TiB!>$q8+p=Vp8BiteIJXYNw#TvoBxc0O)CT^87E`W1H&Qps09oowN?}H za7Lv#JNy}FAgz*r^P7_Lj(5VdBPxoMt+G-DOA)}BZ!G$>RcvhV&sMo1)NjGC_zkg3 z%Z_I&q9r)j6PBLmG9q@m5E$8tc|LTcxZ8Vh#{0q|$)4q{88fel-yH)-=`v)wrwC58 zl;X&CKd36ti>4JsCka+mrZ`L<6041?lbbKN<#~F_^$KNBHj6j&q%wWXkRPa08ZOH>m>$H8ArL*c8`fO?a!U#XZvV8%Mp zK_8Kp_K_w-;lGB`bI=j&_SaMu=>OVQih?pU>oZ)V{2(43qHjW?S(`rOAlc`?HD5cv zq`2zRQNm67f|W;fT;HlAlyZ7JJ@&@QfY7u5sm4OzF8ouCCGd4O@$f&+r9L@5sGtdT zpU?2(GRBsPCZ!KH-jQ=MZ$FYw-60#z_fIypyzxC66yh(V<%bdnI|&j(sgo$|juUl^ zciQ!-{*n3c4nGkQG(@Zem02_6nodmkDMeAG_HR8*CpiCxDf-<&uk8a&0!2oeQUvlSh5OQ}=#rd~2;zjmCOJE^T!`bJ0$xV8~iG&#`xW zEZa5^ zjoYEl-AU!MFkKx8j+hPUdu;d3Z2&GpguS;eHBI{unwxC8%RCTv!ugoIu0KQcBdDpFzBmxl~`JOc+{T(hIqsWTpkj;o^VsCI8F^zF1it<)9H~ zlH1bJHa%LZ9<+xxV&@Ou@CEDsq8h9*c=2i8ByQhEot6XLdQ{%pg@0x+{~F}2NQW${ zk+KN>;y3Z_lRs42y$qLLaI>`YcPS@-4fp9FMS?&Kp@wPv{os!n(t?1njAoEzlW3Y$ zprsf!kEI!?nH2;7$ho~5;os*FZwADBmTWvNJ}L9s5)tbwefrLx(&McOw_NU4-y^F@ zOWiGAk!@fHk;hl%mweH^ZU*>E`tTxpP)0LHTkkx_eU$sp{{PS3SGiAvEAAu=|V0Iou(f;O(Qo6So=}3SJJ%aKhcR=Rd|~*64tW?9awP z4xu4H47fn8F+t9>*&xCP67D%z1p6-(@9)6<_2P$t_p;2y%AkBR`3>&X_yrF%wI8ch zN|`Tg%F5h6MBFo$-Y|a*PyKS*?*u?-{o8Neq^0~9o2i6Y4F7c6?OQDTof-6Or;(;u z$4vR4X5Umn4&i8jNFXV_+%0hk=}7kebcp}XN>*sAoHaHBXe)4J}mz$L~d|OZAqnM9BvWzEWjE zp#NKidEuJ>#_y-Irx&FQ!t5l7Uqi|=P>FnuQ)fjujW<@)bdljc9(rqAJYdPJ;I^E; zO^mUNbfc1YC>sXv(yv4!rh$e{dYomLu%CTDMQ zhJQcdUy=lZLpBSBKowTx@T-1$M>#@MiuFh6K$5aL6Z5y3)TepuR-VOxrDtBAhd;2m z1~vOtiU134zfo_oh%md1P!AQ)(=Fh>h9Qgli@_QO*Zg!L#RJZ; zxLB$@$rng%BzJpM6B*K$4L&i=eA5pGgcpMA&%FmfpzDM~&b_89zijpnV#B#=x~quH z4v0+W*e1{|=Qteq-KI?jz$Ldj3(UWK-ro^@DW9pxR_#8OV|;hgTBZuqJz(J-VrcCH zMpNTFqdW-iaOX~G`c-^i#LfEUOY@M4hH#m$4e9?$_)J;Fu_GL2ipUWEgD~ADcpY`k zm>06hgw)h*=)5hG7kX|*_B@57jvkWM_+3w5{VFw2`8+-Lt;F~H-DFj8IJi6VDx7@= z83z{?&58fP7uPV-*ptzGVy`dLd767D^bTQNspf%Iz2%ahY|&7>Z(3kFk*S1OcQPoc zxr|Otx0n*mR7Z2fL{m4fiHGT*=@&2r1*7L1 z_^-Fx6UQomFm}-+A3m;2&)^uC4rQ>QZY9=rPk`XW&q+@;O!h|SKa_md6jBLf{7Hw~ zgyj8KE&LY&&L5YoS`ZQ$Qd{5EMe+IbpUVEmW@i5vJ?n1{HbOzKveuUKb301Zx0>fa zs_B1Bp40*X?R|5jgVSH%itStBK9FK%KCQIezsP2=2Y9@@(RUrY2}_46K_s@8=S7F^ z64VZ+lFb;ntTy&DecDV&CG@<{<^}88!IJF4ntZwtr8kVB82T@~5zjp(YDUDx1e2^+ z4AJFyCn4L}s->Jw$$@Jlp@j(5T_HY1JIA^Y^Fg(?f9|U}GBp(5t2qLe^_fI(j>? zK1XI-<<5lU0K_d4iMPsOt;%%yLM$Yho;m|sKgdN(%*<-^Ah{*Nb_IlK3cCm z^F@H1Cp)+l2F;YR0J<3w-9niu5jZ5^xT8x=ADU*{gZn{bqT339CbVzhyuNk$`Gsco zpX`grPu~{p;zFRs-nR~uOUmb~c6=eiX^E6|rn#lq6Um(jG?1Ao^cv>oF;uxQUZ?Ic zPU^q@rQJN>b|h3&`*|5p-Hxlus%r6uisq-x=rmW~;s>(V6_#J(;95{yL~rmH;WmK5 z-K!_=7eKsLsyBbESG&ap1*uO+9(Lv7gIY^sP%JaHWyIQTcBsu%4>xsp=^^WA4zOE9 zEOLR{?OD$)|V{7C<-zk>`NCj=6q>oOjFI=%DvVN=r}$pCU5FHd3Wj1 ze=uNgH%+;KNQl24WeI8d8jgRi!MyC(kYVGYGOo;q-&S^GM*4GlE=0@cXC;|E%mu~- z7FQZl^)C2l0SDk+hPBhgDFn%iz?MaQ7%uLk2$8Ss%5B_pYyQ!)DYG$5M#UqoM6i8} zPcQ}|V0qr28M|$i%L`X_q8{za8oTwQ!@<^wWE@hOv80J)A*XRF@s5cN2R~K9D`sj>kC}p&)Ii&h7r~oC`Qt@G ziAd&!Si#g3f;88R_}QyJ-?d#Y{%Q$cpdBG=iGq6m9}3`?wUZs6yFYhZ%Edr9!Ctl7 z5ZuXkFFQgnOAez--oqZUGY0!Fj4|zS7!NoF3C)!h)OF=0;$4ItU|JHhwt;vvfa_^T7$c-E>(wfJl%=Q*Q%)@&WT9R z%_CExNT2D)t*5YIUL@PBB^aOgDl|T~`CK_Q=H8lgE@of9-p&ZZ-NIU>LBL(Slaeta zFz-#-nda_MUogpq?}wyghPDT^*XCU5uEqH><7MX8i$OQJy@fRUK|&xkAaf>oi02aH z83O&Fw>28Nt_jX(!H6tg!BRUNp(Vn9%m@`ytu>ySyhWRfPqe0WRw3EhhD(BNmCO~)AD-!uHh7`^N;aA>?Pf*G4ax;D4SU{%iyXTK@D;8~HbU-b76 z$}ugkGW%aL?M!>Bh_@vARyK43Xy2!m$iJYbe_Lh(jMx|$O+IZxcOvO}1D;@fA+LI! z#7Gv(zwADtkto-);i}IzH{r05MAK*IB_=w%VgD>f2*9NsBX1 zr@r8GV~~LQX>({sL)R){l{{#cDR-rG2j@dlk0Vi*;hqnd&qBs=rkX$;r)$fn(;{f5 zmK9Yv9MWktB9*g&$pfWOQE%-bxyITFH;WVY+d{*ZR%{p~d&66td28$;wEEam!iJSq-%(aS50q1V83O7QlSJJ0hg0NW{)=sz8*oRIk*;DxaK7Ii{BeVn<9 zHXD(57(;KLASi*+C&Eglq9yO+!1eC*U{o7WAf!Sgd&^Ye3g5XFs-C@)4vA1N%7E_N zZ2r)HSnu=OsxoS5D)4v_N4#v4{Z>qmSN*w~26?($y8eeYqw(=z5{J`*O=7L0oYli* zy;c#t`2&-(EB}(PM%VX+v`CKP&*7SuW9ttDDysJ<^t4!I2W+-+7;e2nVs3g3-CXpI zF^bvAW7s0vpWWu-v{^?bHBb&U`kcWVV++Z<@{(*wepo}H!TO*T0}iHWhM`nMx{emG zfFH(V%7$C<>z%GVo1Bk2lYj6%9jcIv(5zci%gMvLfdVLsv;sn?=#ou0X4a(t z=KWqKZH~>Be<=oD_5o3W{=^`&s%+-JYVMy~PW1LKYnl+$xhlW(jg8IMLW9M7du=Pi zoU~%IaJV(^NIF@;CT!}0O3A}d57)D$N>En`UY9BW%Uj5Zy*b8oLP|I2S-xJT4$-f7 z(}5g=|3LRT*hCtTWtKvV_ff8ntDe_mtX6gZBKqOF7_4(U9>#CR74r!>)rKA&1pa+T zQEm;|%T7gSB{guEn!x_Rn({cwAG7|JM+?kfdli#ifrgvxs*yt@M+gI&2^=rFc;+uoubSk zHy6ccvi5$+6wU=ko|U~y9n;N2Kmf35pE|1R#dJKV$1v?_9>r5}P`7$vx(fszUz%=% zHpW!pR#%sn>XjU8kY?QB_VLxz$;4JSXAr6Uzl@lxeBABhB5J+}bOkT`5huJJs&P`p z%9eOD-hNW7DH=|oO3)hQpfDvL;sxaU87H-tE@E}}CKq}yPMIth;<0R5c&mPHZa}-# z>Q(dB;kX3>crwM1`xw&k4{0-g;^x&vsito#@lREZK)=>O-+;`^(B_Yo!IynE4)p}U z-z$T^S9#Y_JE_`7-H}zsB_^li3R-ugvPLAUe(;)T5zU6WJ|@_gFg2YUZ4Gl88ELts z5P+Ssh)FfzLYoylAtE(%$hM-G2d!Hh-2wez^>K3gX)I&8PA}yV0N)xYMW2)?q_g0U zuX!1~w%)M{MlEbdt_ic-ewtfI^40v<#oNj=}Cp&mnw zxg9Q{fCt7R*-D&KlaM3)xttVnvLEMyg&yn{@+FQN#bSO&f zZaHR#-%Rnf%flDAcWWi{dlCfDoSc?Z^g{Bz$g3nuEHOu$ANGqVo{z0NeegdQL*h86Pv!>SOH1U?EvYurcN$|6^SX8`3nb=>Byj}iTNVe`x%qumn_ zt5L_w?H^p%?VN#@8EXR%LMq2b+WhIG%LdLuwZ;MqT&My7)N^K6D(&n)-X$jSJLjV_4tyvI18QBjuLFu!8gZK z)>r?;U08BJlO1S;wq{*B)%U)g|AUUo9O~lC?Sr(8v`juTqeR8HUg2D-6uQo`m$9jb zMkLJPId1QT18$;q6Sy(Hl+y%AR6q|1T{dlW_M(sYi9>j$Gr#^)*QEQz(jhLu>mPWQ zkHX&!!#wJ)KFG5t#Y>*pf8&@Y%pYZ}9Prbg4IE_RO|{)aEqYSYq?-vmTa&stMng6m zh^THGS414tD)Eicaome*c^0h39mz4&RZGt`20d6o%ITKS16@>d%+K15CKtLUZX1^? zhBfyq+e-fbPyc`8X7W5@`mIgeak+9k^=<+>zBgKs;s5J@-Ok<|z(QGeJT@1ERJofK zwZocPki%s%gIpnj{o0JA$@bc3C}Tl}aL)_#3lg)p=}nPP1g`pY9~1XS(+VPy#&g9Q z0+3@;LgRD?$b&8Uq50@&qewm07PeqYuEUYR_{c5Q!a5dO8Q@@-Ej!as@G4dNw}(eA z@q7(5i2QiP!W6XSl6DGkL49Rf4j&ExlO_~R6yxRl>R}Z&sHJ*{6r&!QO^1t^Gb*)o zfr~L4agigmD>(#w(UVhdFVT5tcE9wj;qnGyd+-=pMu#@f29gJm88s(2SHSF2DrM(x z(c&!Xz|T0gU>Z(HzYkzi(c|=@pu{WY6R2tgzhTL8h>uLGW=aWCwI{q&08~}IH&-vN zb_9U&m)W)dxs_AFn<*Z?v{%vcx%$Sae-n}wrKVAB{VN@>8mQ{VheUeoiFEdm*{Img zq(##i%@6qraE(!k4F#WUa1_UglO!WuYtm%fbPR?&9tTW<0j~8a#2tJz5+m(pcw7$) zXXRj4f|K4ScZTz{o5dmO8&M!7$L(h^FNZpDx6^8(gAVzuHP~pP(?WMb$3Z`IZh%z z8uaap;Zjqlf1HhbF}#NhqIu8VCet-ZXa-Yn?tt~)*!Q^LgxhsD{59cYPY|v~A?3-86I!1UBhX13QZo_Hhbf0pBALG7imkaS~$jMr-w; zB&A#})3i-21u$8M6ORu|I7|)OKpo`Z8;$@8CGRD$vuX`BzcY0LA8fy!{fgGC z6VAk^AU251ZDtYs6#B^FT#)98UZcGY>=j2oXyB#KOzi2+2uvbAki&WWg(*vBO1WWp zFY_t@qOdBPK7ekn@*DW)l~_jmXKZDqLoB==ju_5Ka+K9A`P&|C`{33}KX~mQqMj^s68?9ztZ zE+X$YTo8)+7|KDp zglD+UhaZz!+EBVK&1WfN2~t=Ma%WraUyelHB*qcCmwF1Hy?y?!(C#n9L9Wc#5874w z{JbqpFORx(ee;#w+>;_;XSP(CT7c7{PrA(Q`sGCOUJ>@7tAt0~veW0>oX;?UpJWQX zBB2Aco}UMUbu+0N4v&d!W_~KdkCbT^s`=Q@hbL*@9!d$W$%cm7BJVTZbLp%;OG^S_ zB5b87JrkhAH9bEk(kRq@XXzSLqkoTCC$A{LL^fi$qR$~f4VFONa)lLDgD{lz9qCG+ z8JRI(;%96uS)(xDM3F<(?YNo5lpw9Ol;lLSX6 z_m6XOBIs}`h?VWaLoA=+3H6iie3%GGWUJz>DY8^|%}LOuSM(;pI(`zGeu$G8Qmi+t zi@#Rb$~&N@^nH&^%O4*JZX2&$gLBR1b~5R*1*?^S^OMnSrR)wiuvjPtff_{maQj^l zH`RBXTrZyEqn3mw%rh>+s?!1E=v^OvkVKn}G=TX=3sl&90L=tz#1!EAPuUIPqR}O( zvP_p`_cY(X(Q&Ky!B`BnzK34BQuu0!hAQxgBD6u0>x2Aqw*IwPxIces9whYR&> zj`bTW-@WBwDI0$K(6Z7Sn$!#@XFL+rPP#?WlLuo+JyeeQTr zvekwC^_XfsODdmk!&Su z5Z7d80v{b{Ulm!ZCJbYsN0VtDsJQ5pXU{rg4RO6yAU3CsoP(SAA_rvQP%*89-Udu zd=$}1CUYY{g)26=3CyAw6Cil{;pJ_?S1^Pk%0G*Inj_NQ@VpG4I!*n~p@a(sTMl1F zs~*LMv38~_&S4ubD#>Lb`9`#S%40{elRA~iKFqMQD|!#K?%RISArEsAT2VZy!TdHn z&yqbP8?HxaoyCKZc1N#_5zC&~uA~axK$_KNx_JHJL8zNI>|p0$I`Btt5kW>>mMYRY zc+A`ee}8R?MWUn;xtE$rlXLZOeZ$>{to8t1#Xhw%Ev4J2&%p?qz@}8rUo-)!(smnN z1+AC=jBZgZ2`P4T-uLY$4opFu45H%ENs$*+Ydl~LV44;@5q(SpPx2-{e}0*4x(;d8 z@hN0eo6^|N3eIw>g7N8mla!)}7G6PGgiBibiV*63<5ea%^D=}_M5;bO*;^6o#5Fjg z72M0y`k$V!Drh%tuID*Jr3b@A6d@h)(+ayLLWOH1(OX)0{8(Lgv9>j6L5X&USlzit ziUN*i*@Fg$hZYSUl~89(8*mTRlmXukSg46nAT$99~-SleA&<+*YfV+dNy0}G=ebgIq2A`I2@=jLJcN4|jtc6|vFasFIi>Nfje{{Q$ z3+5D^Xla68m5&Li2X!^#~OBN9oSz z`&U^#v{#CKHawtg5Sx~^!LIQ%>jP28yCkD^?UD%u1-(Qcgg%{L?Zlr`jG~lM zFz{rBo-cx(l$Znbra=^Hrz+$<`Zxx-XZOQf6!f@~q7y^5yAMce9BzArF=3u9D$4Lw zv`QQhv_QAtWuCm=_v$tceC+nFWi(LVL-8JxaRmUDH!yn=J!p}% z0=Cl8b)luAcSl+`;0!AD;LOHgiaK}SBD#0%N<n~`YF?YNs1^A zu62~;|a0qqlUwa~Z|_svS*m*uE|T&r#@Mmk(4r>iH0F@X#z zA1`-LZw348g?D-^`CY8$j!Lwr)xDu**c!M04*;G(VZSswU0=HiubjMv6S;7EJ)=#G zaBCe69w*Mi_PmS9SKMMEQ$So6(`U`oX2F3u+d2tN5>1j>sP+?&%=Ti5r5PM&jEx3d zEQUpb)_|lJiZU?_9Etxuml3=)S4JPo^>{Y*mTJ!DD4Zve8R&@l{>J9cD`rN6ebd@|y>D;Ei-*<7>dgVLFL12wgBqwebFx#@CD9BpV(>8NGVYi-r zaV!b`o#Xa@CBxv}OLt1C8WfjH&u15k@s38@1^9JcO%ANLq!T$#kMsV#x*F<|?tjUL z`sptsdHnHFqvdI>AgjGsWOrpc8SP);Q8@4G2U@^D*u$n0y24elh-Zu_s5Wy@- zZdr%p2MJO(T1EjU`iw6(vkT8U?){XQI0HE0&B%_(J+W{aIB9nW%D-8$ixWtfYo-0~ z9AmDkCEu{6ISNyYW{nT?fI)(zHL$#?=-VZ;R54X4sEg=`XN#)SKM4{yf3bvT50gu- zr=Fnz5j|gcMH7yP1ihjUR^&e@@RsI-M&?d4`cLT!d3PT&_BbDmYuni#xm=|b(Tx>M z0h*F-($HD@K6H=kT4Q3{)}b89WWF|+I(?(57h1Ft_LiaX000003raovS=|Ef5eTUy z*=S1Co1@XUU&rAgJ+7R~<1D_G$f!I@&3A_`^elqtFD79RxvWZ;(*df(|@tyzx006Fj ztV#F(LZ>6@CQpd&1_5GP09YJeL;)-I2&6l=H+Xym-mX(;B)XP$lVMv&4B=lEe1)|F zj~#$ek9sVjQmkWIS%xs)0gZti0Y-pJirB0nOicT>V6l@2+lSjT7u9bd@(I)w7;9A} zr-y8BbzY9ko;z|)1jbA>L6w?#A#uO)^N_6*^9y+*mp(=y2#t7Ynq6Y@meh(Hu3VEp zP|Ow8O_K`*zVi z#`#8C-j+!zvYU;Ow?vL17nbl#ibPHihPfv!1^A(HV5F1>cud*y=gTkuKGhmx9|8P- zIxK%>dofwYD3ZxS7}yJLDkT}{k)3P_jL8#;Azwf~dA@UDg3`fP1CTVh=Py8Q=*Gl9g^3)F2pRY5PTn#v6tmwG2O_io@cJwobfRo zqy~7`9Cv0H%%F(Lg$$*g4fX|a@}jC-uU*0D{@jmg>?p0;iZzD%^U4$9%^Y_jjAv-3 z)14LihxX|R^L4GP(p^txf5E?;FtGer>)E|h2&_i~JF_?@+PRiBpNG0yXjGTV4zVvvh4N`Cp$e9!B&@MKvK7OO43IrwOY z&y&It00000FU^yD>T6qsgDCwn+YECgqCwS9R}LMc5E&jkud1)Wlm zlx#j!pb^Zl+=5N%v zQ35cULPT5CDJCiyGfqj|rxVEGcN@&^zgFbd>@@=QJs7hMnLf;wiS&J=<~%b%5#N2Y3%lln`vpV z5C8xG7WS$d89i@nnkbwlx*~+|M5VCZE6zrv;2|)yT(e;SY;$n16x0uv^kvM?N#C*x z$%0L}DD9)U22nCNtKnmxip()Jb$|C>j|yx7{}>Uyq6;W)Y36Vqk_{DT5@7ju7IC;{ z>2umeIGv$=nD>}?uYdpm05co9iO7BsJ&t)LG|WkKDyEoBYeV;lv6F_#kBw8rMe>)Pc=tLBK1qV#1bcan%EA69O$+kUP z4#pAp4D6z-mdTDSOK}?YJqw4tR-;Y9d(f&FBre$4s{xxx?}A{q>Ja*F4w&w@*LG_x zzuWgr^Ggd$u&*MBR5g+g(ZzO3p+EQahJ~)vU0IFY7PN8zPF+}#%dr_`LCLAMV06U^PfUHLen5Yru^xdm&4-`zw?feE6tO%kn?~002muZhRMUw?aYj z$p3OPw2$2tc;0rIs}zh$JS4b^>hf$`tJTu!Y&zvYu%}TS$^j18KP+@D6QcD~Mjib& z{oTpgNR@MsUD}s+(Via~NKa51oUrf!0001e7xJK-Oxc$L%JOhP>4CBYvXZ$-!2#5O znS)x90GWgdiQNzDO_EERQ%AK)A|#n*gkno`oOA{$y!*+S%ks+DVozU)yk*HL#=o$8 zTQml>B3Ub1`S1dhm26iVexPU-^1TsaOIKWA6HjcP4(8zO{PB(fMm8uxsfMiKQor5il2w@Bjb+Da^dM!4kIj0@IS^Cf*_^`hs@ zmyJR$+)q#OwN*~Bq3q7;GKGAvL|Bs5*BAuT+b6t3{C6m2y$n5KNw?;{>!pMLRnB@x z_c+lesW5{i$r`Q1L*h88<#tAr71eeWu;BW|5Ezomt}ipAY%<$lj>H|=&GJlsA<7DX z00000C-PkH({+4&uf285ErV$7a%al9;;}_Y|06N3e$^fh)oW*Z= zUdZy0gOT}0iQEacU_A3AGqKIbssNV&0JASCs^vt^HCJBOCV1!*v#aMCqL!Dh&28k8hJKqkMY*#J$uci|AttSPLjdJHoT|V>V6E0!v?8}7lmYCn& z|Ecx{4BmL=Rp7@YC;5`d0R$q63XyQ4_&ops01WuDk`)F2?)Zez6K*C?k0c=nE7#Z+ z8E#|Y?9070%n9rlM!-i;pp*Hl3Jq8a>xlD+r)f+(t>UfG7x3lcY^LJeS1^+pTrZ{) z_N^xckr`1!w$QYz_cCIgyx`+auPqqv&S8n(wuIJ@=r&SMH)(fs$l z{1${py$zR3>Mp|nDGMSCK4ZIDqp5urN=a`$7JxBmo5e{m0000DVK@PH##4iwwa4$8 zE*=Bd<9f>*T#nKz6=Z_kIsqN*eTTXxV7N5K)o$-kkNaSM0G){Ag%m307l3u5k>F6` znG4oWWV)p&H)F_;H>1DB(e8IKyp|bGn~$j>w0(Biz4%4I0005GP5KK!!3)8R=mU%q z&^@_A^j8&W+|VGM&MM^jHHrhBXbY8uY_Jh>wHE8rgtqT||EW*yTW|?CSEey9Fz!)d z%=v_%(S@K^ugk*kD+V1JR8yo5UT;+u>;h~@)rurEyWjJndzyyn!o*-`mwHKsbp59_ zZQ0NpIs<1nmQmB`)g4ClHef|1l1;ZK)R(t+L?4o(mb39`TSA4geEMVH0001ar25rO z!x!~Brkn*6Oo7WlVXFZD^g;1KFdH*A7G)lg8B8jCxsB4NIrV~S{El`zhU})osK17O zClnfT`vHc~IBOODTe4$_m0n5v>@;h9?eO2rAYsN1z_9G}!!o8IETgB>rN1nMk`VI_ z{T$dW=dX?XxQKgn7z$%!TT2QB7YdZjoVkuIFt>5gENkLA34R`(4h>ceqjT80?SvXT zXaE2J0ly>Mp6@M$Zv`l5qiukS+1$hd7(YiR;bef*OC}K}#;X&WA7zktj)~!!8j($f z2>|TN)eG!uq*^k&{yXT~Uo}?YoCJUX00Bd-6@w6v>P+-IjR3D$_zB2I z9L%K%eUxX@{U|W?AM{PF>|11IGYR|v000Ga_}`LH+sDjmpuJES| zF;TO**z9;*$)Xa!g}+cmbsSE!)<3__ovJZ0r-POJyHlbCU0#smcaL^Y%b*LkL2;Pc zdI`4|rL+J50002v%YmemF)g+5=&I3#0~&>5W2s01H}2KYp>(=OSK0!u000084EWLq z99bZU86+5-p@6AKr&XdTq}n$mp)B+gv3a*5OoC4~OLElX6tvJI86*Ly5kAQ{=9dww zL%Q2*(@>OA#31M(ddyMgoa=GTxGj_Ami6!J6WI8@-6&ZRc>kbbF;?!ojh>oW-la+cVTm%KHS=9s&$a3y&I_Fj=$f70UTLTqP9 zUz$S`S8aQgm(aDr3xpV8OimkVU7C6Z>_DROs4(x!X@lIh)>PEaLY+gj?I&QVg$s+u zCf+41QzC9AG54*1s3 zVm;CQ5O`Z#aWXT3%ZUeiJq&Si4-IF-+M;NSHsq%yoQyqtGh{EFRs(?LAH`qd1;ZfQ zreQe3Ksz$^Li-+N|HLCoL6C&NGm{X200000u5ncKGl18xpG8p9V#PQ>9jE;+# zL^-~`oM?J}O5l?~00c?wYQeC0asS3zo8gPEQ>04w7Md5mfQc@K|4Ud0YFH1H&1XO$ zl7m7rO8`M-5KYfn(3N%Sb3KqLT#;Ld9s91Wu9G)Ya8K_0b*OJ*s{PAt5&`^nW*4sh zKbT+#obkXK+P53P*taFr_$-!0+bt)S;)m`LFQZ+&`v2BOk1M9jbGp@B77y%SQU@LI zhDWNNLDy3AKE8xc|}AqpY}ossq3`&ao;B#YWyk z`bUtStfGvSnPHis$e$i2{l~@)wZt?2_z!PKC}`O-CvEki=ni9(wtEEMCbz_8(ESQjCt%=8kLPQxiy_~kHG4y;`fq*zgASr`rKlwU zqe(SyFekK2W|&mK|vn!iO74zM>oF3&06B#?UE`2f6L|d7XTz6 zL&C1bOmd&_{#~856JBAjcg6H&U_sFaZpFy2FANCE(yKol+MXkgn4{z8eI7NU_{+M@ zEUu@KIFfzZcAL4oqbWZcT)#XcASe*J8vO!-qj6XL3-bUq;|bfj2?@6#62rWq*xUdY zIRfqWd~E4g>uQ>4iq~>b`LhR5*;0AWa_sWPh0h?xHnk%u%0K`C)#da)onFRWd6XnF zx-0b{*m(n^UQ;j9YGR-0Jsg6Q=WD?O3T+$wszYT-+M=G2N^n zO8KJC>FLhYU9GPNjhCbB2FPOp;fY|4S%KvFv&(Zikm5;z5y$LNZGMDt&HmSA_FcD3 zA4y%Hl;t?3;C{*(-EnQL+>2b6F-u2w>a>M^Ejn+iEa)ws+IH+g-O#hC=~TL%di60s z`cNj9zY$1NEj-u!8A&as8~XrQlSCF$CY3I|hsEFl1g6VRuoxUuG>Fwyy67%^=%jrpnQILZxm4JT;o5&AmTsM@6PZ zial4)U#W?+vU&mk94CZaJGRtI{@u$}fg0@`0NPE7CCZj-3ET{F>mcW2^
2UH!mFNdi@<0Fr#RCTVPBzwj;m=->Dy7-CWXBOIx4cpR z>>{GZ;v7N;G`R)ml=@XC9u_`+lf_eKlmXtCT~iQ^M!2 zSb7RCfM>rJH-P}`#WPpe{q>&C*agHx7_YE4xK9S2(1x0)^S^16k4^uP?c+)GT#ec0>;|?awO^gej1gYX!WWVaf8Z4)7KS)b?$&c71Q=FLY=4Mai1(TW z+URJT@AU)3`yKT#Fxk3?<&c=z>T;=W=-c){$AOr5kzY_xV-}!uS%tbw)~#T0BV#I& zSq(*HuzCOh7XlQ%=n*WLen7;#)}TdzomBZ~c7(0jV`!Yu#PkdPKGzW};M~ZJM1RJa zjWv2RARr}5$S^OzGuq2m3-p=OAoiy{uHH-^-$HG7qFM7+kxT&6AYN2!K4% zL|&OY{>?Mj@=*1oF8xFhq$5qjR`6Tl+AY-M(W5vY=NK%Ezd{?if-bz`xE_36vHQsn z_&p(XJCwYI-rI3+3o-o;5PRINJ=Ol6B=v*zb@1JqCybpX2(O4DSa9uy#+0q)jqR- zsM(*%4tzU$D&bvip-|KkYh21D#AyVbLYRW*U#x#Kl#Sy7NH{^9xqra6n!txcUIt9* zA@JEY8N}Y(GpIGtr>H^;RkWTnR2Y$3bCrEK_StALKQN786O_|7|CyXbD5XY+_Aswo zmytZgI!cH>aY0ANNm4sTX#Ta{Ve=+EUYTZQ@#Tv5b=xl>b1LpQ$k^BN8-52v-?+oy zOAU!Nb}}%K(CU1(&*woInu%r;TvRTLQeDIq54gI{`mJSm1i?4lps+^g0_q-{;x@Zp ztp=0BckvUYPrzC>5+^mGpi00DsERj2KieX(k4Zo5@t&fS;gsoA$`1$B6h`{vLDMF*f*J*Nt42jt*sHEW zKpgJ|XujQcpmC$Z!XHRfi^)Ir5=rJbWoUH`RWxvxhQ;e z6&1}_O;j=21v^W2I0F43k~~(w78P6ptID{WR}qO(Rs#ZT8ia&)|Ei9cDP^%@gpV#f zw%VOo7y+>3f*OcZ7|4u+-&ewix{p^oTA}BPw>$aKLKJh(gq=WO8}=JjgXT#4Pxu!| zXy0X^XKzn#H+UM<&aw`sE1od$3-vQ0o1@F5-VZSRw_k~|V@8P#=>x5vP3ZCI_`Yi- zSAYNj003O4Kt>1iqEtp(e%|zg(B_8{NzC!|-hx}&^Fhp0)uXq7!WQlYfJDb2Kw(jA z1(FZzg%n;94du1{@s5o$>{&1X002a_PkkHWd|3eno|yD4B^?}`8~+}`y$Vyr2_|+- zjs)-z{YbFxk6*Lha2tKd7>CpjnHss0v%1i7l|kn=BWJYMl(9MB4)zA+D$*=-b zqqpwgPX6h%ivOX$xN7hhGUIsxn4e@Jw^(c98pW-$aKV)UG>v8UBaS*JuCEeL?#jcOtj$x;_p4I1L> zY30*bJE1n3N3BY{(6OD$3Jg>Wig|@MQMjMsHr$4#s^zUs8cTEWzBL1=!7(QKR^BX( zG4RzblhS466ZhOHzE)t75E3{9OH#u{TqX5&?F`FE`AvnXBOGP8QgcZn$@(Q-Ll;+p zB%UIOJ4X)n_aCd%t0@lZt=~uQ_?Nl&Z8aM^`X&nUgey7dj>#V~URKYh6u?*PLfG%> zPiQSSUIc6oL`AT-!8v2RUMU`*ne2^nof<~L(sNTNqgY|f!F+Q=xH)jjdE5XFphJs` zk}!9mQ2ODXOQ?111?kYN_^L4uWWJVd8Z({pyq%|sq`g0RWiU)OoOUz*r_PWagy}Fs zUEq`xQfvu9qBy>$5%1gMsd5^M?rQh9?@ag|F##^=4Y=&uF`X<#Rm=l%-8}r`{4p`{ zH=+j7MYvDfTcr*q%6!w31Nog|Bq;J}hLp!eq99f0 zLMk+IHnD+N@uG_VWDfS&ZXv=M z>{iSSQlO8-u$sf05BN*uy>=@t>ZApn=K#32{vsN&RzL~)>Lk<$F{K1NwVkz_Od&go z>v*(eYeOThqB$}JnJ2zpDcd7Wi5qc}&5j=0&Sgv?ctAUl4xw+c<@>cL=f*n5uh=qy0&_PGW?*?z&uR4K8;TAB)qn-YCs8W2brm6NfZ zqwc1~+C5-PlWb2*HM;MGN(1xO-SwpS4CsO1-y1MQZ!;6W>@SQxakyS$U!1F^+?kP* zcltHnOeax}12n7ayMs*CzY3Gkum_Lp0J|HSB$FQI?=E%_g3y1>4~-0P@1-dwUlPL)xc$xaiW$iJV+q$GYlT|~GX05D zvuofo*W?riC`|oO*62yoy5ci-~4*^=vjNQ65tEH0>sc(&&e!hA*eBi6n z|N77{$Z3AxFX|i^*>m~$4tM`13xMl78OsUm8H`1g6l(pvJkwc4F)U3+(F@bkwCoEc zV0sVpI(y5UZlUYURWU?``f1W>P*<-2HOf<|_AmqI>whb8=^|Y)=S_U&m-Ect{t_rS)`u8r2b_m)1=<;L_Y!UW7d+8Ky2u$I72K-Btd35jH#= z8%Vz6kD_DOLv!Q<0c8xK{t-B7FJ(AVju$t>j4qW9`(k^5=ia;+*qFZao)927E(x6$ z={H*5!|YbVXxr5q=kRrJE78bJS2PZ@T-Q~Vk`ipA(2VR3)K5-NoXVp1x$oM{h>K5td=U~iB%e1VV1AA{OiPt5@gJ1Xml`o*u-i0D)%7j@hw|t!Y4C}@yOx?g;$OyPP50q^hip}F4Wyo6n=5{(f z`goRA!Smb^TARg!q6A(KdsapswH5+Q*t3Zf+F2zu?anGGnqC6uE`axqR1mZ%$61Bb z#y}6U=7ZFHj_)|Sj8pS^004^l*#wK*d*C=Nr*le*wui&H_Qy?@tGZIACx8Xi6mw^c z&*ykcPBQ$^w^2ksiV^(%lDC*Bpdw3I26OVwT62T;D-A9NWCN9Qrt~NTAF25E{gb1a z$(-?#UhG*IUf*@pHBB4rQ@{TPIXG`h#&V&oP$z(`3f^?i&&0y&el4zIfd4W9DR3P~ zYua1qs@ov;Le~_ie|WN&tTQ`4sqLJOtIJD0j~dg`LR$5$<{-@7WlC%M;J5R+nl?B9 z7EGJ{HTxuTM^zwq-%iqt9JG}QQVjMiflknH@g*wEQ_;a29#mk5+6$3Uem{dnQUK;C z@7GPAB6&R@z3zMh1tMltM+oPCw=@MRnNe|ZdXit}YmTaA%i!A+gHT_^zqGBr!>H87 zEY~`qnBuYg8ay#_jnd5#_)^|B`|Xw13zMPHk;y|hCJDt#{G^+>LZw?FiZR7j-a=}G zrfNTOxSI=4&D0A5c$7Ga6rt-Cb$}jtG4bIQ$r!qoofE~9p36F`)>R+YNZs+^+Ab$V z-ElhTLAn$k003kcu@!dH@k6+-7P2BE-5?r#tGtusdT#j69C$?XBFqqE9k(V!jzH6i zyGS=!(FTPcdLrl^q1%XpHxP8Nh^q)f;s|hkb5P#ARTJVuUa8Qh_gKv?iphKZW%;## z>B@3$0aXm_k1{)G3hU)@&_*8oskPidOX~*QcQY>=^n@CO#z^@&@!%n4zpeVd-@K@* z-xsAQTamIjK)dph?iDT5jLdOnmr9}g6lIXOVyrqK)MQNV(tjJ%6a6Q{iHx7llwAtY zcWPsCZx#O_1H0VUY*kj5Islt(H%KTxd9X!Uh8gjGi8fU$`CI5qT9+#SIX33Z>O)cWS z*HPM&9AnA1_cbNWe^d!|BHh%&B9q0l(HN$T@RY9VQ2q;+LVcCtbGMskyyk{EdOQF7 z2=RP>si-zvbhAu^!0mVN;HX*;z4#i{(62#pIr-7fr$Tt{3SO0Y?Kmtl_#TcJYbU%6 zAJ0u>_{letfT9u+U%Qy&_P6wRcE6f~A0#&1!o4L7)z`(!J=^|Otc)?eHoGOcU zCpH-lmx3O)2Ea}!dXRauK>(H`Pdb-OGvZgpNkmXLoxa{}kzyi7u&YG$(1~n_ujLaZ zvz-$qwXiFGL=m`yrSQF9S*J=n=AjuH{YL=Hb$UX&Uc7Yv4l3V_cVmh^)s=l}m<23686(pn;x6t^l=L2PBIO~p!xl}ANx z;?ez!go=yNcOa0qr{=Rg4md1en5A2A2l`RdoEWB9IAQ6C=v*>iM*f-a#~`FysaC4dKVhc$@A4uoAawT@5pl&APu5jSwq}@ZbS`D?;#y2lnHPK4=5LP>cJQP~fz6Gk3&Mij- zEq;4r>l+LX(*VTEEGjL>2rkyhg=!Ky_OcfR19%cNdzWSGc@54y70K&rkMDQG{3m-B zq}kzfO;w!}1~Vwo&%UB{a*v>J6X9DN5n{0-Rg>f6`#qf}K}Ufj@e5-ri(bSW{!8r z<>S|6E8iIoXHOoe;>z80O7Sm2hMsdIFTEja)nD4Y@9Wghwaf7~lnD=jpo0-Zuax2i z4RC~zCd{lEEr=X_rps<0rMR5t=La>q+#80k?;JI%x4{Lp3HEs)6H6Z$|uy1E(#E&B}6 zX3J39Uj(&VNM$p4(^HPD>**|_mM@dI*p(q1KT0{&`P@tZZ2xwXkgv+oN7x3m++tdM z)ts>iBlW_*mqx6pqU_pRG+06B0DQ<=*&bz%uH@IlA1$}khG)wUzQ<>(w zy}@qVX1}6OE{|hYuSr!xwA$w2);D$lI&;IWX?F6Ch2>s~Rb7DFiMMTh0ni+xj39A) z5Rmd@51h+Vj7U@}A;%__XNLwc@SXkZ)tv>3hvO7?uXN%IZj#ge6fz{;2=G^;WMQVT zd6aQFlvj96@Ls826`HK#I1oWy!hu=P{HXAj7F|*n_bCyle@Y5Vl}9l(-sFJd)#g%n zT`z1P)Ex0I!u^=BnZE~upK3RU{mGI>FYeT3;rUh#bu7`RRypI>9+VRBn763^lqEiw6mXGLcFh_H3bCk!Eb?5 z`gtVAt@)hl-RD5iGSkXdqW*b(BOs_lSCI zt))Rnu?_m$=q>Pn5ph0-Ox-WfUJegD2#tc(@Xp8n^_8IWo(RZ)>}L3adfCt((xlOc zPkU_(H95W^cN^nlg>orIcPeRO_? z5zStKiHbO%XfK}3U-*3+(OzB<5Wd|0RIG$EW1g-qOxStig{N!5&q?;gS{YXgS0e>0 z0^gKu7qlR!_`8Qr{(fI44(u!Iw{eJ$F3(S+RWQu_V9NMx^5I6&RA-T1JIc8U1pAt% zBM$u86fT^7aRs-Qy=be=+8(O6K+u=DqlA##VvVyORXK*pyFX5s_k)I{W%%J?sY4Ca zfUFy;qSmlm?nry=YS;RBc8W85o?t^+9J*a80KiWwKfeV{gB|CIqloDvPCzl6&Nveq zb>+1^voUQi5ChM$k&*z~6oCD~-9d&GICZFsFDq0NST5?vf!eIh`5Lr<6M5APF%XfR zX13S!X+yPWwg1+bPB=C;y)42I=?FPq9X`{vKmo5Hz5yz%VM5cD0T*z&b&|8bPiXso z=r3)Kgd5uq9_|{p3Ddu)aai~p@KB^c*!-dGF!LG&3YOv+hw1S22e91XXu8YI{c#!< zGiWa=?qdK^1%cPVD*=OD*JCaI)xTSnuAI`#(5B_np{DAapwpXv7O7Ioq$WE1e2&59 zoT!jc7GQYZr!Fs(*R>5&W)ousA-DAh66m3P(0Q(E!XD5I3V~nD1ih%I$;ph>8L2U3 zDxT*lxLwIM1%RhDS#ZxtMs1=uw#TuPnZx4u{1XUnk%3*qS1lgwQjb# zk1+51(GroS^mo2#+WYYPAkh~4YyQHG{sFR`BOs5)0|yoq-OOLoi`$!vlzfoDF@DBd zc_T+wjdJ}**HgITI8o6dk9>fqSD)vw|N0wKH=Fl!@IMYUOR~tS7k}=DPrn1>zdjko z@~AiQNSB=pN0@{J`VIA%j_At$f0lP+CVrU#xwGK1i8eILk43;PTg?It(h*hPeJk0& zbmS-afw{zq=oi^2CGhjAIUG(XH0AaK4WLIO9*7Dd=*)nTM#ibS1J_v@*qbbQacW6j z0SAkL0-h1_Oi_BFeSnBCo~GxhL?HU+h<9ey3yCaa+DY}$341y6WIF3U#0o6D$K_e@({(l}PAee?GGNP>h8gJwI^&xy4!k9AZ= z;)F#+fG)RxiOMopu6xi=3H5)KZ-Em3`}z0n=}>l@mZJWiE0HpNiDxGKXVrw<|bhpycbji*Z|-bNy|p~*o(Dp5>OjO>zN+;8(*s6}8`zo4 zQo0R32XbN9@j8Q=Vm*Lwb@Fe@NG+2J`u8Ma=AZ2QulY(rUE);41bma(!<1H=Yw^K8 zZwRm8vMZ^)vC-i6FjeN!Fp;_7^gTG-B}pZ4Hz8L@htt5jp&wv2BDCm6Mlry8Y|T7V(?n^ zLi&+pE{Y$brAPf^xGDkX>Z7%fkptq9v6wXQ&?sjDizXd-jM~%pg?^cj%pk^%`sCNc z9m21FO#R$uCa)k&PaZx`5mX18ue<1e@-1JPn`J|dXqD^ww&Q@uG*N*{)#~=!;I$cX z9E=gYzJ^V5_QFw)=p0G-8{R%FYcT6UPmf!6NxP9Ueg zQ1H+Z5njfnEf!q?4a$2KBFKNAH$q~H6js|3iu#=M6CifnLi)>(CY4gcO4hq3>{9q zA_fWwyJGdG*r${e_+H4KFGNWjO{SzctuuHha)00Drl{0lh6=e(utGr9ul-!irV`b0eZWyg_o&qhguv<}krLL!pvSIHYiHojn#aU}!Uqo`Hk(MwCsW78;QZ4^|6M+#{ z%@Ha;K_*>ItCtgjDk3UmvgaA9T3WvOC8Ja8v5<{6bP5Yw6vB>Xb}mo?@AeQn^bEK& zc=5kHP9Xpd@_Y2#3y1iq1AlyDPzvrpkr!IV1`tq*cIv`T zRM_7Qbg^bdT*F7vB+6MXdt9K9(;1uDDiwvu*R<~|h>$GOEf4O5QR=lB|F5}{YNBA|S4Pm;e33yj3O8(qoriadX~ zApX~6vJ4Brm#;T%7Z@*^E0CP|g;mJ^)net}v2H3F;&0a-m*Psa>Jf+yIReYSRM=|g z(uq}sW6eeH9`EiE9=gJUakR2v{=^KpzeA6i79kK4D#=hSVJW`}D%!e&sYp;LNWD5t z+=a>PfYTSr6!ZWBA_|#OIA%-*8N9vF=upiwgkp?Jj`*mZOohOZMc^3fQGUTkdp6_e zuxZ1aeO-rhY6rD@LLWZQCMKH{%u$Refc&?&Wc4J!hn)!f49wH+Fw5=^xo+M8vE72l zD~9B)0OwZ(AdvixVeBXxKxm-Sk$kS(Lb&1pxR77lzB3{09HY{ngIZh6D{m7D#o=XS zZdZU(5s^q3^>9m9NzetGuokc1_fZgpv@~^kcvVhQqel}S{H3ds_z!|K_S@V&aoGhV zyh)vFm397Dyhm|i*a{L%GKT1rc&Ul5RKAMur?gL)C`CVvUrTh( z?Zkndz0YMXXwVCKPZaE$_P5zj+VRNl<48sQA4DiI7@Lz@+D+%sm$U}iu8EBeHZ071UB~FE~Xmd;F zlH`vT04dSq8C87!aPkq?^4X`8m2CTEP;g!HEEZ5MG*lu!*f{RyZkq_QEgu*1A=_HLW~ZUMl*-RNsT{JQHs&hsso&VIcvG zWm%EZcv*}hT5s@yk#S1Rb<+XOAr!|E@0|i2asIp~IlXjMy?1%D`5g4tCwf>aXmQnZ zA)aYo4hoZH#8bkJ_ja{jI3#Dz#0QLIY|nw0-rvNkp&trGQZyVyh;ZSF*^-bzK{Bn$yH<>imP04+8PPX2Y;v@`VL0IA zex{B=uW!Hm5S{>e-{YaPzi;-s8C7iTk*ZBHJjU$nP%1!ePl}_?%8b7}4x|BvFNxDq z60G=_vJ4WDC`AvlCX^FV-p#oA>>6<9-!OA9M)0KIBz?~3`b%Z$- z!@IS6tHq1@`M~Q9avWp=QUHUN9&Hp)_PLEtu)vWIq@$2ULr23`YBkM-jpu+V_vR@5(SY!f9F|=Oj_&-*GV<=DoDEl5 z!BI8(yvO!N6B``D-$`eYPA4}z&|T@>kaJURlaswHUcw-`^q!`pq?dA0Ib46S=2fBq zkDSCK{iPD-tHJ}UOPF4cM?`>F124vuo9onWnw!GN%~+>74MCA8syTN5m#z~gTtw(F z;HE{sP?eW_E=;@J0r|2jz0(|HV&H)fH^UNTwcHdS3&pv!Aa&)FFn7M^NATIEo-0xO zO5!e4<{efj&mY_-jy-2(>*P4_Dq9#gZry>4-4>+9UEcWTcH$6~tS@1=eU(W!kP#~fF0L~O_tlQ9p8SnYf&;8$hi`26lH;}p5RlO&I0SQ9guVb&kVST6 z)c>kP?n6liI0ds}BKgctVwnau%eS7^qCa#E2BOXOn#UgP>=8X|vlq&W4}yen^D^M& z?k%x7=gLN#2j-Ucxlx?GLdU@3n-J+9&>Z3JA~uO*8xT~Ko=>NFhY=87J(e|aYOwi3 z`J}=5R4Q{C3kw*RoLajmoR6$yIejUC$1uYwrfbM(UOrQ`6+YU=jUFxK4_L{rns(#{GLu8@)EzPm0*`O2w`MPRYvSX;DWT)PUQnYpc{ zfkhLuj!;{QGcE_-3A%Ba-5)*1uj4iMU0_lSUOIpm=VsUpdpR4IMGMH@Rs(?=_;Yrl zV9hb&M>;7>Uj;01fk*PsZe4TwN+=kO9@093vKwG5kq6%!fT`tx=)9fVZaa9=oyC*N zV7JZ_{HzAp;;&vSufXXs#V(P`XJy2ttzqr-jBccp7zcBA6k{Ixe+7~idd+p4GoEnR z1+G*>=j8aQ40VyiL8)XJPh@+<2<>`NBgnU26lL$7q`pf3mf&Mw0qiRi)SPmC_4S6<4LyX#tmROI zWZJ_gB7Pbc@7w<<8k_}hs1$j}0m=9Apf0Z{5OQrRp|rC~t6SjxwJj#AU~zA)#Vnl=LPvSeQp}? zd8u??CC6=cA??I+1CQ?U)uV0ji`|fv5iI}cng&HGYuPYdCt^H+ji5s?sK?eU))ku* zE5xYMfOM}7Rn%k%Iti;nzXOr|NH+j?+1z!LZP8YDFR)xRZ=R~qazKXtrA+!-fKFQh z8J7G@dKDK##@_AnCLTu8($sm3i-w8lewOteb?wYCun!Ea>Hxt}NJ{Ndu+B zxB^o5>c??Ni3HaJa9PPGdl|k#27@y)TN&+H13Ppsx2hKGdF-?!PDlAJH+Cw2q8Ywb z&+-0R6}!%hH>HnE%vVQRu}VjtO;8zN%BlfV4gFAZExTtB1AY3``r~|Z(VMBBNRcN3hPSMGAEG;O@8f5m}A*Jnjnptc$EZX)}T%f02#fMfmk#?4nL5MVKQ zBd<$sZMFJ#59ururA;mi3K)Aru<(d&TT648>vtqLH30?-_Ik{68`o5yiM6^waq;Tv zI4{dL>`+)t0eqPNDuA6k)g&K7MLmLiTmlng$QtYfFvnoDd|HlvfYW4mvwmiVmmBG# z;o20LAFOUyZ;GaW?zni%d4OP?6d_$MYW6T&;jVsHT#SelcuW(ON`oC zVe3jEbt#|NOsY<5Qhsk5)pOu@VSZ)y1iZMA#t>w;fj8xW%d+9gAeFd}&9Eubu995F zI3J3wm<_VNd}?_1sa;)fOumM?NL3W#4FuM333;h(AE zh<4IRPfZNahW-HC9JGtpeGk1hkaEyHqf_gZlr>;d;;vG;UJ=)&@e=);3ZrDKi`a)> zP~k;yj(vlOIgGYj1rzQ7%>gN%JVEtileyw*;)_tDZoiom24>nv=dm8>{)jm;+w)(~ZD85EV9*hP>VD-# zvb}`ne?OnmE^B5t@tU07WP z;HO2-HcBvAS}fODq_mJal#~WRXn?pmqy$B=`dEmzuiZE*rsf>NL+SgzO)3f;3nhTe z06Ikp?e(L_tO4TZ_jr)rGQRZ{sFCV^bwZ10ekjSJOjxxk3D4Rb)i4gyntP2=Py7Gx z6mHj9=qRu-sb;Q%6l9+K_949&K@Bg&-u|zReVznImoAE`?!&L(CC7#YN-& z?OFc=dRw9YWNya!g5_ZSHmJ?#V~#om-HimK-u^~K&HAWm-Wz3CCd3T7?!G;v{qB!F zTF?E75aPRwpcVs&@nbX0bYO+r&XV^mKK1>{B(_E)sXCyxKI8_RP~(`6>{dMt0(1`U zc9>b|Ar&I(?}&7#51KO<#^MgNe>4VDWVv)d(m(zcJ^EQKwRVO|&3dL*N@ zPaDpVz$3E1L=o_E=M~4iK)qveK2jD=VAC1k8-hRp006PUFy$+)GZAI7z%nNq9eC>p zC~nvjW)0X;b~6kk%bc77hifSMb;k#Si+Jrc(nY(=JS1?&R=(b&oUONIuz={zoRd}P zscW?p>GFB=)J1;N-1IUAF%;l#CqK^rgcz(}=Z55`b8WQ)rk26RMohKM-fA50 z+lVmvr&zdN{PTj(s8upiRe+$(14PC9o~OsG8?V$e!EE<@#TYhTH&aaNTb^ZEMR?00e))8VmOK2x%_E`h{jX zzc6=gZmN0Kll(L>o&4N3fyH*uS0NvwY~Fl~H(Kw7v0p{WVF$oRL;7Y}S?xU@OU$`AV+xqx%03bO~t_C{!sb-e~d(1oE{(hO(=*l~?{bZCcH3WFV|@ zjsC*RuRgaPYp>@f+kHFMdsG#m=>2Mv)kX`*gCL?zgxoO?3cWu!kg)ICB(@MkFCj(- z5DsGFihL7n5;j2V1)nDU#`bGl_%-LxtB=@i9+Gf-{m||rN<}5!=)jkcKgusEGDtmf zWwDI$9}bu0MUZE1fBesJO&6asXDmO`rT%5d1*62K;RI!Zd zo5hR$Mc!vWc~9pnU=5M8J{cpf&0=+}R?8q$V9|m%;o7eIO*+0a#sm%=m#KQoy)MM@ zJ?&>7jWLpndQCN>oxOYfF4Nzc6@UwWZ3p$&FJeb>*)GU$@h7T-YbxpuUsMkX{ItXQ zYkTYHGSkxRLP-Yb4@@GM@)%Pu$Q66(`5ZEDfIE_8@^O;c-$Hzii~RodBm2Bj*$I@w z_3t(h`@>;w3QquuU^0oQVPyP-)dbr1K#BAK00WI2mA;RI=2O$}aY z>Qnnx-EpwH724ZY3-p=PtnziJ0T2$%Q#F3)L)h!*;Vi3=009P_XH{@qulxhR2C#=M z(JZ5Z5y#NR6~+BohjMi-abt7?sPwfs06Uil&4(4jV`acsHAo2?XS6F9@m(Sdq#%*< zUW|52-W(UzFA5EHgs+Mk*mr;*TJrz=_Ox~HH~5HI5aF)M%s=5Ios|jh&QFgi=MRV^ zcuQ}FL#gtcfeE7K)X-1#)d+g%i4uS>3#ar@+_XQZqlO3{Ai5r#VeF#6kyf=7^B5xG zoySpRu^huhWH?srHinVq=CzRJ=3J)?y_g#c|0w+^mrLHaLSOO+g=H^mwWgCc$VhXT zJYm%h?}sdWrd>MUkKh7*fu;?bd+;3}TJjqi5 zGCoypY{3WG?d6jE;!=X8hkWvcY4TRv)2KPI-jijRGfCDrZ z5A2B`E5r1j!OtWAnj-(sGlQ}Y3bBvOUHv@QUFnF*6fGV@7LJ#Fn+x(EiT6T zkqU;gLRfjRolQxuJGK*_Zv?s)9(FsBx0_9IK|}mTPSwVWAmji50JxXM8^V6^)pf?5 zhxdKfU3GksH3Yh+;ow_z`yG`=zDukU+|g}eZXe!DDw z#aSz9h?zf@C72i(o5@mAY3%A)!|{WJsT(aOyJovZAH7Tta1{U`*6%tQiB7UY&0<6V zr1#!%nIq3cIqk35x$iqW47u*7XFgeK%>aCjz%*deTZEP;W6`(GQ|xG51Ga$fmP{_P zv4+mup>O~I01sx!8j<6Z+`iJ`Z;Yu$H@Bwt;T7|TOBu$HEmERhSr=qT0g7deAQz{x z6tM%!9Hv`R=nzhiuW3$6CrX>?R9zu!yY0}gmFSBSTDs!^ntNpTh<}dd47Z_&tTYjb zr8krm)zZJ*7-DFob$I1b8-f6dfRfGT_W8iNGNzi9BSzBxbk+z6%)a6v@Rf33OrzUA zz#~Wi001*-C*m-M%P`Qm(Ndcq_B@LTQy^Qc_GL}J*UZyW+7JTAu(%5mRf1PIZRIxV zmkQPqoCEVgxRK$C9U8^sqlN^8jM=>Ucn=4JTb_Ub00VNGjoh&lddXC2dUmH&qXZ3h zvCN=w@}X0wdjXqiZ8`~_dUd|A2Pd2f*3V-uyvp5%PT3-=Tt|1%PMq-ThWn9@hWcsU zll(WoMlzQT-2D7Q$Lv}-ZVK%(XZ@~-o4IhXD=4MK|I=jM%Fwv<&7DZqnAAAH;g27g z`}nz8Ui75$@O(BsmKxMQUac_GWj7Y$xrCU);e9ZdwT9IN3xlXl4wFrF4vFMiO~8Lr zhqm&#-^neUkm8C#{o%Guc6Wi!<+*Ov*asXM&Rf=uS@#;k{o)-`rH&H*mj^U2fXG!l z{(L0c~~M-f(Vh&C0AX7zf9l zzfoaSAK1t$cS`-OCxW%URr4zBYou*O#4VRix}dW(Lr3YINfAwvs&5&Gmzuf=;00TlKTER;pp{k}af}YuS5drYxb~mtBH)Q*)JYH2dt~}0z zRUP#}Wkqcqy9kqrIgJF+m&3t2P*ZHdj(0-O|L+{))vxE2BTtg8Fx-ti>tbJH97L-& zDmSx+jc=X~OpD~r7dtuwXFzP|4bFOXG`X}gfT)M}^>EYt>(o2D!W5-<;LnX`s4F>j zyk`P=ws4gHpq{@r#`ID4d_d(U)8O?I8Pa`prZFqjE$TGhi-Iz=0Rf=RBy#nqw9`s| z0003iC^Dp}XH$Dy2`E1hAFmN}?s?JdVt z$JL`N3c0dbn)e2jxvfJOcotXy002*`C|qYs>LyR+4uPk_t|&Q`%&#m=DEaHEnbvV~ zkEzUeoZVs<0;mvTapZ8*KtL)VT#8XAjcx;@X?VwW!fzR)zD&EtOpiDW@G2I~e__(M z$CCY~DmHgJ9gj_qK!$QCB?TW1bbqE%-%r8w+%G3~0;C+OIg_*Mr?kmSYy>nUBDt!9 zstd$@H->({c1Qry`&s3m*}wn+BMd@6GxcgNLh03Qt4Ra_hc1hMLxXsD%oab1Nx9W) zPe6mJQGge;n&sLv`zG@`IITzU!he>k`C`g1(~L8{h(JY?Q=N4NZ*9KykFdE9qMdWG z*zmcNL?vD|cb8yUEw?G0+o@t@{o^t-0p6I*?nb%;4X=kVAp#Xiis_EDXe-$E+r-O} zF4+a5aQopynFDi%i31?HBqj}ouvd@uFKeXB$d=5=+gQEkXXWB>pG zC-b+M8r-JhNfiJNu@@IXNTI8rLhwSRNR}|^>0^~j_w(u<%^BF}MKc^?`WZfbHq8By zY&g2D?j#5r!J~pp^ng&iNbW(UJhD5zC{`vr4w98W%+;n@Zs|%&pZz1KAmo7P-$$g2 zu1fxQOg+ZD+zyJUuyNF6^k*gXhyDpdhXq>h0zQ!_qM)`B!W z#byi(k}0bivRzkmP3*D89rL`nc^J@@6D764RrFPh+B=E)6(O1^y%;HEpmtrILpXPD zCI7K0QJIdo2}A-lF*((ZNexw@lvXxq`Tl@Zy-=&0G&%%K3^uHs8`DLMQ91olSRt{ z_k|fwG(r|m_)feq80~0B;z6}@rjGChCm$~MG{NL@MqO-(K?KvnG*NmmQprH>gP#+PbO1xE25$h=c00G?oNM5B$DC&1~*IH@2U`Yl_L}LK= zI(>1fynrbbGO-{-dqx-8*h>gwi%-}$l9zC6%_pqLAy2njEdU0FeNuP`amD@Vti^jx=hdWlJpgb64LGiwbvfdINv@V5-iEHvgp-#%zX>Q*HV+$NuB zXWJ6p?$o@Mx_p@`lq$J7O!=CsY*8QNNy<|KhSn9}ZI$Q%08=wNbSFLSow7YLTyyfF z^g58U@5xVsu6F=t3qF+fwkeR#&t9v3^`5_4aE0hpC-cUXXU8lRD}e@42Yi7rddUQ60V4-s+RqcdjqhXGsm3HTop&nCGbS0 zU@+Zlz81%o2<5Nc(=S4?utY52lr#ru3sZ;;wPZc+Zo3rId1X@C{>lUB+Ab}1Nsk7T ztpbrsCjr10PEE_WYszI2uA4MaHquLw_rHX4vT&6^6ow*pop-!E`}fZoKmY)7R4OUC zcIA$C_Q(M15piVw!CW)K2#NuC?Qh z;AJRU(nPG+vOT&C%VEDtnVJN{dlS4Fev}+yVJp^W1I!;np8FUJJQsR|C7xd<|E;RR z<@o@=i6#mnyCQqF<`e;j>spvS+Q}7C-?I0y)N?paff6;MYS?!H-c56&M~|map}zJT zp<(~`V$LrMYplJHe9fC=^`K>Y^WjI;orU7+`{uueA#b`m*uyZS5o911reES_Xd%2z7nzlqxtxDA;LO+n z(}_?ow%tM2R4YBJ7F(JFm54{CQo&9vG8a7UX*IwC%E!k6y}%;07;Hu4mM_Gk>~B}h zxPNe!BW1Eot$nXrtw&mwu3%4ba6%1nVDvsg)+A42!ZV@DOgwVY_BIxTsCgEU0Snjn ze};yudB^klU;qlrF&OdUmTVXya}1&ufUF$Qk?NriMy}UrQN#n_B%j^alM3*gjh+T|}!C)e9PBH@8s)tagh0ugk-Mq9mk}ieq z9-{l}P!)hD&ByoX6)52ok!O_Wg8k@|yP3OsDYw|i1Q%ZRhv@bU3cG>_%8H>(Jc>C` z*d!I`pIh8Ipd^1n1h0F$qf!P7=~G%tE;4YcnV`%sWM*b7atWQ+)%z&E=bVdG4w5DK z(S?Zmwe?x%O>L6~gnNnigA0^L@}L(Z&{C%La?+)|t~OeUj44!YI6Iz)2$S(zy;=aj zJfS)w+w-*}&A6;>#G=BeKca}7KyYumNS;X)yz>`{4ajklTM2-^cm-DwE?FaGI(Hw* zRXUhKW%KQ`)m``sF{@gU~R>k^KJvAGjlvx`?9%pfGh>;SQ@_S~j@|LqtEk-SW%)J4ld|HAyV3`!Msd z*@&}rFVM(&Iqcuc-$()iW+@S|b_ zAI>g)#XC2P(+hGRA;Utz)FU09H&~eC>G+gL-u&$opyGgeG*`LTBPYq9q~`~I_8-#$1&EtNoYv7_m9}?Hl;*xA`tsfLbVB1;lRja zlDSu&uFVDq6F@7m!lsvX=LMMegZ&86A9+<8n)+(2!9tm#61Z*v^EJo3ye^kDmw6*Rh&eZ8k7F1Xb8RX3j;lqvF~xzudGe+nAOxYr9{6k9n^1FG&j zl%72bu8RNwqYwn7JOGz&A;v*UZ2nf7S0T&i!lAyZR&a))k%(Lu{;6X8l;|hfVY$0=73!8Yq{K2EwEOqSvk252xR-4p06P zQ22jJ&PHWxIz^-01{@0fjP1AGQ_wSATl_X#Uk~K&`mYQLu^(0}dezp|)FO!k+v<%o zk+t28J1UBRCx;2tYkk=pP)q(ak3J<#9}`j+us{(hgwxyMUK zdp0^^e7sY(MRcinJ(fIkh+WHI0V=Qkw-6PQc;5Zdx{E)j}Vsnu`g@XTL`9twP@dHmB0FuF5o1k(gTkJ;$OM_^t{_ zXkU^)EJaLKVBk2XoCbqt%dC(9C4ZsL0$B1_3am_nwgx zIRF3v1UUm`cyXN7MnTStwuj2hDsa8rj^DhQoWC*C&v2|1V|YvhZ2i2^9@~>54aTu! zhc_avj75?DX+$gKdLqP@uDHM^p4mO(ALF@0E$CtE4FqB-P2~kObg%aY7@8>^UO7}o zyo!rA2@&E2u&AP&S4UfbL;QCrW^`%=0qIZ7=U0BKb&2DR^$}@BBhG6Nf$vAO*?=n` z3}$FS?TbfbOWutSaZxRd(QpIzI&Q1(CjE9tTSP7NNwFr1>^(E}E-TMxT|Zg!>YW-^ zy3>O-uwr0@AsoJ%8}hcl+2JdKmPspgpvai-#|O}3x#wu1^rmPJb&G1$K^ugr8uIz0 z5oEk=fdtmvOpL?673jtErsxx@W(};1IiRU@l>+awqZ*%_a%GxUy3>O-uwr0@As?=y z^|1ra8dKn^D~0%u!m-L$!{xSUzd1+Qa*2#LM9Xw5<$5B-mae$K7>h&5&{F;~wDd?e z{MX%cj6Q_Qt$M;zqePnuNKvGO;ev7^`+E{3*afB%Xt|YPw!x``6H*2tAMXjXHY@9R zziN6x+m@%MgwhMu6RLc0dcZoK{{9F;zE`3wNowni0%`4&-XZ=wlrr9i9@vvFyW|Qh>ZSf0lDb4OOF_GT$~}@WH!xJu?lfwDH49N-N@YG%s?5Y>IzR?y z;Zg~M6kALH000000s+S6^MN|q>}A)PTd>L7Bvng@?)nMSo*i)Caxu`~O*^uGhPgWV zUtoI>!<)!5q=YO=L)p^PSb&IFMVu>oJ&d~ZCyG%wyvvp!lkz?=l?iy8DY&;WyTr*y z*CiE?!bdHMXQ~c8L5ebeOKN*F*}-bW(-7#UjNgD6zQy|QkJz);huT4{cYH@S-(Gns zQQ_=m*bTTalkkC{j6Vj+`-S*zrC<1ARET$zT8>00Wi2vo6JDo#+YB^pK`#C?-}_yM z9)YL;cpQ72ILaGwHtnx}^on$3j@)3-335(gil{grQBiQAl(fmO`2CALXnmv_cPARd zX!QA{zPi7!=56=aCcBisAF*iRxGS{Dk7r9y678dZ|IFt&XNrpSBIH*a`6aWG98pL= zyf{{X4+@O2@)ZN zQm6}h@=(eKJc(EpTO=b-1Q2>Q8qIC5+iF!`E`)U)mlUT_2t!$|uB_N~9^q)c;vttA znX!^J%Mjm9>biMels&ZJ+sT=v56zE66`!k7JVKzf2LSuMXc4SS)pJaO7VGx^kD@{Jh zY=cb@Y#33=+9VR49Vh|`aS~qdx)?_1 zhIj{w=c`TDh>ZkhKuDxZ*4zS#cfB!|S1P90oQI(@u~#%`Vtn$1Y4TO389ftp3i1FpD?K)ab;pbO0NVk%J@I7FX{)*w%0UT|(?NbMymh}|bz zwAT!KD?i9z#T21eSaU)rMVJryqI2mbiuB-Zeh^n~!!Mk+$Udj_)3(H7sGt6~Ut^;doLuV($zkf8tI zo-dWd|8)+CHV@^=xb=6R8?29%GpcNi{j1?4yIVy+wWW>#b$>UnPJ<&>4)vsGeoD_G z4JHeF8qQButD_(~NwWou^KDvb(h~uV2m?wQ?o?5fP<0cqp-kwzp~hZO9bbY_3}W2P{-4XMwv7XJR^DD zXidKsnk#}S{kSCjxhXOIMG&-sI}hq}15;{yVG+K8+5{?y03th#&;u#J01aBH{I$v3 zQJT_Hnidd*OEE2d2-O7MT)iU>0D_2eJDGLae}@MS|9-u@2?v9>mT3e zPSlgYRz13s>i|?euIAJdb`(|J2!0X5P+ycjk=k4IJ3C(&(|9?mzg+J4VNnq9%Rvi38i_u06!$)!mpjLfZRw=YHv&5~Cj-u!=k6rfV$A zssFbLb`|7`HpP2TOa7eKx%cwo*;|kYP@b+tS0m}IeffXx8!Kv^TxLtQa{9xlFBJ@* z6OuK{5a6~i_3DdgDS9f@w{J-da%??PJK!edK|z~RJgU^(1zleGLs?O*!Jk9~m9iWC z9i$0RNvP~J9WlwkWSOQsNasZ~B3kIB(1JXXuZK7B1=}sL@^=hIL^w3TV{YCiT)i?~ zvzOK#L3pTS_?(fhSce6^~kG#ZX_34mmTbl^Ty!!+8_&pXwVJ{-0&B8=;$`Ey_TagZQtZNhpG6CUu^b)XgcS+h9iM4$bB&kQ)a(OwC zL7BThku~UhiYDuT=%VysrILZ!c6AKl-Mp8+yAMqLON#T^%6!y6$8d`|m;Ws0N{B|M z5A_qY+p0}Yy0nB>71cSBL7BFZ`Rq#ObpPwrV|Yj#YnBmGlJkBOh-+-g@|fS)jR@bM zST~Ulbp!`jip%vn%Pu69=~HDO1irZ~Gzi8?0BQtJvP}{#3e$q%B`fYGMsPWCA_OV1 zn$jX35*uNqBl;CcG%(3QdME ze@Fx&pJc;2tOo(q#|DeH+ep`Pe;);sMZm-H5biWP+#U2na1LsXMz*GP*G%byM4QNa zOq$Nu> + Create a Etsy OAuth application{" "} + here + + ), + }], + }, + { + [EtsyTools.getListing]: getListingClientConfig + } +) \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/server.ts b/src/toolkits/toolkits/Etsy/server.ts new file mode 100644 index 00000000..adfc6856 --- /dev/null +++ b/src/toolkits/toolkits/Etsy/server.ts @@ -0,0 +1,15 @@ +import { createServerToolkit } from "../../create-toolkit"; +import { baseEtsyToolkitConfig } from "./base"; +import { EtsyTools } from "./tools/tools"; +import { getListingServerConfig } from "./tools/getListing/server"; +import { api } from "@/trpc/server"; +import { env } from "@/env"; +import EtsyProvider from "@/server/auth/custom-providers/etsy"; + +export const etsyToolkitServer = createServerToolkit( + baseEtsyToolkitConfig, + 'You have access to the Etsy toolkit for general account management. Currently, this toolkit provides:\n' + + '- **Get Listing**: Retrieve detailed information about a specific Etsy listing using its ID.\n\n' + async () => { + } +); \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/client.tsx b/src/toolkits/toolkits/Etsy/tools/getListing/client.tsx index c3922248..c9650882 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/client.tsx +++ b/src/toolkits/toolkits/Etsy/tools/getListing/client.tsx @@ -16,6 +16,7 @@ export const getListingClientConfig: ClientToolConfig<

Search Results for: {args.listingId}

{result.title}

+

{result.price}

) }; \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/wrapper.tsx b/src/toolkits/toolkits/Etsy/wrapper.tsx new file mode 100644 index 00000000..5134b5d9 --- /dev/null +++ b/src/toolkits/toolkits/Etsy/wrapper.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useState } from "react"; + +import { SiEtsy } from "@icons-pack/react-simple-icons"; + +import { signIn } from "next-auth/react"; + +import { api } from "@/trpc/react"; + +import { + AuthButton, + AuthRequiredDialog, +} from "@/toolkits/lib/auth-required-dialog"; + +import type { ClientToolkitWrapper } from "@/toolkits/types"; +import { Toolkits } from "../shared"; + +export const EtsyWrapper: ClientToolkitWrapper = ({ Item }) => { + const { data: hasAccount, isLoading } = + api.accounts.hasProviderAccount.useQuery("etsy"); + + const { data: hasAccess, isLoading: isLoadingAccess } = + api.features.hasFeature.useQuery({ + feature: "etsy", + }); + + const [isAuthRequiredDialogOpen, setIsAuthRequiredDialogOpen] = + useState(false); + + if (isLoading || isLoadingAccess) { + return ; + } + + if (!hasAccount || !hasAccess) { + return ( + <> + setIsAuthRequiredDialogOpen(true)} + /> + { + void signIn("etsy", { + callbackUrl: `${window.location.href}?${Toolkits.Etsy}=true`, + }); + }} + > + Connect + + } + /> + + ); + } + + return ; +}; \ No newline at end of file From 7a19342e9fca4c6f72737c27c2945ebba95103c0 Mon Sep 17 00:00:00 2001 From: Derrick Chen Date: Tue, 12 Aug 2025 23:47:38 -0400 Subject: [PATCH 05/18] add etsy --- package.json | 1 + pnpm-lock.yaml | 24 ++++++++++++++++++++++++ src/toolkits/toolkits/Etsy/server.ts | 11 +++++++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 76f01365..92e9294c 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "cmdk": "^1.1.1", "cookies-next": "^6.0.0", "date-fns": "^4.1.0", + "etsy-ts": "^4.2.0", "exa-js": "^1.8.8", "fast-deep-equal": "^3.1.3", "googleapis": "^150.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0db3f784..2b7e3bf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + etsy-ts: + specifier: ^4.2.0 + version: 4.2.0 exa-js: specifier: ^1.8.8 version: 1.8.8(encoding@0.1.13)(ws@8.18.2)(zod@3.25.56) @@ -2425,6 +2428,11 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axios-auth-refresh@3.3.6: + resolution: {integrity: sha512-2CeBUce/SxIfFxow5/n8vApJ97yYF6qoV4gh1UrswT7aEOnlOdBLxxyhOI4IaxGs6BY0l8YujU2jlc4aCmK17Q==} + peerDependencies: + axios: '>= 0.18 < 0.19.0 || >= 0.19.1' + axios@1.7.7: resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} @@ -3104,6 +3112,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + etsy-ts@4.2.0: + resolution: {integrity: sha512-YrVSiIP1s1FE7/isCnnkUR86te4+UABHfW7gWUsAHUDtPZj2NOw4VswabcfWxbtis+8pmZm7XQO0cQs8LZL+Fw==} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -7646,6 +7657,10 @@ snapshots: axe-core@4.10.3: {} + axios-auth-refresh@3.3.6(axios@1.7.7): + dependencies: + axios: 1.7.7 + axios@1.7.7: dependencies: follow-redirects: 1.15.9 @@ -8491,6 +8506,15 @@ snapshots: etag@1.8.1: {} + etsy-ts@4.2.0: + dependencies: + axios: 1.7.7 + axios-auth-refresh: 3.3.6(axios@1.7.7) + form-data: 4.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - debug + event-target-shim@5.0.1: {} eventemitter3@4.0.7: {} diff --git a/src/toolkits/toolkits/Etsy/server.ts b/src/toolkits/toolkits/Etsy/server.ts index adfc6856..7e99d35e 100644 --- a/src/toolkits/toolkits/Etsy/server.ts +++ b/src/toolkits/toolkits/Etsy/server.ts @@ -4,12 +4,19 @@ import { EtsyTools } from "./tools/tools"; import { getListingServerConfig } from "./tools/getListing/server"; import { api } from "@/trpc/server"; import { env } from "@/env"; -import EtsyProvider from "@/server/auth/custom-providers/etsy"; export const etsyToolkitServer = createServerToolkit( baseEtsyToolkitConfig, 'You have access to the Etsy toolkit for general account management. Currently, this toolkit provides:\n' + - '- **Get Listing**: Retrieve detailed information about a specific Etsy listing using its ID.\n\n' + '- **Get Listing**: Retrieve detailed information about a specific Etsy listing using its ID.\n\n', async () => { + const account = await api.accounts.getAccountByProvider("etsy"); + + if (!account) { + throw new Error("No Etsy account found"); + } + if (!account.access_token) { + throw new Error("No Etsy access token found"); + } } ); \ No newline at end of file From cc087ef4ab2d4d779e5db1e3d619a6739902fd1c Mon Sep 17 00:00:00 2001 From: Derrick Chen Date: Wed, 13 Aug 2025 00:12:41 -0400 Subject: [PATCH 06/18] add etsy toolkit to client --- src/toolkits/toolkits/Etsy/client.tsx | 2 +- src/toolkits/toolkits/Etsy/server.ts | 8 ++++++-- src/toolkits/toolkits/client.ts | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/toolkits/toolkits/Etsy/client.tsx b/src/toolkits/toolkits/Etsy/client.tsx index af0ad9ab..e72fd983 100644 --- a/src/toolkits/toolkits/Etsy/client.tsx +++ b/src/toolkits/toolkits/Etsy/client.tsx @@ -7,7 +7,7 @@ import { Link } from "../components/link"; import { EtsyTools } from "./tools/tools"; import { getListingClientConfig } from "./tools/getListing/client"; -export const EtsyClientToolkit = createClientToolkit( +export const etsyClientToolkit = createClientToolkit( baseEtsyToolkitConfig, { name: "Etsy Toolkit", diff --git a/src/toolkits/toolkits/Etsy/server.ts b/src/toolkits/toolkits/Etsy/server.ts index 7e99d35e..0cda0f16 100644 --- a/src/toolkits/toolkits/Etsy/server.ts +++ b/src/toolkits/toolkits/Etsy/server.ts @@ -4,7 +4,7 @@ import { EtsyTools } from "./tools/tools"; import { getListingServerConfig } from "./tools/getListing/server"; import { api } from "@/trpc/server"; import { env } from "@/env"; - +import { Etsy } from 'etsy-ts' export const etsyToolkitServer = createServerToolkit( baseEtsyToolkitConfig, 'You have access to the Etsy toolkit for general account management. Currently, this toolkit provides:\n' + @@ -18,5 +18,9 @@ export const etsyToolkitServer = createServerToolkit( if (!account.access_token) { throw new Error("No Etsy access token found"); } - } + + return { + [EtsyTools.getListing]: getListingServerConfig + }; + } ); \ No newline at end of file diff --git a/src/toolkits/toolkits/client.ts b/src/toolkits/toolkits/client.ts index ccf3140a..7139b60d 100644 --- a/src/toolkits/toolkits/client.ts +++ b/src/toolkits/toolkits/client.ts @@ -15,6 +15,7 @@ import { notionClientToolkit } from "./notion/client"; import { e2bClientToolkit } from "./e2b/client"; import { stravaClientToolkit } from "./strava/client"; import { spotifyClientToolkit } from "./spotify/client"; +import { etsyClientToolkit } from "./Etsy/client"; export type ClientToolkits = { [K in Toolkits]: ClientToolkit< @@ -34,6 +35,7 @@ export const clientToolkits: ClientToolkits = { [Toolkits.GoogleDrive]: googleDriveClientToolkit, [Toolkits.Strava]: stravaClientToolkit, [Toolkits.Spotify]: spotifyClientToolkit, + [Toolkits.Etsy]: etsyClientToolkit, }; export function getClientToolkit( From e738fc8c4e1adc2ae3e0f3cc9c4b8cbd0013f2fc Mon Sep 17 00:00:00 2001 From: Derrick Chen Date: Wed, 13 Aug 2025 17:05:50 -0400 Subject: [PATCH 07/18] update ETsy toolkit params --- src/toolkits/toolkits/Etsy/base.ts | 5 +---- src/toolkits/toolkits/Etsy/wrapper.tsx | 8 ++------ src/toolkits/toolkits/shared.ts | 5 +++++ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/toolkits/toolkits/Etsy/base.ts b/src/toolkits/toolkits/Etsy/base.ts index 77b90716..d9d5be42 100644 --- a/src/toolkits/toolkits/Etsy/base.ts +++ b/src/toolkits/toolkits/Etsy/base.ts @@ -4,10 +4,7 @@ import { EtsyTools } from "./tools/tools"; import { getListing } from "./tools/getListing/base"; -export const etsyParameters = z.object({ - tokens: z.string().describe("Etsy authorization token"), - listingId: z.string().describe("The ID of the Etsy listing to query"), -}); +export const etsyParameters = z.object({}); export const baseEtsyToolkitConfig: ToolkitConfig< EtsyTools, diff --git a/src/toolkits/toolkits/Etsy/wrapper.tsx b/src/toolkits/toolkits/Etsy/wrapper.tsx index 5134b5d9..00605934 100644 --- a/src/toolkits/toolkits/Etsy/wrapper.tsx +++ b/src/toolkits/toolkits/Etsy/wrapper.tsx @@ -20,19 +20,15 @@ export const EtsyWrapper: ClientToolkitWrapper = ({ Item }) => { const { data: hasAccount, isLoading } = api.accounts.hasProviderAccount.useQuery("etsy"); - const { data: hasAccess, isLoading: isLoadingAccess } = - api.features.hasFeature.useQuery({ - feature: "etsy", - }); const [isAuthRequiredDialogOpen, setIsAuthRequiredDialogOpen] = useState(false); - if (isLoading || isLoadingAccess) { + if (isLoading) { return ; } - if (!hasAccount || !hasAccess) { + if (!hasAccount) { return ( <> Date: Wed, 13 Aug 2025 18:09:54 -0400 Subject: [PATCH 08/18] modify to return all listings associated with the signed in account --- .../toolkits/Etsy/tools/getListing/base.ts | 18 +++--- .../toolkits/Etsy/tools/getListing/server.ts | 63 ++++++++++--------- src/toolkits/toolkits/server.ts | 3 + 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/base.ts b/src/toolkits/toolkits/Etsy/tools/getListing/base.ts index e20cb45a..4cea4d33 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/base.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListing/base.ts @@ -1,17 +1,19 @@ import { z } from "zod"; import { createBaseTool } from "@/toolkits/create-tool"; +import { ShopListing } from "etsy-ts/dist/api/ShopListing"; export const getListing = createBaseTool({ description: "Get details about an Etsy listing", inputSchema: z.object({ - tokens: z.object({access_tokens: z.string()}).describe("Etsy authorization token"), - listingId: z.string().describe("The ID of the Etsy listing to query"), }), outputSchema: z.object({ - title: z.string().describe("The title of the Etsy listing"), - description: z.string().describe("The description of the Etsy listing"), - price: z.number().describe("The price of the Etsy listing"), - currencyCode: z.string().describe("The currency code for the price"), - images: z.array(z.string()).describe("URLs of images for the Etsy listing"), - }), + listings: z.array(z.custom()) + }) + // outputSchema: z.object({ + // title: z.string().describe("The title of the Etsy listing"), + // description: z.string().describe("The description of the Etsy listing"), + // price: z.number().describe("The price of the Etsy listing"), + // currencyCode: z.string().describe("The currency code for the price"), + // images: z.array(z.string()).describe("URLs of images for the Etsy listing"), + // }), }); \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts index 1f7d1e07..1d2d1970 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts @@ -1,36 +1,39 @@ import type { ServerToolConfig } from "@/toolkits/types"; import type { getListing } from "./base"; +import type { Etsy } from "etsy-ts"; +import { api } from "@/trpc/server"; -export const getListingServerConfig: ServerToolConfig< - typeof getListing.inputSchema.shape - > = { - callback: async ({ tokens, listingId }: { tokens: { access_tokens: string }, listingId: string }) => { - const response = await fetch( - `https://openapi.etsy.com/v3/application/listings/${listingId}`, - { - headers: { - "x-api-key": process.env.AUTH_ETSY_ID || "", - Authorization: `Bearer ${tokens.access_tokens}` - }, - }, - ); - if (!response.ok) { - throw new Error(`Failed to fetch listing: ${response.statusText}`); - } +export const getListingServerConfig = ( + etsy: Etsy + ): ServerToolConfig< + typeof getListing.inputSchema.shape, + typeof getListing.outputSchema.shape +> => { + return { + callback: async () => { + try { + const account = await api.accounts.getAccountByProvider("etsy"); + const userID = account?.providerAccountId; + const etsyUserId = Number(userID); - const data = await response.json(); - return { - title: data.title, - description: data.description, - price: data.price, - currencyCode: data.currency_code, - images: data.images.map((image: any) => ({ - url: image.url_170x135, - fullUrl: image.url_fullxfull, - })), - }; - }, - message: (result) => 'Tool completed' -} \ No newline at end of file + const shop = await etsy.Shop.getShopByOwnerUserId(etsyUserId, {etsyUserId}); + const shopId = shop.data.shop_id; + if (typeof shopId !== "number") { + throw new Error("shop_id is undefined"); + } + const listings = await etsy.ShopListing.getListingsByShop({ shopId: shopId }, { etsyUserId }); + return { + listings: listings.data, + }; + } catch (error) { + console.error("Etsy API error:", error); + throw new Error("Failed to fetch listings from Etsy"); + } + }, + message: + "Successfully retrieved the Etsy listing. The user is shown the responses in the UI. Do not reiterate them. The user is shown the responses in the UI. " + + "If you called this tool because the user asked a question, answer the question.", + }; +}; \ No newline at end of file diff --git a/src/toolkits/toolkits/server.ts b/src/toolkits/toolkits/server.ts index e65e3c19..360e76ad 100644 --- a/src/toolkits/toolkits/server.ts +++ b/src/toolkits/toolkits/server.ts @@ -9,6 +9,8 @@ import { notionToolkitServer } from "./notion/server"; import { e2bToolkitServer } from "./e2b/server"; import { stravaToolkitServer } from "./strava/server"; import { spotifyToolkitServer } from "./spotify/server"; +import { etsyToolkitServer } from "./Etsy/server"; + import { Toolkits, type ServerToolkitNames, @@ -33,6 +35,7 @@ export const serverToolkits: ServerToolkits = { [Toolkits.E2B]: e2bToolkitServer, [Toolkits.Strava]: stravaToolkitServer, [Toolkits.Spotify]: spotifyToolkitServer, + [Toolkits.Etsy]: etsyToolkitServer, }; export function getServerToolkit( From 8a51b5cf2035dba923f6dbd419fc1b9f33aa8171 Mon Sep 17 00:00:00 2001 From: Derrick Chen Date: Wed, 13 Aug 2025 18:18:18 -0400 Subject: [PATCH 09/18] try adding client --- src/toolkits/toolkits/Etsy/server.ts | 9 +++++++-- src/toolkits/toolkits/Etsy/tools/getListing/base.ts | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/toolkits/toolkits/Etsy/server.ts b/src/toolkits/toolkits/Etsy/server.ts index 0cda0f16..73634b95 100644 --- a/src/toolkits/toolkits/Etsy/server.ts +++ b/src/toolkits/toolkits/Etsy/server.ts @@ -8,7 +8,7 @@ import { Etsy } from 'etsy-ts' export const etsyToolkitServer = createServerToolkit( baseEtsyToolkitConfig, 'You have access to the Etsy toolkit for general account management. Currently, this toolkit provides:\n' + - '- **Get Listing**: Retrieve detailed information about a specific Etsy listing using its ID.\n\n', + '- **Get Listings**: Retrieves all listings associated with the shop associated with the signed-in user.\n\n', async () => { const account = await api.accounts.getAccountByProvider("etsy"); @@ -19,8 +19,13 @@ export const etsyToolkitServer = createServerToolkit( throw new Error("No Etsy access token found"); } + const etsy = new Etsy({ + apiKey: env.AUTH_ETSY_ID, + // TODO:: workaround for ISecurityDataStorage + }); + return { - [EtsyTools.getListing]: getListingServerConfig + [EtsyTools.getListing]: getListingServerConfig(etsy) }; } ); \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/base.ts b/src/toolkits/toolkits/Etsy/tools/getListing/base.ts index 4cea4d33..bf4cc61f 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/base.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListing/base.ts @@ -1,13 +1,14 @@ import { z } from "zod"; import { createBaseTool } from "@/toolkits/create-tool"; import { ShopListing } from "etsy-ts/dist/api/ShopListing"; +import type { IShopListingsWithAssociations } from "etsy-ts"; export const getListing = createBaseTool({ - description: "Get details about an Etsy listing", + description: "Fetches all listings from the Etsy shop associated with the authenticated user.", inputSchema: z.object({ }), outputSchema: z.object({ - listings: z.array(z.custom()) + listings: z.custom() }) // outputSchema: z.object({ // title: z.string().describe("The title of the Etsy listing"), From 8d26db1f6c3a16eb9f0f789990fa82a7646578f3 Mon Sep 17 00:00:00 2001 From: Derrick Chen Date: Sun, 17 Aug 2025 15:45:44 -0400 Subject: [PATCH 10/18] everything working --- src/toolkits/toolkits/Etsy/server.ts | 7 +--- .../toolkits/Etsy/tools/getListing/base.ts | 1 - .../toolkits/Etsy/tools/getListing/server.ts | 40 ++++++++++++++----- src/toolkits/toolkits/Etsy/wrapper.tsx | 2 +- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/toolkits/toolkits/Etsy/server.ts b/src/toolkits/toolkits/Etsy/server.ts index 73634b95..55b41838 100644 --- a/src/toolkits/toolkits/Etsy/server.ts +++ b/src/toolkits/toolkits/Etsy/server.ts @@ -19,13 +19,8 @@ export const etsyToolkitServer = createServerToolkit( throw new Error("No Etsy access token found"); } - const etsy = new Etsy({ - apiKey: env.AUTH_ETSY_ID, - // TODO:: workaround for ISecurityDataStorage - }); - return { - [EtsyTools.getListing]: getListingServerConfig(etsy) + [EtsyTools.getListing]: getListingServerConfig() }; } ); \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/base.ts b/src/toolkits/toolkits/Etsy/tools/getListing/base.ts index bf4cc61f..8e93147c 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/base.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListing/base.ts @@ -8,7 +8,6 @@ export const getListing = createBaseTool({ inputSchema: z.object({ }), outputSchema: z.object({ - listings: z.custom() }) // outputSchema: z.object({ // title: z.string().describe("The title of the Etsy listing"), diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts index 1d2d1970..8bb196c6 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts @@ -1,11 +1,10 @@ import type { ServerToolConfig } from "@/toolkits/types"; import type { getListing } from "./base"; -import type { Etsy } from "etsy-ts"; import { api } from "@/trpc/server"; export const getListingServerConfig = ( - etsy: Etsy + ): ServerToolConfig< typeof getListing.inputSchema.shape, typeof getListing.outputSchema.shape @@ -16,16 +15,39 @@ export const getListingServerConfig = ( const account = await api.accounts.getAccountByProvider("etsy"); const userID = account?.providerAccountId; const etsyUserId = Number(userID); + const apiKey = process.env.AUTH_ETSY_ID; + if (!apiKey) throw new Error("Missing AUTH_ETSY_ID"); + const accessToken = account?.access_token; + if (!accessToken) throw new Error("Missing Etsy access token"); + + + + const shopResponse = await fetch( + `https://openapi.etsy.com/v3/application/users/${etsyUserId}/shops`, + { + headers: { + "x-api-key": apiKey, + Authorization: `Bearer ${accessToken}`, + } + }, + ); + + const shop = (await shopResponse.json()); + + const listingResponse = await fetch( + `https://openapi.etsy.com/v3/application/shops/${shop.shop_id}/listings`, + { + headers: { + "x-api-key": apiKey, + Authorization: `Bearer ${accessToken}`, + } + }, + ); + const listings = (await listingResponse.json()); - const shop = await etsy.Shop.getShopByOwnerUserId(etsyUserId, {etsyUserId}); - const shopId = shop.data.shop_id; - if (typeof shopId !== "number") { - throw new Error("shop_id is undefined"); - } - const listings = await etsy.ShopListing.getListingsByShop({ shopId: shopId }, { etsyUserId }); return { - listings: listings.data, + listings: listings, }; } catch (error) { console.error("Etsy API error:", error); diff --git a/src/toolkits/toolkits/Etsy/wrapper.tsx b/src/toolkits/toolkits/Etsy/wrapper.tsx index 00605934..4a68ba14 100644 --- a/src/toolkits/toolkits/Etsy/wrapper.tsx +++ b/src/toolkits/toolkits/Etsy/wrapper.tsx @@ -28,7 +28,7 @@ export const EtsyWrapper: ClientToolkitWrapper = ({ Item }) => { return ; } - if (!hasAccount) { + if (!hasAccount || true) { return ( <> Date: Sun, 17 Aug 2025 17:03:32 -0400 Subject: [PATCH 11/18] refresh token logic for expired access tokens --- src/server/auth/custom-providers/etsy.ts | 38 ++++++++++++++++++- .../toolkits/Etsy/tools/getListing/server.ts | 9 ++++- src/toolkits/toolkits/Etsy/wrapper.tsx | 3 +- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/server/auth/custom-providers/etsy.ts b/src/server/auth/custom-providers/etsy.ts index 13d032bc..0e721c74 100644 --- a/src/server/auth/custom-providers/etsy.ts +++ b/src/server/auth/custom-providers/etsy.ts @@ -1,5 +1,6 @@ import { env } from "@/env"; import type { OAuth2Config, OAuthUserConfig } from "@auth/core/providers"; +import {db} from "@/server/db"; export interface EtsyProfile { user_id: number; // The numeric ID of a user. Also a valid shop ID. @@ -21,7 +22,7 @@ export default function EtsyProvider

( authorization: { url: "https://www.etsy.com/oauth/connect", params: { - scope: "email_r", + scope: "email_r listing_r", state: Math.random().toString(36).substring(2, 15), }, }, @@ -63,3 +64,38 @@ export default function EtsyProvider

( options, }; } + +export async function refreshEtsyAccessToken( + refreshToken: string, + providerAccountId: string +) { + const response = await fetch("https://api.etsy.com/v3/public/oauth/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "refresh_token", + client_id: process.env.AUTH_ETSY_ID!, // your app's client_id + refresh_token: refreshToken, // the one you stored + }), + }); + if (!response.ok) { + throw new Error("Failed to refresh Etsy access token"); + } + const tokens = await response.json(); + + await db.account.update({ + where: { + provider_providerAccountId: { + provider: "etsy", + providerAccountId: providerAccountId, + }, + }, + data: { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + scope: tokens.scope, + token_type: tokens.token_type, + expires_at: tokens.expires_at, + }, + }); +} diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts index 8bb196c6..831d6529 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListing/server.ts @@ -1,6 +1,7 @@ import type { ServerToolConfig } from "@/toolkits/types"; import type { getListing } from "./base"; import { api } from "@/trpc/server"; +import { refreshEtsyAccessToken} from "@/server/auth/custom-providers/etsy"; export const getListingServerConfig = ( @@ -16,12 +17,18 @@ export const getListingServerConfig = ( const userID = account?.providerAccountId; const etsyUserId = Number(userID); const apiKey = process.env.AUTH_ETSY_ID; + const refreshToken = account?.refresh_token; + const accessExpiry = account?.expires_at; + if (!apiKey) throw new Error("Missing AUTH_ETSY_ID"); + if (accessExpiry && refreshToken && userID && (accessExpiry < Date.now() / 1000)) { + await refreshEtsyAccessToken(refreshToken, userID); + } + const accessToken = account?.access_token; if (!accessToken) throw new Error("Missing Etsy access token"); - const shopResponse = await fetch( `https://openapi.etsy.com/v3/application/users/${etsyUserId}/shops`, { diff --git a/src/toolkits/toolkits/Etsy/wrapper.tsx b/src/toolkits/toolkits/Etsy/wrapper.tsx index 4a68ba14..9b71a4e9 100644 --- a/src/toolkits/toolkits/Etsy/wrapper.tsx +++ b/src/toolkits/toolkits/Etsy/wrapper.tsx @@ -20,7 +20,6 @@ export const EtsyWrapper: ClientToolkitWrapper = ({ Item }) => { const { data: hasAccount, isLoading } = api.accounts.hasProviderAccount.useQuery("etsy"); - const [isAuthRequiredDialogOpen, setIsAuthRequiredDialogOpen] = useState(false); @@ -28,7 +27,7 @@ export const EtsyWrapper: ClientToolkitWrapper = ({ Item }) => { return ; } - if (!hasAccount || true) { + if (!hasAccount) { return ( <> Date: Sun, 17 Aug 2025 17:31:32 -0400 Subject: [PATCH 12/18] refactor to bulk getListings rather than getListing --- src/toolkits/toolkits/Etsy/base.ts | 4 +-- src/toolkits/toolkits/Etsy/client.tsx | 4 +-- src/toolkits/toolkits/Etsy/server.ts | 8 +++--- .../tools/{getListing => getListings}/base.ts | 2 +- .../{getListing => getListings}/client.tsx | 13 ++++----- .../{getListing => getListings}/server.ts | 27 +++++++++++++++---- src/toolkits/toolkits/Etsy/tools/tools.ts | 2 +- 7 files changed, 36 insertions(+), 24 deletions(-) rename src/toolkits/toolkits/Etsy/tools/{getListing => getListings}/base.ts (94%) rename src/toolkits/toolkits/Etsy/tools/{getListing => getListings}/client.tsx (51%) rename src/toolkits/toolkits/Etsy/tools/{getListing => getListings}/server.ts (68%) diff --git a/src/toolkits/toolkits/Etsy/base.ts b/src/toolkits/toolkits/Etsy/base.ts index d9d5be42..c5016970 100644 --- a/src/toolkits/toolkits/Etsy/base.ts +++ b/src/toolkits/toolkits/Etsy/base.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import type { ToolkitConfig } from "@/toolkits/types"; import { EtsyTools } from "./tools/tools"; -import { getListing } from "./tools/getListing/base"; +import { getListings } from "@/toolkits/toolkits/Etsy/tools/getListings/base"; export const etsyParameters = z.object({}); @@ -11,7 +11,7 @@ export const baseEtsyToolkitConfig: ToolkitConfig< typeof etsyParameters.shape > = { tools: { - [EtsyTools.getListing]: getListing + [EtsyTools.getListings]: getListings }, parameters: etsyParameters, } \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/client.tsx b/src/toolkits/toolkits/Etsy/client.tsx index e72fd983..66ca6125 100644 --- a/src/toolkits/toolkits/Etsy/client.tsx +++ b/src/toolkits/toolkits/Etsy/client.tsx @@ -5,7 +5,7 @@ import { baseEtsyToolkitConfig } from "./base"; import { EtsyWrapper } from "./wrapper"; import { Link } from "../components/link"; import { EtsyTools } from "./tools/tools"; -import { getListingClientConfig } from "./tools/getListing/client"; +import { getListingsClientConfig } from "@/toolkits/toolkits/Etsy/tools/getListings/client"; export const etsyClientToolkit = createClientToolkit( baseEtsyToolkitConfig, @@ -29,6 +29,6 @@ export const etsyClientToolkit = createClientToolkit( }], }, { - [EtsyTools.getListing]: getListingClientConfig + [EtsyTools.getListings]: getListingsClientConfig } ) \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/server.ts b/src/toolkits/toolkits/Etsy/server.ts index 55b41838..784b9449 100644 --- a/src/toolkits/toolkits/Etsy/server.ts +++ b/src/toolkits/toolkits/Etsy/server.ts @@ -1,14 +1,12 @@ import { createServerToolkit } from "../../create-toolkit"; import { baseEtsyToolkitConfig } from "./base"; import { EtsyTools } from "./tools/tools"; -import { getListingServerConfig } from "./tools/getListing/server"; +import { getListingsServerConfig } from "@/toolkits/toolkits/Etsy/tools/getListings/server"; import { api } from "@/trpc/server"; -import { env } from "@/env"; -import { Etsy } from 'etsy-ts' export const etsyToolkitServer = createServerToolkit( baseEtsyToolkitConfig, 'You have access to the Etsy toolkit for general account management. Currently, this toolkit provides:\n' + - '- **Get Listings**: Retrieves all listings associated with the shop associated with the signed-in user.\n\n', + '- **Get Listings**: Retrieves all listings and their image URLs associated with the shop associated with the signed-in user.\n\n', async () => { const account = await api.accounts.getAccountByProvider("etsy"); @@ -20,7 +18,7 @@ export const etsyToolkitServer = createServerToolkit( } return { - [EtsyTools.getListing]: getListingServerConfig() + [EtsyTools.getListings]: getListingsServerConfig() }; } ); \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/base.ts b/src/toolkits/toolkits/Etsy/tools/getListings/base.ts similarity index 94% rename from src/toolkits/toolkits/Etsy/tools/getListing/base.ts rename to src/toolkits/toolkits/Etsy/tools/getListings/base.ts index 8e93147c..7016066e 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/base.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListings/base.ts @@ -3,7 +3,7 @@ import { createBaseTool } from "@/toolkits/create-tool"; import { ShopListing } from "etsy-ts/dist/api/ShopListing"; import type { IShopListingsWithAssociations } from "etsy-ts"; -export const getListing = createBaseTool({ +export const getListings = createBaseTool({ description: "Fetches all listings from the Etsy shop associated with the authenticated user.", inputSchema: z.object({ }), diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/client.tsx b/src/toolkits/toolkits/Etsy/tools/getListings/client.tsx similarity index 51% rename from src/toolkits/toolkits/Etsy/tools/getListing/client.tsx rename to src/toolkits/toolkits/Etsy/tools/getListings/client.tsx index c9650882..bf54fedd 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/client.tsx +++ b/src/toolkits/toolkits/Etsy/tools/getListings/client.tsx @@ -1,22 +1,19 @@ import type { ClientToolConfig} from "@/toolkits/types"; -import type { getListing } from "./base"; +import type { getListings } from "./base"; -export const getListingClientConfig: ClientToolConfig< - typeof getListing.inputSchema.shape, - typeof getListing.outputSchema.shape +export const getListingsClientConfig: ClientToolConfig< + typeof getListings.inputSchema.shape, + typeof getListings.outputSchema.shape > = { CallComponent: ({ args, isPartial }) => (

🔍 - Searching for: {args.listingId} {isPartial && ...}
), ResultComponent: ({ args, result }) => (
-

Search Results for: {args.listingId}

-

{result.title}

-

{result.price}

+
) }; \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts b/src/toolkits/toolkits/Etsy/tools/getListings/server.ts similarity index 68% rename from src/toolkits/toolkits/Etsy/tools/getListing/server.ts rename to src/toolkits/toolkits/Etsy/tools/getListings/server.ts index 831d6529..d1fdcaea 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListing/server.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListings/server.ts @@ -1,14 +1,14 @@ import type { ServerToolConfig } from "@/toolkits/types"; -import type { getListing } from "./base"; +import type { getListings } from "./base"; import { api } from "@/trpc/server"; import { refreshEtsyAccessToken} from "@/server/auth/custom-providers/etsy"; -export const getListingServerConfig = ( +export const getListingsServerConfig = ( ): ServerToolConfig< - typeof getListing.inputSchema.shape, - typeof getListing.outputSchema.shape + typeof getListings.inputSchema.shape, + typeof getListings.outputSchema.shape > => { return { callback: async () => { @@ -52,6 +52,23 @@ export const getListingServerConfig = ( ); const listings = (await listingResponse.json()); + // this is going to be very inefficient code for large shops, but let's do this for now. I can raise QPS ratelimits if needed + // for each listing, fetch the images + for (let i = 0; i < listings.results.length; i++) { + const listing = listings.results[i]; + const imageResponse = await fetch( + `https://openapi.etsy.com/v3/application/listings/${listing.listing_id}/images`, + { + headers: { + "x-api-key": apiKey, + Authorization: `Bearer ${accessToken}`, + } + }, + ); + const images = (await imageResponse.json()); + listing.images = images.results; + } + return { listings: listings, @@ -62,7 +79,7 @@ export const getListingServerConfig = ( } }, message: - "Successfully retrieved the Etsy listing. The user is shown the responses in the UI. Do not reiterate them. The user is shown the responses in the UI. " + + "Successfully retrieved the Etsy listing. The user is shown the responses in the UI. Do not reiterate them. " + "If you called this tool because the user asked a question, answer the question.", }; }; \ No newline at end of file diff --git a/src/toolkits/toolkits/Etsy/tools/tools.ts b/src/toolkits/toolkits/Etsy/tools/tools.ts index c2761dd5..5a9ea648 100644 --- a/src/toolkits/toolkits/Etsy/tools/tools.ts +++ b/src/toolkits/toolkits/Etsy/tools/tools.ts @@ -1,3 +1,3 @@ export enum EtsyTools { - getListing = "get-listing", + getListings = "get-listings", } \ No newline at end of file From 1cc399111b262dedad28638c452de87c1ae5c137 Mon Sep 17 00:00:00 2001 From: Derrick Chen Date: Sun, 17 Aug 2025 17:51:20 -0400 Subject: [PATCH 13/18] prettier fix --- src/server/auth/custom-providers/etsy.ts | 8 ++--- src/toolkits/toolkits/Etsy/base.ts | 5 ++-- src/toolkits/toolkits/Etsy/client.tsx | 9 +++--- src/toolkits/toolkits/Etsy/server.ts | 12 ++++---- .../toolkits/Etsy/tools/getListings/base.ts | 11 ++++--- .../Etsy/tools/getListings/client.tsx | 10 +++---- .../toolkits/Etsy/tools/getListings/server.ts | 30 +++++++++---------- src/toolkits/toolkits/Etsy/tools/tools.ts | 2 +- src/toolkits/toolkits/Etsy/wrapper.tsx | 2 +- 9 files changed, 43 insertions(+), 46 deletions(-) diff --git a/src/server/auth/custom-providers/etsy.ts b/src/server/auth/custom-providers/etsy.ts index 0e721c74..19f3774f 100644 --- a/src/server/auth/custom-providers/etsy.ts +++ b/src/server/auth/custom-providers/etsy.ts @@ -1,6 +1,6 @@ import { env } from "@/env"; import type { OAuth2Config, OAuthUserConfig } from "@auth/core/providers"; -import {db} from "@/server/db"; +import { db } from "@/server/db"; export interface EtsyProfile { user_id: number; // The numeric ID of a user. Also a valid shop ID. @@ -66,8 +66,8 @@ export default function EtsyProvider

( } export async function refreshEtsyAccessToken( - refreshToken: string, - providerAccountId: string + refreshToken: string, + providerAccountId: string, ) { const response = await fetch("https://api.etsy.com/v3/public/oauth/token", { method: "POST", @@ -75,7 +75,7 @@ export async function refreshEtsyAccessToken( body: new URLSearchParams({ grant_type: "refresh_token", client_id: process.env.AUTH_ETSY_ID!, // your app's client_id - refresh_token: refreshToken, // the one you stored + refresh_token: refreshToken, // the one you stored }), }); if (!response.ok) { diff --git a/src/toolkits/toolkits/Etsy/base.ts b/src/toolkits/toolkits/Etsy/base.ts index c5016970..8fbcbc01 100644 --- a/src/toolkits/toolkits/Etsy/base.ts +++ b/src/toolkits/toolkits/Etsy/base.ts @@ -3,7 +3,6 @@ import type { ToolkitConfig } from "@/toolkits/types"; import { EtsyTools } from "./tools/tools"; import { getListings } from "@/toolkits/toolkits/Etsy/tools/getListings/base"; - export const etsyParameters = z.object({}); export const baseEtsyToolkitConfig: ToolkitConfig< @@ -11,7 +10,7 @@ export const baseEtsyToolkitConfig: ToolkitConfig< typeof etsyParameters.shape > = { tools: { - [EtsyTools.getListings]: getListings + [EtsyTools.getListings]: getListings, }, parameters: etsyParameters, -} \ No newline at end of file +}; diff --git a/src/toolkits/toolkits/Etsy/client.tsx b/src/toolkits/toolkits/Etsy/client.tsx index 66ca6125..20e925a8 100644 --- a/src/toolkits/toolkits/Etsy/client.tsx +++ b/src/toolkits/toolkits/Etsy/client.tsx @@ -26,9 +26,10 @@ export const etsyClientToolkit = createClientToolkit( here ), - }], + }, + ], }, { - [EtsyTools.getListings]: getListingsClientConfig - } -) \ No newline at end of file + [EtsyTools.getListings]: getListingsClientConfig, + }, +); diff --git a/src/toolkits/toolkits/Etsy/server.ts b/src/toolkits/toolkits/Etsy/server.ts index 784b9449..726bbe85 100644 --- a/src/toolkits/toolkits/Etsy/server.ts +++ b/src/toolkits/toolkits/Etsy/server.ts @@ -5,8 +5,8 @@ import { getListingsServerConfig } from "@/toolkits/toolkits/Etsy/tools/getListi import { api } from "@/trpc/server"; export const etsyToolkitServer = createServerToolkit( baseEtsyToolkitConfig, - 'You have access to the Etsy toolkit for general account management. Currently, this toolkit provides:\n' + - '- **Get Listings**: Retrieves all listings and their image URLs associated with the shop associated with the signed-in user.\n\n', + "You have access to the Etsy toolkit for general account management. Currently, this toolkit provides:\n" + + "- **Get Listings**: Retrieves all listings and their image URLs associated with the shop associated with the signed-in user.\n\n", async () => { const account = await api.accounts.getAccountByProvider("etsy"); @@ -18,7 +18,7 @@ export const etsyToolkitServer = createServerToolkit( } return { - [EtsyTools.getListings]: getListingsServerConfig() - }; - } -); \ No newline at end of file + [EtsyTools.getListings]: getListingsServerConfig(), + }; + }, +); diff --git a/src/toolkits/toolkits/Etsy/tools/getListings/base.ts b/src/toolkits/toolkits/Etsy/tools/getListings/base.ts index 7016066e..a7b28e41 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListings/base.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListings/base.ts @@ -4,11 +4,10 @@ import { ShopListing } from "etsy-ts/dist/api/ShopListing"; import type { IShopListingsWithAssociations } from "etsy-ts"; export const getListings = createBaseTool({ - description: "Fetches all listings from the Etsy shop associated with the authenticated user.", - inputSchema: z.object({ - }), - outputSchema: z.object({ - }) + description: + "Fetches all listings from the Etsy shop associated with the authenticated user.", + inputSchema: z.object({}), + outputSchema: z.object({}), // outputSchema: z.object({ // title: z.string().describe("The title of the Etsy listing"), // description: z.string().describe("The description of the Etsy listing"), @@ -16,4 +15,4 @@ export const getListings = createBaseTool({ // currencyCode: z.string().describe("The currency code for the price"), // images: z.array(z.string()).describe("URLs of images for the Etsy listing"), // }), -}); \ No newline at end of file +}); diff --git a/src/toolkits/toolkits/Etsy/tools/getListings/client.tsx b/src/toolkits/toolkits/Etsy/tools/getListings/client.tsx index bf54fedd..da24f34f 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListings/client.tsx +++ b/src/toolkits/toolkits/Etsy/tools/getListings/client.tsx @@ -1,4 +1,4 @@ -import type { ClientToolConfig} from "@/toolkits/types"; +import type { ClientToolConfig } from "@/toolkits/types"; import type { getListings } from "./base"; export const getListingsClientConfig: ClientToolConfig< @@ -12,8 +12,6 @@ export const getListingsClientConfig: ClientToolConfig< ), ResultComponent: ({ args, result }) => ( -

- -
- ) -}; \ No newline at end of file +
+ ), +}; diff --git a/src/toolkits/toolkits/Etsy/tools/getListings/server.ts b/src/toolkits/toolkits/Etsy/tools/getListings/server.ts index d1fdcaea..116b68eb 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListings/server.ts +++ b/src/toolkits/toolkits/Etsy/tools/getListings/server.ts @@ -1,12 +1,9 @@ import type { ServerToolConfig } from "@/toolkits/types"; import type { getListings } from "./base"; import { api } from "@/trpc/server"; -import { refreshEtsyAccessToken} from "@/server/auth/custom-providers/etsy"; +import { refreshEtsyAccessToken } from "@/server/auth/custom-providers/etsy"; - -export const getListingsServerConfig = ( - - ): ServerToolConfig< +export const getListingsServerConfig = (): ServerToolConfig< typeof getListings.inputSchema.shape, typeof getListings.outputSchema.shape > => { @@ -21,25 +18,29 @@ export const getListingsServerConfig = ( const accessExpiry = account?.expires_at; if (!apiKey) throw new Error("Missing AUTH_ETSY_ID"); - if (accessExpiry && refreshToken && userID && (accessExpiry < Date.now() / 1000)) { + if ( + accessExpiry && + refreshToken && + userID && + accessExpiry < Date.now() / 1000 + ) { await refreshEtsyAccessToken(refreshToken, userID); } const accessToken = account?.access_token; if (!accessToken) throw new Error("Missing Etsy access token"); - const shopResponse = await fetch( `https://openapi.etsy.com/v3/application/users/${etsyUserId}/shops`, { headers: { "x-api-key": apiKey, Authorization: `Bearer ${accessToken}`, - } + }, }, ); - const shop = (await shopResponse.json()); + const shop = await shopResponse.json(); const listingResponse = await fetch( `https://openapi.etsy.com/v3/application/shops/${shop.shop_id}/listings`, @@ -47,10 +48,10 @@ export const getListingsServerConfig = ( headers: { "x-api-key": apiKey, Authorization: `Bearer ${accessToken}`, - } + }, }, ); - const listings = (await listingResponse.json()); + const listings = await listingResponse.json(); // this is going to be very inefficient code for large shops, but let's do this for now. I can raise QPS ratelimits if needed // for each listing, fetch the images @@ -62,14 +63,13 @@ export const getListingsServerConfig = ( headers: { "x-api-key": apiKey, Authorization: `Bearer ${accessToken}`, - } + }, }, ); - const images = (await imageResponse.json()); + const images = await imageResponse.json(); listing.images = images.results; } - return { listings: listings, }; @@ -82,4 +82,4 @@ export const getListingsServerConfig = ( "Successfully retrieved the Etsy listing. The user is shown the responses in the UI. Do not reiterate them. " + "If you called this tool because the user asked a question, answer the question.", }; -}; \ No newline at end of file +}; diff --git a/src/toolkits/toolkits/Etsy/tools/tools.ts b/src/toolkits/toolkits/Etsy/tools/tools.ts index 5a9ea648..8af37ff8 100644 --- a/src/toolkits/toolkits/Etsy/tools/tools.ts +++ b/src/toolkits/toolkits/Etsy/tools/tools.ts @@ -1,3 +1,3 @@ export enum EtsyTools { getListings = "get-listings", -} \ No newline at end of file +} diff --git a/src/toolkits/toolkits/Etsy/wrapper.tsx b/src/toolkits/toolkits/Etsy/wrapper.tsx index 9b71a4e9..238990ad 100644 --- a/src/toolkits/toolkits/Etsy/wrapper.tsx +++ b/src/toolkits/toolkits/Etsy/wrapper.tsx @@ -57,4 +57,4 @@ export const EtsyWrapper: ClientToolkitWrapper = ({ Item }) => { } return ; -}; \ No newline at end of file +}; From 1c855dd156eafacb0e6f424444a45069e14153ea Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 18 Aug 2025 14:00:43 -0400 Subject: [PATCH 14/18] fix imports on etsy auth provider --- src/server/auth/custom-providers/etsy.ts | 25 ++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/server/auth/custom-providers/etsy.ts b/src/server/auth/custom-providers/etsy.ts index 19f3774f..bc56d289 100644 --- a/src/server/auth/custom-providers/etsy.ts +++ b/src/server/auth/custom-providers/etsy.ts @@ -1,13 +1,14 @@ import { env } from "@/env"; -import type { OAuth2Config, OAuthUserConfig } from "@auth/core/providers"; import { db } from "@/server/db"; +import type { OAuth2Config, OAuthUserConfig } from "next-auth/providers"; + export interface EtsyProfile { - user_id: number; // The numeric ID of a user. Also a valid shop ID. - primary_email?: string | null; // The user's primary email address (nullable). - first_name?: string | null; // The user's first name (nullable). - last_name?: string | null; // The user's last name (nullable). - image_url_75x75?: string | null; // The user's avatar URL (nullable). + user_id: number; + primary_email?: string | null; + first_name?: string | null; + last_name?: string | null; + image_url_75x75?: string | null; } export default function EtsyProvider

( @@ -48,9 +49,7 @@ export default function EtsyProvider

( }, ); - const user = (await response.json()) as EtsyProfile; - - return user; + return (await response.json()) as EtsyProfile; }, }, profile(profile) { @@ -81,7 +80,13 @@ export async function refreshEtsyAccessToken( if (!response.ok) { throw new Error("Failed to refresh Etsy access token"); } - const tokens = await response.json(); + const tokens = (await response.json()) as { + access_token: string; + refresh_token: string; + scope: string; + token_type: string; + expires_at: number; + }; await db.account.update({ where: { From a22b1892ac185bef7d247954586322e3a32a0124 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 18 Aug 2025 14:03:29 -0400 Subject: [PATCH 15/18] update imports --- src/server/auth/providers.ts | 4 +--- src/toolkits/toolkits/Etsy/base.ts | 7 +++++-- src/toolkits/toolkits/Etsy/client.tsx | 14 ++++++++++---- src/toolkits/toolkits/Etsy/server.ts | 2 +- .../tools/{getListings => get-listings}/base.ts | 2 +- .../tools/{getListings => get-listings}/client.tsx | 0 .../tools/{getListings => get-listings}/server.ts | 0 7 files changed, 18 insertions(+), 11 deletions(-) rename src/toolkits/toolkits/Etsy/tools/{getListings => get-listings}/base.ts (92%) rename src/toolkits/toolkits/Etsy/tools/{getListings => get-listings}/client.tsx (100%) rename src/toolkits/toolkits/Etsy/tools/{getListings => get-listings}/server.ts (100%) diff --git a/src/server/auth/providers.ts b/src/server/auth/providers.ts index e0ff9292..b152ba91 100644 --- a/src/server/auth/providers.ts +++ b/src/server/auth/providers.ts @@ -14,8 +14,7 @@ import SpotifyProvider, { } from "next-auth/providers/spotify"; import StravaProvider, { type StravaProfile } from "next-auth/providers/strava"; import CredentialsProvider from "next-auth/providers/credentials"; - -import type { EtsyProfile } from "./custom-providers/etsy"; +import EtsyProvider, { type EtsyProfile } from "./custom-providers/etsy"; import { IS_DEVELOPMENT } from "@/lib/constants"; import { db } from "../db"; @@ -25,7 +24,6 @@ import type { CredentialsConfig, OAuthConfig, } from "next-auth/providers"; -import EtsyProvider from "./custom-providers/etsy"; export const providers: ( | OAuthConfig diff --git a/src/toolkits/toolkits/Etsy/base.ts b/src/toolkits/toolkits/Etsy/base.ts index 8fbcbc01..e15e7444 100644 --- a/src/toolkits/toolkits/Etsy/base.ts +++ b/src/toolkits/toolkits/Etsy/base.ts @@ -1,7 +1,10 @@ import { z } from "zod"; -import type { ToolkitConfig } from "@/toolkits/types"; + import { EtsyTools } from "./tools/tools"; -import { getListings } from "@/toolkits/toolkits/Etsy/tools/getListings/base"; + +import { getListings } from "@/toolkits/toolkits/Etsy/tools/get-listings/base"; + +import type { ToolkitConfig } from "@/toolkits/types"; export const etsyParameters = z.object({}); diff --git a/src/toolkits/toolkits/Etsy/client.tsx b/src/toolkits/toolkits/Etsy/client.tsx index 20e925a8..557f8073 100644 --- a/src/toolkits/toolkits/Etsy/client.tsx +++ b/src/toolkits/toolkits/Etsy/client.tsx @@ -1,11 +1,17 @@ import { SiEtsy } from "@icons-pack/react-simple-icons"; -import { createClientToolkit } from "@/toolkits/create-toolkit"; -import { ToolkitGroups } from "@/toolkits/types"; -import { baseEtsyToolkitConfig } from "./base"; + import { EtsyWrapper } from "./wrapper"; + import { Link } from "../components/link"; + +import { baseEtsyToolkitConfig } from "./base"; + +import { createClientToolkit } from "@/toolkits/create-toolkit"; + +import { getListingsClientConfig } from "@/toolkits/toolkits/Etsy/tools/get-listings/client"; + +import { ToolkitGroups } from "@/toolkits/types"; import { EtsyTools } from "./tools/tools"; -import { getListingsClientConfig } from "@/toolkits/toolkits/Etsy/tools/getListings/client"; export const etsyClientToolkit = createClientToolkit( baseEtsyToolkitConfig, diff --git a/src/toolkits/toolkits/Etsy/server.ts b/src/toolkits/toolkits/Etsy/server.ts index 726bbe85..95f9566b 100644 --- a/src/toolkits/toolkits/Etsy/server.ts +++ b/src/toolkits/toolkits/Etsy/server.ts @@ -1,7 +1,7 @@ import { createServerToolkit } from "../../create-toolkit"; import { baseEtsyToolkitConfig } from "./base"; import { EtsyTools } from "./tools/tools"; -import { getListingsServerConfig } from "@/toolkits/toolkits/Etsy/tools/getListings/server"; +import { getListingsServerConfig } from "@/toolkits/toolkits/Etsy/tools/get-listings/server"; import { api } from "@/trpc/server"; export const etsyToolkitServer = createServerToolkit( baseEtsyToolkitConfig, diff --git a/src/toolkits/toolkits/Etsy/tools/getListings/base.ts b/src/toolkits/toolkits/Etsy/tools/get-listings/base.ts similarity index 92% rename from src/toolkits/toolkits/Etsy/tools/getListings/base.ts rename to src/toolkits/toolkits/Etsy/tools/get-listings/base.ts index a7b28e41..d986dcc8 100644 --- a/src/toolkits/toolkits/Etsy/tools/getListings/base.ts +++ b/src/toolkits/toolkits/Etsy/tools/get-listings/base.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { createBaseTool } from "@/toolkits/create-tool"; -import { ShopListing } from "etsy-ts/dist/api/ShopListing"; import type { IShopListingsWithAssociations } from "etsy-ts"; +import type { ShopListing } from "etsy-ts/dist/api/ShopListing"; export const getListings = createBaseTool({ description: diff --git a/src/toolkits/toolkits/Etsy/tools/getListings/client.tsx b/src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx similarity index 100% rename from src/toolkits/toolkits/Etsy/tools/getListings/client.tsx rename to src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx diff --git a/src/toolkits/toolkits/Etsy/tools/getListings/server.ts b/src/toolkits/toolkits/Etsy/tools/get-listings/server.ts similarity index 100% rename from src/toolkits/toolkits/Etsy/tools/getListings/server.ts rename to src/toolkits/toolkits/Etsy/tools/get-listings/server.ts From 37007b2c19c064a8562e671b0739a7e26e5e641f Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 18 Aug 2025 14:22:11 -0400 Subject: [PATCH 16/18] use etsy-ts --- src/server/api/routers/accounts.ts | 26 +++++++ src/server/auth/custom-providers/etsy.ts | 45 +---------- src/toolkits/toolkits/Etsy/base.ts | 2 +- src/toolkits/toolkits/Etsy/client.tsx | 2 +- .../toolkits/Etsy/security-data-storage.ts | 37 ++++++++++ src/toolkits/toolkits/Etsy/server.ts | 12 ++- .../toolkits/Etsy/tools/get-listings/base.ts | 14 +--- .../Etsy/tools/get-listings/client.tsx | 10 ++- .../Etsy/tools/get-listings/server.ts | 74 +++++-------------- src/toolkits/toolkits/client.ts | 2 +- src/toolkits/toolkits/server.ts | 2 +- src/toolkits/toolkits/shared.ts | 4 +- 12 files changed, 110 insertions(+), 120 deletions(-) create mode 100644 src/toolkits/toolkits/Etsy/security-data-storage.ts diff --git a/src/server/api/routers/accounts.ts b/src/server/api/routers/accounts.ts index b4f28077..bb6043ca 100644 --- a/src/server/api/routers/accounts.ts +++ b/src/server/api/routers/accounts.ts @@ -73,4 +73,30 @@ export const accountsRouter = createTRPCRouter({ }, }); }), + + updateAccount: protectedProcedure + .input( + z.object({ + provider: z.string(), + providerAccountId: z.string(), + data: z.object({ + access_token: z.string(), + refresh_token: z.string(), + expires_at: z.number(), + token_type: z.string(), + scope: z.string(), + }), + }), + ) + .mutation(async ({ ctx, input: { provider, providerAccountId, data } }) => { + return ctx.db.account.update({ + where: { + provider_providerAccountId: { + provider, + providerAccountId, + }, + }, + data, + }); + }), }); diff --git a/src/server/auth/custom-providers/etsy.ts b/src/server/auth/custom-providers/etsy.ts index bc56d289..fe0a5b30 100644 --- a/src/server/auth/custom-providers/etsy.ts +++ b/src/server/auth/custom-providers/etsy.ts @@ -11,6 +11,8 @@ export interface EtsyProfile { image_url_75x75?: string | null; } +export const etsyScopes = "email_r listing_r"; + export default function EtsyProvider

( options: OAuthUserConfig

, ): OAuth2Config

{ @@ -23,7 +25,7 @@ export default function EtsyProvider

( authorization: { url: "https://www.etsy.com/oauth/connect", params: { - scope: "email_r listing_r", + scope: etsyScopes, state: Math.random().toString(36).substring(2, 15), }, }, @@ -63,44 +65,3 @@ export default function EtsyProvider

( options, }; } - -export async function refreshEtsyAccessToken( - refreshToken: string, - providerAccountId: string, -) { - const response = await fetch("https://api.etsy.com/v3/public/oauth/token", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - grant_type: "refresh_token", - client_id: process.env.AUTH_ETSY_ID!, // your app's client_id - refresh_token: refreshToken, // the one you stored - }), - }); - if (!response.ok) { - throw new Error("Failed to refresh Etsy access token"); - } - const tokens = (await response.json()) as { - access_token: string; - refresh_token: string; - scope: string; - token_type: string; - expires_at: number; - }; - - await db.account.update({ - where: { - provider_providerAccountId: { - provider: "etsy", - providerAccountId: providerAccountId, - }, - }, - data: { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - scope: tokens.scope, - token_type: tokens.token_type, - expires_at: tokens.expires_at, - }, - }); -} diff --git a/src/toolkits/toolkits/Etsy/base.ts b/src/toolkits/toolkits/Etsy/base.ts index e15e7444..d585bf7d 100644 --- a/src/toolkits/toolkits/Etsy/base.ts +++ b/src/toolkits/toolkits/Etsy/base.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { EtsyTools } from "./tools/tools"; -import { getListings } from "@/toolkits/toolkits/Etsy/tools/get-listings/base"; +import { getListings } from "@/toolkits/toolkits/etsy/tools/get-listings/base"; import type { ToolkitConfig } from "@/toolkits/types"; diff --git a/src/toolkits/toolkits/Etsy/client.tsx b/src/toolkits/toolkits/Etsy/client.tsx index 557f8073..dc79b09b 100644 --- a/src/toolkits/toolkits/Etsy/client.tsx +++ b/src/toolkits/toolkits/Etsy/client.tsx @@ -8,7 +8,7 @@ import { baseEtsyToolkitConfig } from "./base"; import { createClientToolkit } from "@/toolkits/create-toolkit"; -import { getListingsClientConfig } from "@/toolkits/toolkits/Etsy/tools/get-listings/client"; +import { getListingsClientConfig } from "@/toolkits/toolkits/etsy/tools/get-listings/client"; import { ToolkitGroups } from "@/toolkits/types"; import { EtsyTools } from "./tools/tools"; diff --git a/src/toolkits/toolkits/Etsy/security-data-storage.ts b/src/toolkits/toolkits/Etsy/security-data-storage.ts new file mode 100644 index 00000000..4638ac4f --- /dev/null +++ b/src/toolkits/toolkits/Etsy/security-data-storage.ts @@ -0,0 +1,37 @@ +import type { ISecurityDataStorage, SecurityDataFilter, Tokens } from "etsy-ts"; +import { api } from "@/trpc/server"; +import { etsyScopes } from "@/server/auth/custom-providers/etsy"; + +export class EtsySecurityDataStorage implements ISecurityDataStorage { + async storeAccessToken(filter: SecurityDataFilter, accessToken: Tokens) { + await api.accounts.updateAccount({ + provider: "etsy", + providerAccountId: filter.etsyUserId.toString(), + data: { + access_token: accessToken.accessToken, + refresh_token: accessToken.refreshToken, + token_type: accessToken.tokenType, + expires_at: accessToken.expiresIn + Date.now() / 1000, + scope: etsyScopes, + }, + }); + } + + async findAccessToken(): Promise { + const account = await api.accounts.getAccountByProvider("etsy"); + if ( + !account?.access_token || + !account.refresh_token || + !account.token_type || + !account.expires_at + ) + return undefined; + + return { + accessToken: account.access_token, + refreshToken: account.refresh_token, + tokenType: account.token_type, + expiresIn: account.expires_at - Date.now() / 1000, + }; + } +} diff --git a/src/toolkits/toolkits/Etsy/server.ts b/src/toolkits/toolkits/Etsy/server.ts index 95f9566b..9030a09c 100644 --- a/src/toolkits/toolkits/Etsy/server.ts +++ b/src/toolkits/toolkits/Etsy/server.ts @@ -1,8 +1,10 @@ +import { Etsy } from "etsy-ts"; import { createServerToolkit } from "../../create-toolkit"; import { baseEtsyToolkitConfig } from "./base"; import { EtsyTools } from "./tools/tools"; -import { getListingsServerConfig } from "@/toolkits/toolkits/Etsy/tools/get-listings/server"; +import { getListingsServerConfig } from "@/toolkits/toolkits/etsy/tools/get-listings/server"; import { api } from "@/trpc/server"; +import { EtsySecurityDataStorage } from "./security-data-storage"; export const etsyToolkitServer = createServerToolkit( baseEtsyToolkitConfig, "You have access to the Etsy toolkit for general account management. Currently, this toolkit provides:\n" + @@ -17,8 +19,14 @@ export const etsyToolkitServer = createServerToolkit( throw new Error("No Etsy access token found"); } + const etsy = new Etsy({ + apiKey: process.env.AUTH_ETSY_ID!, + securityDataStorage: new EtsySecurityDataStorage(), + enableTokenRefresh: true, + }); + return { - [EtsyTools.getListings]: getListingsServerConfig(), + [EtsyTools.getListings]: getListingsServerConfig(etsy), }; }, ); diff --git a/src/toolkits/toolkits/Etsy/tools/get-listings/base.ts b/src/toolkits/toolkits/Etsy/tools/get-listings/base.ts index d986dcc8..74dc6515 100644 --- a/src/toolkits/toolkits/Etsy/tools/get-listings/base.ts +++ b/src/toolkits/toolkits/Etsy/tools/get-listings/base.ts @@ -1,18 +1,12 @@ import { z } from "zod"; import { createBaseTool } from "@/toolkits/create-tool"; -import type { IShopListingsWithAssociations } from "etsy-ts"; -import type { ShopListing } from "etsy-ts/dist/api/ShopListing"; +import type { IShopListing } from "etsy-ts"; export const getListings = createBaseTool({ description: "Fetches all listings from the Etsy shop associated with the authenticated user.", inputSchema: z.object({}), - outputSchema: z.object({}), - // outputSchema: z.object({ - // title: z.string().describe("The title of the Etsy listing"), - // description: z.string().describe("The description of the Etsy listing"), - // price: z.number().describe("The price of the Etsy listing"), - // currencyCode: z.string().describe("The currency code for the price"), - // images: z.array(z.string()).describe("URLs of images for the Etsy listing"), - // }), + outputSchema: z.object({ + results: z.array(z.custom()), + }), }); diff --git a/src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx b/src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx index da24f34f..2c99dac6 100644 --- a/src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx +++ b/src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx @@ -5,13 +5,17 @@ export const getListingsClientConfig: ClientToolConfig< typeof getListings.inputSchema.shape, typeof getListings.outputSchema.shape > = { - CallComponent: ({ args, isPartial }) => ( + CallComponent: ({ isPartial }) => (

🔍 {isPartial && ...}
), - ResultComponent: ({ args, result }) => ( -
+ ResultComponent: ({ result }) => ( +
+ {result.results.map((listing) => ( +
{listing.title}
+ ))} +
), }; diff --git a/src/toolkits/toolkits/Etsy/tools/get-listings/server.ts b/src/toolkits/toolkits/Etsy/tools/get-listings/server.ts index 116b68eb..f6e11259 100644 --- a/src/toolkits/toolkits/Etsy/tools/get-listings/server.ts +++ b/src/toolkits/toolkits/Etsy/tools/get-listings/server.ts @@ -1,77 +1,37 @@ +import type { Etsy } from "etsy-ts"; + import type { ServerToolConfig } from "@/toolkits/types"; import type { getListings } from "./base"; -import { api } from "@/trpc/server"; -import { refreshEtsyAccessToken } from "@/server/auth/custom-providers/etsy"; -export const getListingsServerConfig = (): ServerToolConfig< +export const getListingsServerConfig = ( + etsy: Etsy, +): ServerToolConfig< typeof getListings.inputSchema.shape, typeof getListings.outputSchema.shape > => { return { callback: async () => { try { - const account = await api.accounts.getAccountByProvider("etsy"); - const userID = account?.providerAccountId; - const etsyUserId = Number(userID); - const apiKey = process.env.AUTH_ETSY_ID; - const refreshToken = account?.refresh_token; - const accessExpiry = account?.expires_at; + const user = await etsy.User.getMe(); + + const userId = user.data.user_id; - if (!apiKey) throw new Error("Missing AUTH_ETSY_ID"); - if ( - accessExpiry && - refreshToken && - userID && - accessExpiry < Date.now() / 1000 - ) { - await refreshEtsyAccessToken(refreshToken, userID); - } + if (!userId) throw new Error("Missing Etsy user ID"); - const accessToken = account?.access_token; - if (!accessToken) throw new Error("Missing Etsy access token"); + const shop = await etsy.Shop.getShopByOwnerUserId(userId); - const shopResponse = await fetch( - `https://openapi.etsy.com/v3/application/users/${etsyUserId}/shops`, - { - headers: { - "x-api-key": apiKey, - Authorization: `Bearer ${accessToken}`, - }, - }, - ); + const shopId = shop.data.shop_id; - const shop = await shopResponse.json(); + if (!shopId) throw new Error("Missing Etsy shop ID"); - const listingResponse = await fetch( - `https://openapi.etsy.com/v3/application/shops/${shop.shop_id}/listings`, - { - headers: { - "x-api-key": apiKey, - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - const listings = await listingResponse.json(); + const listings = await etsy.ShopListing.getFeaturedListingsByShop({ + shopId, + }); - // this is going to be very inefficient code for large shops, but let's do this for now. I can raise QPS ratelimits if needed - // for each listing, fetch the images - for (let i = 0; i < listings.results.length; i++) { - const listing = listings.results[i]; - const imageResponse = await fetch( - `https://openapi.etsy.com/v3/application/listings/${listing.listing_id}/images`, - { - headers: { - "x-api-key": apiKey, - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - const images = await imageResponse.json(); - listing.images = images.results; - } + if (!listings.data.results) throw new Error("Missing Etsy listings"); return { - listings: listings, + results: listings.data.results, }; } catch (error) { console.error("Etsy API error:", error); diff --git a/src/toolkits/toolkits/client.ts b/src/toolkits/toolkits/client.ts index 5f91838b..29087577 100644 --- a/src/toolkits/toolkits/client.ts +++ b/src/toolkits/toolkits/client.ts @@ -16,7 +16,7 @@ import { e2bClientToolkit } from "./e2b/client"; import { discordClientToolkit } from "./discord/client"; import { stravaClientToolkit } from "./strava/client"; import { spotifyClientToolkit } from "./spotify/client"; -import { etsyClientToolkit } from "./Etsy/client"; +import { etsyClientToolkit } from "./etsy/client"; import { videoClientToolkit } from "./video/client"; import { twitterClientToolkit } from "./twitter/client"; diff --git a/src/toolkits/toolkits/server.ts b/src/toolkits/toolkits/server.ts index 86347a64..ed03b433 100644 --- a/src/toolkits/toolkits/server.ts +++ b/src/toolkits/toolkits/server.ts @@ -10,7 +10,7 @@ import { e2bToolkitServer } from "./e2b/server"; import { discordToolkitServer } from "./discord/server"; import { stravaToolkitServer } from "./strava/server"; import { spotifyToolkitServer } from "./spotify/server"; -import { etsyToolkitServer } from "./Etsy/server"; +import { etsyToolkitServer } from "./etsy/server"; import { videoToolkitServer } from "./video/server"; import { twitterToolkitServer } from "./twitter/server"; import { diff --git a/src/toolkits/toolkits/shared.ts b/src/toolkits/toolkits/shared.ts index a8f42a8d..12040b75 100644 --- a/src/toolkits/toolkits/shared.ts +++ b/src/toolkits/toolkits/shared.ts @@ -20,8 +20,8 @@ import type { stravaParameters } from "./strava/base"; import type { StravaTools } from "./strava/tools"; import type { spotifyParameters } from "./spotify/base"; import type { SpotifyTools } from "./spotify/tools"; -import type { etsyParameters } from "./Etsy/base"; -import type { EtsyTools } from "./Etsy/tools/tools"; +import type { etsyParameters } from "./etsy/base"; +import type { EtsyTools } from "./etsy/tools/tools"; import type { VideoTools } from "./video/tools"; import type { videoParameters } from "./video/base"; import type { TwitterTools } from "./twitter/tools"; From 3a5644779b66184e375261b3c8cff0ed1a273164 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 18 Aug 2025 14:26:32 -0400 Subject: [PATCH 17/18] update styles and import order --- public/icons/etsy.png | Bin 47976 -> 0 bytes src/toolkits/toolkits/Etsy/server.ts | 10 ++++++++-- .../toolkits/Etsy/tools/get-listings/client.tsx | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) delete mode 100644 public/icons/etsy.png diff --git a/public/icons/etsy.png b/public/icons/etsy.png deleted file mode 100644 index 5ff35c743962ac7c59b74af98834342f50ccee63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47976 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
w*IwPxIces9whYR&> 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 = { CallComponent: ({ isPartial }) => (
- 🔍 + {isPartial && ...}
), From 5428ca40b8511fed0886699437638951e4048f46 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 18 Aug 2025 15:03:43 -0400 Subject: [PATCH 18/18] update scopes --- src/server/auth/custom-providers/etsy.ts | 3 +-- .../Etsy/tools/get-listings/client.tsx | 18 +++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/server/auth/custom-providers/etsy.ts b/src/server/auth/custom-providers/etsy.ts index fe0a5b30..2358607b 100644 --- a/src/server/auth/custom-providers/etsy.ts +++ b/src/server/auth/custom-providers/etsy.ts @@ -1,5 +1,4 @@ import { env } from "@/env"; -import { db } from "@/server/db"; import type { OAuth2Config, OAuthUserConfig } from "next-auth/providers"; @@ -11,7 +10,7 @@ export interface EtsyProfile { image_url_75x75?: string | null; } -export const etsyScopes = "email_r listing_r"; +export const etsyScopes = "email_r shops_r listings_r"; export default function EtsyProvider

( options: OAuthUserConfig

, diff --git a/src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx b/src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx index ed0aff6f..828a8026 100644 --- a/src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx +++ b/src/toolkits/toolkits/Etsy/tools/get-listings/client.tsx @@ -12,11 +12,15 @@ export const getListingsClientConfig: ClientToolConfig< {isPartial && ...} ), - ResultComponent: ({ result }) => ( -

- {result.results.map((listing) => ( -
{listing.title}
- ))} -
- ), + ResultComponent: ({ result: { results } }) => + results.length > 0 ? ( +
+

Listings

+ {results.map((listing) => ( +
{listing.title}
+ ))} +
+ ) : ( +

No listings found

+ ), };