diff --git a/package.json b/package.json index eae06e2..e47a8eb 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "author": "", "license": "ISC", "devDependencies": { - "@types/node": "^20.2.5", + "@octokit/types": "^9.3.0", + "@types/node": "^20.2.6", "@types/prettier": "^2.7.3", "tsc-watch": "^6.0.4", "typescript": "^5.1.3" @@ -27,10 +28,12 @@ "@effect/io": "^0.26.0", "@effect/schema": "^0.20.1", "@effect/stream": "^0.22.0", - "dfx": "^0.48.0", + "dfx": "^0.48.1", "dotenv": "^16.1.4", "effect-schema-class": "^0.4.0", + "gpt-tokenizer": "^2.1.1", "html-entities": "^2.3.5", + "octokit": "^2.0.19", "openai": "^3.2.1", "prettier": "^2.8.8" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a28a572..c72abd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,17 +21,23 @@ dependencies: specifier: ^0.22.0 version: 0.22.0 dfx: - specifier: ^0.48.0 - version: 0.48.0 + specifier: ^0.48.1 + version: 0.48.1 dotenv: specifier: ^16.1.4 version: 16.1.4 effect-schema-class: specifier: ^0.4.0 version: 0.4.0 + gpt-tokenizer: + specifier: ^2.1.1 + version: 2.1.1 html-entities: specifier: ^2.3.5 version: 2.3.5 + octokit: + specifier: ^2.0.19 + version: 2.0.19 openai: specifier: ^3.2.1 version: 3.2.1 @@ -40,9 +46,12 @@ dependencies: version: 2.8.8 devDependencies: + '@octokit/types': + specifier: ^9.3.0 + version: 9.3.0 '@types/node': - specifier: ^20.2.5 - version: 20.2.5 + specifier: ^20.2.6 + version: 20.2.6 '@types/prettier': specifier: ^2.7.3 version: 2.7.3 @@ -89,23 +98,284 @@ packages: '@effect/io': 0.26.0 dev: false + /@octokit/app@13.1.5: + resolution: {integrity: sha512-6qTa24S+gdQUU66SCVfqTkyt2jAr9/ZeyPqJhnNI9PZ8Wum4lQy3bPS+voGlxABNOlzRKnxbSdYKoraMr3MqBA==} + engines: {node: '>= 14'} + dependencies: + '@octokit/auth-app': 4.0.13 + '@octokit/auth-unauthenticated': 3.0.5 + '@octokit/core': 4.2.1 + '@octokit/oauth-app': 4.2.2 + '@octokit/plugin-paginate-rest': 6.1.2(@octokit/core@4.2.1) + '@octokit/types': 9.3.0 + '@octokit/webhooks': 10.9.1 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/auth-app@4.0.13: + resolution: {integrity: sha512-NBQkmR/Zsc+8fWcVIFrwDgNXS7f4XDrkd9LHdi9DPQw1NdGHLviLzRO2ZBwTtepnwHXW5VTrVU9eFGijMUqllg==} + engines: {node: '>= 14'} + dependencies: + '@octokit/auth-oauth-app': 5.0.6 + '@octokit/auth-oauth-user': 2.1.2 + '@octokit/request': 6.2.5 + '@octokit/request-error': 3.0.3 + '@octokit/types': 9.3.0 + deprecation: 2.3.1 + lru-cache: 9.1.2 + universal-github-app-jwt: 1.1.1 + universal-user-agent: 6.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/auth-oauth-app@5.0.6: + resolution: {integrity: sha512-SxyfIBfeFcWd9Z/m1xa4LENTQ3l1y6Nrg31k2Dcb1jS5ov7pmwMJZ6OGX8q3K9slRgVpeAjNA1ipOAMHkieqyw==} + engines: {node: '>= 14'} + dependencies: + '@octokit/auth-oauth-device': 4.0.5 + '@octokit/auth-oauth-user': 2.1.2 + '@octokit/request': 6.2.5 + '@octokit/types': 9.3.0 + '@types/btoa-lite': 1.0.0 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/auth-oauth-device@4.0.5: + resolution: {integrity: sha512-XyhoWRTzf2ZX0aZ52a6Ew5S5VBAfwwx1QnC2Np6Et3MWQpZjlREIcbcvVZtkNuXp6Z9EeiSLSDUqm3C+aMEHzQ==} + engines: {node: '>= 14'} + dependencies: + '@octokit/oauth-methods': 2.0.6 + '@octokit/request': 6.2.5 + '@octokit/types': 9.3.0 + universal-user-agent: 6.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/auth-oauth-user@2.1.2: + resolution: {integrity: sha512-kkRqNmFe7s5GQcojE3nSlF+AzYPpPv7kvP/xYEnE57584pixaFBH8Vovt+w5Y3E4zWUEOxjdLItmBTFAWECPAg==} + engines: {node: '>= 14'} + dependencies: + '@octokit/auth-oauth-device': 4.0.5 + '@octokit/oauth-methods': 2.0.6 + '@octokit/request': 6.2.5 + '@octokit/types': 9.3.0 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/auth-token@3.0.4: + resolution: {integrity: sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==} + engines: {node: '>= 14'} + dev: false + + /@octokit/auth-unauthenticated@3.0.5: + resolution: {integrity: sha512-yH2GPFcjrTvDWPwJWWCh0tPPtTL5SMgivgKPA+6v/XmYN6hGQkAto8JtZibSKOpf8ipmeYhLNWQ2UgW0GYILCw==} + engines: {node: '>= 14'} + dependencies: + '@octokit/request-error': 3.0.3 + '@octokit/types': 9.3.0 + dev: false + + /@octokit/core@4.2.1: + resolution: {integrity: sha512-tEDxFx8E38zF3gT7sSMDrT1tGumDgsw5yPG6BBh/X+5ClIQfMH/Yqocxz1PnHx6CHyF6pxmovUTOfZAUvQ0Lvw==} + engines: {node: '>= 14'} + dependencies: + '@octokit/auth-token': 3.0.4 + '@octokit/graphql': 5.0.6 + '@octokit/request': 6.2.5 + '@octokit/request-error': 3.0.3 + '@octokit/types': 9.3.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/endpoint@7.0.6: + resolution: {integrity: sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==} + engines: {node: '>= 14'} + dependencies: + '@octokit/types': 9.3.0 + is-plain-object: 5.0.0 + universal-user-agent: 6.0.0 + dev: false + + /@octokit/graphql@5.0.6: + resolution: {integrity: sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==} + engines: {node: '>= 14'} + dependencies: + '@octokit/request': 6.2.5 + '@octokit/types': 9.3.0 + universal-user-agent: 6.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/oauth-app@4.2.2: + resolution: {integrity: sha512-/jsPd43Yu2UXJ4XGq9KyOjPj5kNWQ5pfVzeDEfIVE8ENchyIPS+/IY2a8b0+OQSAsBKBLTHVp9m51RfGHmPZlw==} + engines: {node: '>= 14'} + dependencies: + '@octokit/auth-oauth-app': 5.0.6 + '@octokit/auth-oauth-user': 2.1.2 + '@octokit/auth-unauthenticated': 3.0.5 + '@octokit/core': 4.2.1 + '@octokit/oauth-authorization-url': 5.0.0 + '@octokit/oauth-methods': 2.0.6 + '@types/aws-lambda': 8.10.117 + fromentries: 1.3.2 + universal-user-agent: 6.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/oauth-authorization-url@5.0.0: + resolution: {integrity: sha512-y1WhN+ERDZTh0qZ4SR+zotgsQUE1ysKnvBt1hvDRB2WRzYtVKQjn97HEPzoehh66Fj9LwNdlZh+p6TJatT0zzg==} + engines: {node: '>= 14'} + dev: false + + /@octokit/oauth-methods@2.0.6: + resolution: {integrity: sha512-l9Uml2iGN2aTWLZcm8hV+neBiFXAQ9+3sKiQe/sgumHlL6HDg0AQ8/l16xX/5jJvfxueqTW5CWbzd0MjnlfHZw==} + engines: {node: '>= 14'} + dependencies: + '@octokit/oauth-authorization-url': 5.0.0 + '@octokit/request': 6.2.5 + '@octokit/request-error': 3.0.3 + '@octokit/types': 9.3.0 + btoa-lite: 1.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/openapi-types@18.0.0: + resolution: {integrity: sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==} + + /@octokit/plugin-paginate-rest@6.1.2(@octokit/core@4.2.1): + resolution: {integrity: sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==} + engines: {node: '>= 14'} + peerDependencies: + '@octokit/core': '>=4' + dependencies: + '@octokit/core': 4.2.1 + '@octokit/tsconfig': 1.0.2 + '@octokit/types': 9.3.0 + dev: false + + /@octokit/plugin-rest-endpoint-methods@7.2.0(@octokit/core@4.2.1): + resolution: {integrity: sha512-yZhsKs8QnQbEpeAUd0WGgNJLoVHzENir7H0JmZg9BWRCZB+Fmqj7x/+sUFK7kw24XjWWVIQ4qGswps42/lmiIQ==} + engines: {node: '>= 14'} + peerDependencies: + '@octokit/core': '>=3' + dependencies: + '@octokit/core': 4.2.1 + '@octokit/types': 9.3.0 + dev: false + + /@octokit/plugin-retry@4.1.6(@octokit/core@4.2.1): + resolution: {integrity: sha512-obkYzIgEC75r8+9Pnfiiqy3y/x1bc3QLE5B7qvv9wi9Kj0R5tGQFC6QMBg1154WQ9lAVypuQDGyp3hNpp15gQQ==} + engines: {node: '>= 14'} + peerDependencies: + '@octokit/core': '>=3' + dependencies: + '@octokit/core': 4.2.1 + '@octokit/types': 9.3.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/plugin-throttling@5.2.3(@octokit/core@4.2.1): + resolution: {integrity: sha512-C9CFg9mrf6cugneKiaI841iG8DOv6P5XXkjmiNNut+swePxQ7RWEdAZRp5rJoE1hjsIqiYcKa/ZkOQ+ujPI39Q==} + engines: {node: '>= 14'} + peerDependencies: + '@octokit/core': ^4.0.0 + dependencies: + '@octokit/core': 4.2.1 + '@octokit/types': 9.3.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/request-error@3.0.3: + resolution: {integrity: sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==} + engines: {node: '>= 14'} + dependencies: + '@octokit/types': 9.3.0 + deprecation: 2.3.1 + once: 1.4.0 + dev: false + + /@octokit/request@6.2.5: + resolution: {integrity: sha512-z83E8UIlPNaJUsXpjD8E0V5o/5f+vJJNbNcBwVZsX3/vC650U41cOkTLjq4PKk9BYonQGOnx7N17gvLyNjgGcQ==} + engines: {node: '>= 14'} + dependencies: + '@octokit/endpoint': 7.0.6 + '@octokit/request-error': 3.0.3 + '@octokit/types': 9.3.0 + is-plain-object: 5.0.0 + node-fetch: 2.6.11 + universal-user-agent: 6.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/tsconfig@1.0.2: + resolution: {integrity: sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==} + dev: false + + /@octokit/types@9.3.0: + resolution: {integrity: sha512-ZNO1In0QuWZLDngSLcn5H4ExRhYOd1rDcWnwc/LuR55cO1d6Sex6+T6RiSQwp/tyEg7eNWx+MUdJGL7Fu1kMjw==} + dependencies: + '@octokit/openapi-types': 18.0.0 + + /@octokit/webhooks-methods@3.0.3: + resolution: {integrity: sha512-2vM+DCNTJ5vL62O5LagMru6XnYhV4fJslK+5YUkTa6rWlW2S+Tqs1lF9Wr9OGqHfVwpBj3TeztWfVON/eUoW1Q==} + engines: {node: '>= 14'} + dev: false + + /@octokit/webhooks-types@6.11.0: + resolution: {integrity: sha512-AanzbulOHljrku1NGfafxdpTCfw2ENaWzH01N2vqQM+cUFbk868Cgh0xylz0JIM9BoKbfI++bdD6EYX0Q/UTEw==} + dev: false + + /@octokit/webhooks@10.9.1: + resolution: {integrity: sha512-5NXU4VfsNOo2VSU/SrLrpPH2Z1ZVDOWFcET4EpnEBX1uh/v8Uz65UVuHIRx5TZiXhnWyRE9AO1PXHa+M/iWwZA==} + engines: {node: '>= 14'} + dependencies: + '@octokit/request-error': 3.0.3 + '@octokit/webhooks-methods': 3.0.3 + '@octokit/webhooks-types': 6.11.0 + aggregate-error: 3.1.0 + dev: false + + /@types/aws-lambda@8.10.117: + resolution: {integrity: sha512-6T1aHTSSK4l8+67ANKHha/CRVxyk/bAl6OGCOxsKVsHaSxWpqsqgupc8rPw8vQGjtIgIZ+EaHqMz8gA4d6xZhQ==} + dev: false + /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.2.5 + '@types/node': 20.2.6 + dev: false + + /@types/btoa-lite@1.0.0: + resolution: {integrity: sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg==} dev: false /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.2.6 dev: false /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.2.6 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.1 @@ -120,6 +390,12 @@ packages: '@types/serve-static': 1.15.1 dev: false + /@types/jsonwebtoken@9.0.2: + resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} + dependencies: + '@types/node': 20.2.6 + dev: false + /@types/mime@1.3.2: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} dev: false @@ -128,8 +404,8 @@ packages: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} dev: false - /@types/node@20.2.5: - resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==} + /@types/node@20.2.6: + resolution: {integrity: sha512-GQBWUtGoefMEOx/vu+emHEHU5aw6JdDoEtZhoBrHFPZbA/YNRFfN996XbBASEWdvmLSLyv9FKYppYGyZjCaq/g==} /@types/prettier@2.7.3: resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} @@ -147,14 +423,22 @@ packages: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: '@types/mime': 1.3.2 - '@types/node': 20.2.5 + '@types/node': 20.2.6 dev: false /@types/serve-static@1.15.1: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.2.5 + '@types/node': 20.2.6 + dev: false + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 dev: false /asynckit@0.4.0: @@ -169,6 +453,22 @@ packages: - debug dev: false + /before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + dev: false + + /bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + dev: false + + /btoa-lite@1.0.0: + resolution: {integrity: sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==} + dev: false + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /bufferutil@4.0.7: resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} engines: {node: '>=6.14.2'} @@ -178,6 +478,11 @@ packages: dev: false optional: true + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: false + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -199,8 +504,12 @@ packages: engines: {node: '>=0.4.0'} dev: false - /dfx@0.48.0: - resolution: {integrity: sha512-mwLHSShglRtJAY/ZcA6GBCGuQoe4SKYXi1qPksUqO4EeYnzLbh/gx0i/YE7r7koSempk4UKTNf9kyvHixvi8+Q==} + /deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dev: false + + /dfx@0.48.1: + resolution: {integrity: sha512-k1ZtD64kCSjzC/F4V+4TkmSzIuzk7BmZyjkeiKQaqUT1GQUKRTfZMuKosWMh27G26spiTGQQAWHKqeiI7JeFWw==} dependencies: '@effect-http/client': 0.27.0 '@effect/data': 0.12.5 @@ -230,6 +539,12 @@ packages: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: true + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /effect-schema-class@0.4.0: resolution: {integrity: sha512-LYO1AFK3+mXIrOi6iwHtjErKNj9LvM8Jcwj7fRs5nNEozbOgIWFbcy6voE95AznR4OVa8lUsnIHRUiOp0KHR4A==} dependencies: @@ -280,10 +595,30 @@ packages: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} dev: true + /fromentries@1.3.2: + resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} + dev: false + + /gpt-tokenizer@2.1.1: + resolution: {integrity: sha512-WlX+vj6aPaZ71U6Bf18fem+5k58zlgh2a4nbc7KHy6aGVIyq3nCh709b/8momu34sV/5t/SpzWi8LayWD9uyDw==} + dependencies: + rfc4648: 1.5.2 + dev: false + /html-entities@2.3.5: resolution: {integrity: sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg==} dev: false + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: false + + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -298,6 +633,47 @@ packages: dev: false optional: true + /jsonwebtoken@9.0.0: + resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash: 4.17.21 + ms: 2.1.3 + semver: 7.5.1 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: false + + /lru-cache@9.1.2: + resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==} + engines: {node: 14 || >=16.14} + dev: false + /map-stream@0.1.0: resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} dev: true @@ -314,16 +690,54 @@ packages: mime-db: 1.52.0 dev: false + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + /node-cleanup@2.1.2: resolution: {integrity: sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==} dev: true + /node-fetch@2.6.11: + resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-gyp-build@4.6.0: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true dev: false optional: true + /octokit@2.0.19: + resolution: {integrity: sha512-hSloK4MK78QGbAuBrtIir0bsxMoRVZE5CkwKSbSRH9lqv2hx9EwhCxtPqEF+BtHqLXkXdfUaGkJMyMBotYno+A==} + engines: {node: '>= 14'} + dependencies: + '@octokit/app': 13.1.5 + '@octokit/core': 4.2.1 + '@octokit/oauth-app': 4.2.2 + '@octokit/plugin-paginate-rest': 6.1.2(@octokit/core@4.2.1) + '@octokit/plugin-rest-endpoint-methods': 7.2.0(@octokit/core@4.2.1) + '@octokit/plugin-retry': 4.1.6(@octokit/core@4.2.1) + '@octokit/plugin-throttling': 5.2.3(@octokit/core@4.2.1) + '@octokit/types': 9.3.0 + transitivePeerDependencies: + - encoding + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: false + /openai@3.2.1: resolution: {integrity: sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==} dependencies: @@ -362,6 +776,22 @@ packages: resolution: {integrity: sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==} dev: false + /rfc4648@1.5.2: + resolution: {integrity: sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==} + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /semver@7.5.1: + resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -395,6 +825,10 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /tsc-watch@6.0.4(typescript@5.1.3): resolution: {integrity: sha512-cHvbvhjO86w2aGlaHgSCeQRl+Aqw6X6XN4sQMPZKF88GoP30O+oTuh5lRIJr5pgFWrRpF1AgXnJJ2DoFEIPHyg==} engines: {node: '>=12.12.0'} @@ -415,6 +849,17 @@ packages: hasBin: true dev: true + /universal-github-app-jwt@1.1.1: + resolution: {integrity: sha512-G33RTLrIBMFmlDV4u4CBF7dh71eWwykck4XgaxaIVeZKOYZRAAxvcGMRFTUclVY6xoUPQvO4Ne5wKGxYm/Yy9w==} + dependencies: + '@types/jsonwebtoken': 9.0.2 + jsonwebtoken: 9.0.0 + dev: false + + /universal-user-agent@6.0.0: + resolution: {integrity: sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==} + dev: false + /utf-8-validate@6.0.3: resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==} engines: {node: '>=6.14.2'} @@ -424,6 +869,17 @@ packages: dev: false optional: true + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -432,6 +888,10 @@ packages: isexe: 2.0.0 dev: true + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: false + /ws@8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} @@ -449,3 +909,7 @@ packages: utf-8-validate: 6.0.3 dev: false optional: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false diff --git a/src/Github.ts b/src/Github.ts new file mode 100644 index 0000000..2111eb3 --- /dev/null +++ b/src/Github.ts @@ -0,0 +1,77 @@ +import type { OctokitResponse } from "@octokit/types" +import { + Chunk, + Config, + ConfigSecret, + Effect, + Layer, + Option, + Stream, + Tag, + pipe, +} from "bot/_common" +import { Octokit } from "octokit" + +export interface GithubConfig { + readonly token: ConfigSecret.ConfigSecret +} + +export class GithubError { + readonly _tag = "GithubError" + constructor(readonly reason: unknown) {} +} + +const make = ({ token }: GithubConfig) => { + const octokit = new Octokit({ auth: ConfigSecret.value(token) }) + + const rest = octokit.rest + type Endpoints = typeof rest + + const request = (f: (_: Endpoints) => Promise) => + Effect.tryCatchPromise( + () => f(rest), + reason => new GithubError(reason), + ) + + const wrap = + ( + f: (_: Endpoints) => (...args: Args) => Promise>, + ) => + (...args: Args) => + Effect.map( + Effect.tryCatchPromise( + () => f(rest)(...args), + reason => new GithubError(reason), + ), + _ => _.data, + ) + + const stream = ( + f: (_: Endpoints, page: number) => Promise>, + ) => + Stream.paginateChunkEffect(0, page => + Effect.map( + Effect.tryCatchPromise( + () => f(rest, page), + reason => new GithubError(reason), + ), + _ => [Chunk.fromIterable(_.data), maybeNextPage(page, _.headers.link)], + ), + ) + + return { octokit, token, request, wrap, stream } +} + +export interface Github extends ReturnType {} +export const Github = Tag() +export const makeLayer = (_: Config.Config.Wrap) => + Layer.effect(Github, Effect.map(Effect.config(Config.unwrap(_)), make)) + +// == helpers + +const maybeNextPage = (page: number, linkHeader?: string) => + pipe( + Option.fromNullable(linkHeader), + Option.filter(_ => _.includes(`rel=\"next\"`)), + Option.as(page + 1), + ) diff --git a/src/Issueifier.ts b/src/Issueifier.ts new file mode 100644 index 0000000..6c7cbcc --- /dev/null +++ b/src/Issueifier.ts @@ -0,0 +1,152 @@ +import { ChannelsCache, ChannelsCacheLive } from "bot/ChannelsCache" +import { Github } from "bot/Github" +import { Messages, MessagesLive } from "bot/Messages" +import { OpenAI, OpenAIMessage } from "bot/OpenAI" +import { Summarizer, SummarizerLive } from "bot/Summarizer" +import { Chunk, Config, Data, Effect, Layer, Stream, pipe } from "bot/_common" +import { Discord, DiscordREST, Ix } from "dfx" +import { InteractionsRegistry, InteractionsRegistryLive } from "dfx/gateway" + +export interface IssueifierConfig { + readonly githubRepo: string +} + +export class NotInThreadError extends Data.TaggedClass( + "NotInThreadError", +)<{}> {} + +const make = ({ githubRepo }: IssueifierConfig) => + Effect.gen(function* (_) { + const rest = yield* _(DiscordREST) + const channels = yield* _(ChannelsCache) + const openai = yield* _(OpenAI) + const messages = yield* _(Messages) + const registry = yield* _(InteractionsRegistry) + const scope = yield* _(Effect.scope()) + const github = yield* _(Github) + const summarizer = yield* _(Summarizer) + + const [repoOwner, repoName] = githubRepo.split("/") + const createGithubIssue = github.wrap(_ => _.issues.create) + + const application = yield* _( + Effect.flatMap(rest.getCurrentBotApplicationInformation(), _ => _.json), + ) + + const createIssue = (channel: Discord.Channel) => + pipe( + messages.cleanForChannel(channel), + Stream.runCollect, + Effect.map(Chunk.reverse), + Effect.bindTo("messages"), + Effect.let("openAiMessages", ({ messages }) => + Chunk.map( + messages, + (msg): OpenAIMessage => ({ + bot: false, + name: msg.author.username, + content: msg.content, + }), + ), + ), + Effect.flatMap(({ openAiMessages, messages }) => + Effect.all({ + summary: openai.generateSummary( + channel.name!, + Chunk.toReadonlyArray(openAiMessages), + ), + fullThread: summarizer.messages(channel, messages), + }), + ), + Effect.flatMap(({ summary, fullThread }) => + createGithubIssue({ + owner: repoOwner, + repo: repoName, + title: `From Discord: ${channel.name}`, + body: `${summary} + +## Discord thread + +
+Click to expand + +${fullThread} + +
`, + }), + ), + ) + + const followUp = (context: Discord.Interaction, channel: Discord.Channel) => + pipe( + createIssue(channel), + Effect.tap(issue => + rest.editOriginalInteractionResponse(application.id, context.token, { + content: `Created Github issue for thread: ${issue.html_url}`, + }), + ), + Effect.tapErrorCause(() => + rest.deleteOriginalInteractionResponse(application.id, context.token), + ), + Effect.catchAllCause(Effect.logErrorCause), + ) + + const command = Ix.global( + { + name: "issueify", + description: + "Convert this thread into an issue for the Effect Website repo", + }, + pipe( + Effect.all({ context: Ix.Interaction }), + Effect.bind("channel", ({ context }) => + channels.get(context.guild_id!, context.channel_id!), + ), + Effect.filterOrFail( + ({ channel }) => channel.type === Discord.ChannelType.PUBLIC_THREAD, + () => new NotInThreadError(), + ), + Effect.tap(({ context, channel }) => + Effect.forkIn(followUp(context, channel), scope), + ), + Effect.as( + Ix.response({ + type: Discord.InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Creating issue on Github...", + }, + }), + ), + ), + ) + + const ix = Ix.builder + .add(command) + .catchTagRespond("NotInThreadError", () => + Effect.succeed( + Ix.response({ + type: Discord.InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "This command can only be used in a thread", + flags: Discord.MessageFlag.EPHEMERAL, + }, + }), + ), + ) + .catchAllCause(Effect.logErrorCause) + + yield* _(registry.register(ix)) + }) + +export const makeLayer = (config: Config.Config.Wrap) => + Layer.provide( + Layer.mergeAll( + ChannelsCacheLive, + InteractionsRegistryLive, + MessagesLive, + SummarizerLive, + ), + Layer.scopedDiscard( + Effect.flatMap(Effect.config(Config.unwrap(config)), make), + ), + ) diff --git a/src/Mentions.ts b/src/Mentions.ts index 2d96b75..ba206a2 100644 --- a/src/Mentions.ts +++ b/src/Mentions.ts @@ -49,11 +49,11 @@ const make = Effect.gen(function* (_) { .filter(msg => msg.content.trim().length > 0) .map( (msg): OpenAIMessage => ({ - content: + content: msg.content, + name: msg.author.id === botUser.id - ? msg.content - : `<@${msg.author.id}> said: -${msg.content}`, + ? undefined + : `<@${msg.author.id}>`, bot: msg.author.id === botUser.id, }), ), diff --git a/src/Messages.ts b/src/Messages.ts new file mode 100644 index 0000000..2c7f099 --- /dev/null +++ b/src/Messages.ts @@ -0,0 +1,114 @@ +import { MemberCache, MemberCacheLive } from "bot/MemberCache" +import { Chunk, Effect, Layer, Option, Stream, Tag, pipe } from "bot/_common" +import { Discord, DiscordREST } from "dfx" + +export const cleanupMarkdown = (content: string) => + content + .replace(/```ts\b/g, "```typescript") + .replace(/^```/, "\n```") + .replace(/[^\n]```/gm, "\n\n```") + .replace(/([^\n])\n```([^\n]*\n[^\n])/gm, "$1\n\n```$2") + +const make = Effect.gen(function* (_) { + const rest = yield* _(DiscordREST) + const members = yield* _(MemberCache) + + const replaceMentions = (guildId: Discord.Snowflake, content: string) => + Effect.gen(function* (_) { + const mentions = yield* _( + Effect.forEachPar(content.matchAll(/<@(\d+)>/g), ([, userId]) => + Effect.option(members.get(guildId, userId as Discord.Snowflake)), + ), + ) + + return mentions.reduce( + (content, member) => + Option.match( + member, + () => content, + member => + content.replace( + new RegExp(`<@${member.user!.id}>`, "g"), + `**@${member.nick ?? member.user!.username}**`, + ), + ), + content, + ) + }) + + const regularForChannel = (channelId: string) => + pipe( + Stream.paginateChunkEffect(Option.none(), before => + pipe( + rest.getChannelMessages(channelId, { + limit: 100, + before: Option.getOrUndefined(before), + }), + Effect.flatMap(_ => _.json), + Effect.map(messages => + messages.length < 100 + ? ([ + Chunk.unsafeFromArray(messages), + Option.none>(), + ] as const) + : ([ + Chunk.unsafeFromArray(messages), + Option.some(Option.some(messages[messages.length - 1].id)), + ] as const), + ), + ), + ), + + // only include normal messages + Stream.flatMapPar(Number.MAX_SAFE_INTEGER, msg => { + if (msg.type === Discord.MessageType.THREAD_STARTER_MESSAGE) { + return Effect.flatMap( + rest.getChannelMessage( + msg.message_reference!.channel_id!, + msg.message_reference!.message_id!, + ), + _ => _.json, + ) + } else if ( + msg.content !== "" && + (msg.type === Discord.MessageType.REPLY || + msg.type === Discord.MessageType.DEFAULT) + ) { + return Stream.succeed(msg) + } + + return Stream.empty + }), + ) + + const cleanForChannel = (channel: Discord.Channel) => + pipe( + regularForChannel(channel.id), + Stream.map(msg => ({ + ...msg, + content: cleanupMarkdown(msg.content), + })), + Stream.flatMapPar(Number.MAX_SAFE_INTEGER, msg => + Effect.map( + replaceMentions(channel.guild_id!, msg.content), + (content): Discord.Message => ({ + ...msg, + content, + }), + ), + ), + ) + + return { + regularForChannel, + cleanForChannel, + replaceMentions, + } as const +}) + +export interface Messages extends Effect.Effect.Success {} +export const Messages = Tag() +export const MessagesLive = Layer.provide( + Layer.mergeAll(MemberCacheLive), + Layer.effect(Messages, make), +) diff --git a/src/OpenAI.ts b/src/OpenAI.ts index 732c70a..3915b37 100644 --- a/src/OpenAI.ts +++ b/src/OpenAI.ts @@ -11,6 +11,7 @@ import { } from "bot/_common" import * as Str from "bot/utils/String" import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai" +import * as Tokenizer from "gpt-tokenizer" export interface OpenAIOptions { readonly apiKey: ConfigSecret.ConfigSecret @@ -23,6 +24,7 @@ export class OpenAIError extends Data.TaggedClass("OpenAIError")<{ export interface OpenAIMessage { readonly bot: boolean + readonly name?: string readonly content: string } @@ -113,7 +115,50 @@ The title of this conversation is "${title}".`, _ => Option.fromNullable(_.data.choices[0]?.message?.content), ) - return { client, call, generateTitle, generateReply } as const + const generateSummary = ( + title: string, + messages: ReadonlyArray, + ) => + Effect.flatMap( + call((_, signal) => + _.createChatCompletion( + { + model: "gpt-3.5-turbo", + temperature: 0.25, + messages: [ + { + role: "system", + content: `You are a helpful assistant for the Effect-TS typescript library. + +The title of this chat is "${title}".`, + }, + ...limitMessageTokens(messages, 3300).map( + ({ content, bot, name }): ChatCompletionRequestMessage => ({ + role: bot ? "assistant" : "user", + name, + content, + }), + ), + { + role: "user", + content: + "Summarize the conversation, then add some key takeaways.", + }, + ], + }, + { signal }, + ), + ), + _ => Option.fromNullable(_.data.choices[0]?.message?.content), + ) + + return { + client, + call, + generateTitle, + generateReply, + generateSummary, + } as const } export interface OpenAI extends ReturnType {} @@ -122,3 +167,20 @@ export const makeLayer = (config: Config.Config.Wrap) => Layer.effect(OpenAI, Effect.map(Effect.config(Config.unwrap(config)), make)) const cleanTitle = flow(Str.firstParagraph, Str.removeQuotes, Str.removePeriod) + +const limitMessageTokens = ( + messages: ReadonlyArray, + count: number, +): ReadonlyArray => { + let content = "" + const newMessages: OpenAIMessage[] = [] + for (const message of messages) { + content += message.content + const tokens = Tokenizer.encode(content).length + if (tokens > count) { + break + } + newMessages.push(message) + } + return newMessages +} diff --git a/src/Summarizer.ts b/src/Summarizer.ts index 03f9acb..ca08864 100644 --- a/src/Summarizer.ts +++ b/src/Summarizer.ts @@ -9,10 +9,12 @@ import { Layer, Option, Stream, + Tag, pipe, } from "bot/_common" import { Discord, DiscordREST, Ix } from "dfx" import { InteractionsRegistry, InteractionsRegistryLive } from "dfx/gateway" +import { Messages, MessagesLive } from "bot/Messages" export class NotInThreadError extends Data.TaggedClass( "NotInThreadError", @@ -28,54 +30,36 @@ const make = Effect.gen(function* (_) { const channels = yield* _(ChannelsCache) const registry = yield* _(InteractionsRegistry) const members = yield* _(MemberCache) + const messages = yield* _(Messages) const scope = yield* _(Effect.scope()) const application = yield* _( Effect.flatMap(rest.getCurrentBotApplicationInformation(), _ => _.json), ) - const getAllMessages = (channelId: string) => + const summarizeThread = (channel: Discord.Channel, small = true) => pipe( - Stream.paginateChunkEffect(Option.none(), before => - pipe( - rest.getChannelMessages(channelId, { - limit: 100, - before: Option.getOrUndefined(before), - }), - Effect.flatMap(_ => _.json), - Effect.map(messages => - messages.length < 100 - ? ([ - Chunk.unsafeFromArray(messages), - Option.none>(), - ] as const) - : ([ - Chunk.unsafeFromArray(messages), - Option.some(Option.some(messages[messages.length - 1].id)), - ] as const), - ), + Effect.all({ + parentChannel: channels.get(channel.guild_id!, channel.parent_id!), + messages: Effect.map( + Stream.runCollect(messages.cleanForChannel(channel)), + Chunk.reverse, ), + }), + Effect.flatMap(({ parentChannel, messages }) => + summarize(parentChannel, channel, messages, small), ), + ) - // only include normal messages - Stream.flatMapPar(Number.MAX_SAFE_INTEGER, msg => { - if (msg.type === Discord.MessageType.THREAD_STARTER_MESSAGE) { - return Effect.flatMap( - rest.getChannelMessage( - msg.message_reference!.channel_id!, - msg.message_reference!.message_id!, - ), - _ => _.json, - ) - } else if ( - msg.content !== "" && - (msg.type === Discord.MessageType.REPLY || - msg.type === Discord.MessageType.DEFAULT) - ) { - return Stream.succeed(msg) - } - - return Stream.empty - }), + const summarizeWithMessages = ( + channel: Discord.Channel, + messages: Chunk.Chunk, + small = true, + ) => + pipe( + channels.get(channel.guild_id!, channel.parent_id!), + Effect.flatMap(parentChannel => + summarize(parentChannel, channel, messages, small), + ), ) const summarize = ( @@ -133,34 +117,8 @@ ${messageContent.join("\n\n")}`, message.timestamp, ).toUTCString()}${smallClose}${smallClose}` - const content = `${header}
-${message.content - .replace(/```ts\b/g, "```typescript") - .replace(/^```/, "\n```") - .replace(/[^\n]```/gm, "\n\n```") - .replace(/([^\n])\n```([^\n]*\n[^\n])/gm, "$1\n\n```$2")}` - - const mentions = yield* _( - Effect.forEachPar(content.matchAll(/<@(\d+)>/g), ([, userId]) => - Effect.option( - members.get(thread.guild_id!, userId as Discord.Snowflake), - ), - ), - ) - - return mentions.reduce( - (content, member) => - Option.match( - member, - () => content, - member => - content.replace( - new RegExp(`<@${member.user!.id}>`, "g"), - `**@${member.nick ?? member.user!.username}**`, - ), - ), - content, - ) + return `${header}
+${message.content}` }) const followUpResponse = ( @@ -169,19 +127,8 @@ ${message.content small: boolean, ) => pipe( - Effect.all({ - parentChannel: channels.get(channel.guild_id!, channel.parent_id!), - }), - Effect.bind("messages", () => - Effect.map( - Stream.runCollect(getAllMessages(channel.id)), - Chunk.reverse, - ), - ), - Effect.bind("summary", ({ parentChannel, messages }) => - summarize(parentChannel, channel, messages, small), - ), - Effect.tap(({ summary }) => { + summarizeThread(channel, small), + Effect.tap(summary => { const formData = new FormData() formData.append( @@ -264,9 +211,22 @@ ${message.content .catchAllCause(Effect.logErrorCause) yield* _(registry.register(ix)) + + return { + thread: summarizeThread, + messages: summarizeWithMessages, + message: summarizeMessage, + } as const }) +export interface Summarizer extends Effect.Effect.Success {} +export const Summarizer = Tag() export const SummarizerLive = Layer.provide( - Layer.mergeAll(ChannelsCacheLive, InteractionsRegistryLive, MemberCacheLive), - Layer.scopedDiscard(make), + Layer.mergeAll( + ChannelsCacheLive, + InteractionsRegistryLive, + MemberCacheLive, + MessagesLive, + ), + Layer.scoped(Summarizer, make), ) diff --git a/src/main.ts b/src/main.ts index d611204..22b9a3c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,8 @@ import * as AutoThreads from "bot/AutoThreads" import { BotLive } from "bot/Bot" import { DocsLookupLive } from "bot/DocsLookup" +import * as Github from "bot/Github" +import * as Issueifier from "bot/Issueifier" import { MentionsLive } from "bot/Mentions" import * as NoEmbed from "bot/NoEmbed" import * as OpenAI from "bot/OpenAI" @@ -41,12 +43,24 @@ const NoEmbedLive = NoEmbed.makeLayer({ ), }) +const IssueifierLive = Issueifier.makeLayer({ + githubRepo: Config.withDefault( + Config.string("ISSUEIFIER_REPO"), + "effect-ts/website", + ), +}) + +const GithubLive = Github.makeLayer({ + token: Config.secret("GITHUB_TOKEN"), +}) + const MainLive = pipe( - Layer.mergeAll(DiscordLive, OpenAILive), + Layer.mergeAll(DiscordLive, GithubLive, OpenAILive), Layer.provide( Layer.mergeAll( AutoThreadsLive, DocsLookupLive, + IssueifierLive, NoEmbedLive, MentionsLive, SummarizerLive, diff --git a/tsconfig.json b/tsconfig.json index 9972ec8..ee8e119 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "moduleResolution": "NodeNext", "module": "ESNext", "allowSyntheticDefaultImports": true, + "noErrorTruncation": true, "lib": ["ESNext", "DOM"], "sourceMap": true, "strict": true,