[Feat] Notify via Email When codeflash --all Completes (#1699)
### Without repo <img width="744" alt="Screenshot 2025-07-08 at 1 16 26 PM" src="https://github.com/user-attachments/assets/f095ce35-24be-4add-bb19-306220f09d74" /> ### With repo <img width="803" alt="Screenshot 2025-07-08 at 1 16 43 PM" src="https://github.com/user-attachments/assets/c12fd194-abe1-410a-b78d-2c33957ca510" /> --------- Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
This commit is contained in:
parent
7da78da953
commit
fedadfc6ce
6 changed files with 635 additions and 2 deletions
60
js/cf-api/endpoints/send-completed-optimization-email.ts
Normal file
60
js/cf-api/endpoints/send-completed-optimization-email.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import * as Sentry from "@sentry/node"
|
||||
import { prisma } from "@codeflash-ai/common"
|
||||
import { loadAndRenderHtml, sendEmail } from "resend/email-service.js"
|
||||
import { Response, Request } from "express"
|
||||
|
||||
export async function sendOptimizationCompletedEmail(req: Request, res: Response): Promise<void> {
|
||||
const user = await prisma.users.findUnique({
|
||||
where: { user_id: (req as any).userId },
|
||||
})
|
||||
|
||||
if (user?.email && user?.name) {
|
||||
const { repo, owner } = req.body
|
||||
const showRepo = owner && repo
|
||||
try {
|
||||
const html = await loadAndRenderHtml("resend/completed_optimization_email_template.html", {
|
||||
userName: user.github_username,
|
||||
...{
|
||||
PR: showRepo
|
||||
? ` <div style="text-align: center; margin: 30px 0;">
|
||||
<a href="https://github.com/${owner}/${repo}/pulls/app%2Fcodeflash-ai" class="cta-button">
|
||||
<span class="pr-icon">🔀</span>
|
||||
View Pull Requests
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
: "",
|
||||
repoHtml: showRepo
|
||||
? `<a
|
||||
href="https://github.com/${owner}/${repo}"
|
||||
target="_blank"
|
||||
style="text-decoration: none; color: inherit"
|
||||
>
|
||||
<div class="repo-info">
|
||||
<img
|
||||
class="repo-avatar"
|
||||
src="https://github.com/${owner}.png?size=20"
|
||||
alt="${owner}'s avatar"
|
||||
/>
|
||||
<div class="repo-name-text">${owner}/${repo}</div>
|
||||
</div>
|
||||
</a>`
|
||||
: "",
|
||||
},
|
||||
})
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: `Codeflash: Optimization Completed${showRepo ? ` For ${owner}/${repo}` : ""}`,
|
||||
html,
|
||||
})
|
||||
res.status(200).json({ status: "success", message: "Email has been successfully sent." })
|
||||
return
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
Sentry.captureException(error)
|
||||
res.status(500).json({ status: "error", message: "Failed to send email." })
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,8 @@ import {
|
|||
} from "./endpoints/code-context-hash.js"
|
||||
import { optimizationSuccess } from "./endpoints/optimiaztion-success.js"
|
||||
import { handleSlackEvents } from "./endpoints/slack-events.js"
|
||||
import { sendOptimizationCompletedEmail } from "./endpoints/send-completed-optimization-email.js"
|
||||
|
||||
const port = process.env.PORT ?? 3001
|
||||
// Define a custom type for the wrapped Express app
|
||||
const app = express()
|
||||
|
|
@ -156,6 +158,8 @@ appExpress.get("/cfapi/cli-get-user", getUser)
|
|||
// appExpress.getAsync("/cfapi/installed_repositories", installedRepositories)
|
||||
appExpress.postAsync("/cfapi/test-repo", addRepositoryManually)
|
||||
|
||||
appExpress.postAsync("/cfapi/send-completion-email", sendOptimizationCompletedEmail)
|
||||
|
||||
appExpress.postAsync("/cfapi/mark-as-success", optimizationSuccess)
|
||||
|
||||
appExpress.postAsync("/cfapi/suggest-pr-changes", suggestPrChanges)
|
||||
|
|
|
|||
249
js/cf-api/package-lock.json
generated
249
js/cf-api/package-lock.json
generated
|
|
@ -36,9 +36,11 @@
|
|||
"express": "^4.19.2",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"http": "^0.0.1-security",
|
||||
"marked": "^16.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"octokit": "^5.0.2",
|
||||
"posthog-node": "^4.0.0",
|
||||
"resend": "^4.6.0",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"stripe": "^17.7.0",
|
||||
"tsx": "^4.1.4"
|
||||
|
|
@ -4376,6 +4378,24 @@
|
|||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/render": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.2.tgz",
|
||||
"integrity": "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"html-to-text": "^9.0.5",
|
||||
"prettier": "^3.5.3",
|
||||
"react-promise-suspense": "^0.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
|
|
@ -4383,6 +4403,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@selderee/plugin-htmlparser2": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.3",
|
||||
"selderee": "^0.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/node-cpu-profiler": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.2.0.tgz",
|
||||
|
|
@ -6572,7 +6605,6 @@
|
|||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -6762,6 +6794,61 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
||||
|
|
@ -6855,6 +6942,18 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/environment": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
|
||||
|
|
@ -8491,6 +8590,41 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-to-text": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@selderee/plugin-htmlparser2": "^0.11.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"htmlparser2": "^8.0.2",
|
||||
"selderee": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http": {
|
||||
"version": "0.0.1-security",
|
||||
"resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz",
|
||||
|
|
@ -10120,6 +10254,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/leac": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
|
|
@ -10593,6 +10736,18 @@
|
|||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-16.0.0.tgz",
|
||||
"integrity": "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -11234,6 +11389,19 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parseley": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"leac": "^0.6.0",
|
||||
"peberminta": "^0.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
|
@ -11294,6 +11462,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/peberminta": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
|
|
@ -11512,7 +11689,6 @@
|
|||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
|
|
@ -11705,6 +11881,29 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
|
|
@ -11712,6 +11911,21 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-promise-suspense": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
|
||||
"integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-promise-suspense/node_modules/fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
|
||||
|
|
@ -11792,6 +12006,18 @@
|
|||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resend": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/resend/-/resend-4.6.0.tgz",
|
||||
"integrity": "sha512-D5T2I82FvEUYFlrHzaDvVtr5ADHdhuoLaXgLFGABKyNtQgPWIuz0Vp2L2Evx779qjK37aF4kcw1yXJDHhA2JnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-email/render": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
|
|
@ -12085,6 +12311,25 @@
|
|||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/selderee": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
||||
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parseley": "^0.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
|
|
|
|||
|
|
@ -51,9 +51,11 @@
|
|||
"express": "^4.19.2",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"http": "^0.0.1-security",
|
||||
"marked": "^16.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"octokit": "^5.0.2",
|
||||
"posthog-node": "^4.0.0",
|
||||
"resend": "^4.6.0",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"stripe": "^17.7.0",
|
||||
"tsx": "^4.1.4"
|
||||
|
|
|
|||
244
js/cf-api/resend/completed_optimization_email_template.html
Normal file
244
js/cf-api/resend/completed_optimization_email_template.html
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Optimization Complete</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: hsl(0, 0%, 99%);
|
||||
color: hsl(222.2, 84%, 4.9%);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: hsl(0, 0%, 100%);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, hsl(38, 100%, 63%), hsl(41, 88%, 70%));
|
||||
padding: 30px 40px;
|
||||
text-align: center;
|
||||
color: hsl(0, 6%, 4%);
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
display: inline-block;
|
||||
background-color: hsl(0, 0%, 100%);
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.repo-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #f6f8fa;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 6px;
|
||||
padding: 4px 12px;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
.repo-info:hover {
|
||||
background-color: #eaeef2;
|
||||
}
|
||||
|
||||
.repo-avatar {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.repo-name-text {
|
||||
color: #0969da;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Hide repo info if empty */
|
||||
.repo-info:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
color: hsl(222.2, 84%, 4.9%);
|
||||
}
|
||||
|
||||
.main-message {
|
||||
background-color: hsl(41, 20%, 96%);
|
||||
border-left: 4px solid hsl(38, 100%, 63%);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.main-message h2 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
color: hsl(38, 100%, 35%);
|
||||
}
|
||||
|
||||
.main-message p {
|
||||
margin: 0;
|
||||
color: hsl(41, 8%, 46%);
|
||||
}
|
||||
|
||||
.repo-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.repo-details:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.repo-details img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.repo-details-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.repo-name-text {
|
||||
font-weight: 600;
|
||||
color: hsl(222.2, 84%, 4.9%);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background-color: hsl(38, 100%, 63%);
|
||||
color: hsl(0, 6%, 4%);
|
||||
text-decoration: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
margin: 20px 0;
|
||||
transition: background-color 0.3s ease;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background-color: hsl(38, 100%, 55%);
|
||||
}
|
||||
|
||||
.cta-button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.pr-icon {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: hsl(41, 20%, 96%);
|
||||
padding: 20px 40px;
|
||||
text-align: center;
|
||||
color: hsl(41, 8%, 46%);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: hsl(38, 100%, 63%);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: hsl(41, 30%, 90%);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.email-container {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<div class="logo-wrapper">
|
||||
<img
|
||||
src="https://app.codeflash.ai/images/codeflash_light.svg"
|
||||
alt="Codeflash Logo"
|
||||
style="height: 40px; width: auto; vertical-align: middle"
|
||||
/>
|
||||
</div>
|
||||
<h1>Optimization Completed</h1>
|
||||
|
||||
{{repoHtml}}
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="greeting">Dear {{userName}},</div>
|
||||
|
||||
<div class="main-message">
|
||||
<h2>Optimization Completed</h2>
|
||||
<p>We are pleased to inform you that your optimization has been successfully completed. A pull request has been created with the optimized code changes.</p>
|
||||
</div>
|
||||
|
||||
{{PR}}
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
Best regards,<br />
|
||||
Codeflash Team
|
||||
</p>
|
||||
<p style="margin-top: 15px; font-size: 12px">
|
||||
This is an automated notification. Please do not reply to this email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
78
js/cf-api/resend/email-service.ts
Normal file
78
js/cf-api/resend/email-service.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { Resend } from "resend"
|
||||
import dotenv from "dotenv"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { marked } from "marked"
|
||||
dotenv.config()
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY!)
|
||||
|
||||
function escapeRegex(string: string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
function flattenObject(
|
||||
obj: any,
|
||||
prefix = "",
|
||||
res: Record<string, string> = {},
|
||||
): Record<string, string> {
|
||||
for (const key in obj) {
|
||||
const value = obj[key]
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key
|
||||
if (typeof value === "object" && value !== null) {
|
||||
flattenObject(value, fullKey, res)
|
||||
} else {
|
||||
res[fullKey] = String(value)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export async function loadAndRenderHtml(
|
||||
templatePath: string,
|
||||
params: Record<string, any>,
|
||||
): Promise<string> {
|
||||
const rawHtml = fs.readFileSync(path.resolve(templatePath), "utf-8")
|
||||
const flatParams = flattenObject(params)
|
||||
if (flatParams.optimization_explanation) {
|
||||
flatParams.optimization_explanation = await marked.parse(flatParams.optimization_explanation)
|
||||
}
|
||||
let renderedHtml = rawHtml
|
||||
for (const [key, value] of Object.entries(flatParams)) {
|
||||
const safeKey = escapeRegex(key)
|
||||
const regex = new RegExp(`{{\\s*${safeKey}\\s*}}`, "g")
|
||||
renderedHtml = renderedHtml.replace(regex, value ?? "")
|
||||
}
|
||||
return renderedHtml
|
||||
}
|
||||
|
||||
export async function sendEmail({
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
}: {
|
||||
to: string
|
||||
subject: string
|
||||
html: string
|
||||
}) {
|
||||
const fromEmail = process.env.RESEND_FROM_EMAIL
|
||||
if (!fromEmail || fromEmail.trim() === "") {
|
||||
throw new Error("RESEND_FROM_EMAIL environment variable is missing or empty")
|
||||
}
|
||||
|
||||
// TODO: Add logic to check if the email was already sent (e.g., using a database or cache)
|
||||
|
||||
console.log(fromEmail)
|
||||
|
||||
console.log(`Preparing to send email to ${to} with subject "${subject}"`)
|
||||
|
||||
const response = await resend.emails.send({
|
||||
from: fromEmail,
|
||||
to: [to],
|
||||
subject,
|
||||
html,
|
||||
})
|
||||
|
||||
console.log(`Email send response: ${JSON.stringify(response)}`)
|
||||
return response
|
||||
}
|
||||
Loading…
Reference in a new issue