-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from theosli/feature/newsletter_mail
Send default newsletter mail
- Loading branch information
Showing
8 changed files
with
1,867 additions
and
1,144 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
POSTMARK_SERVER_API_KEY= | ||
POSTMARK_DEFAULT_MAIL= | ||
OPENAI_API_KEY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Curator AI - Newsletter Mail Agent | ||
|
||
Mailing part of the Newsletter. Uses the curate agent to generate the content and formats the data to send it in an email. | ||
|
||
## Send a sample email | ||
|
||
First, define the POSTMARK_SERVER_API_KEY and POSTMARK_DEFAULT_MAIL in .env located at root of the repository. | ||
|
||
```txt | ||
POSTMARK_SERVER_API_KEY=XXX | ||
POSTMARK_DEFAULT_MAIL=XXX | ||
``` | ||
|
||
Make sure you have dependencies installed : | ||
|
||
```sh | ||
make install | ||
``` | ||
|
||
Send a sample email from the root of the project : | ||
|
||
```sh | ||
make send_mail | ||
``` | ||
|
||
The mail will be sent to the mail you defined in POSTMARK_DEFAULT_MAIL. It's visible in both raw and html format. If your mail agent doesn't show one of them, you can visualize both on Postmark --> Your server --> Transactional Stream --> Activity. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import globals from 'globals'; | ||
import pluginJs from '@eslint/js'; | ||
import tsEsLint from 'typescript-eslint'; | ||
import pluginReact from 'eslint-plugin-react'; | ||
import pluginReactHooks from 'eslint-plugin-react-hooks'; | ||
import eslintConfigPrettier from 'eslint-config-prettier'; | ||
|
||
/** @type {import('eslint').Linter.FlatConfig[]} */ | ||
export default [ | ||
{ | ||
files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'], | ||
languageOptions: { | ||
globals: { ...globals.browser, ...globals.node }, | ||
}, | ||
plugins: { | ||
react: pluginReact, | ||
'react-hooks': pluginReactHooks, | ||
}, | ||
rules: { | ||
quotes: ['error', 'single'], | ||
semi: ['error', 'always'], | ||
indent: ['error', 2], | ||
'no-multi-spaces': ['error'], | ||
'react-hooks/rules-of-hooks': 'error', | ||
'react-hooks/exhaustive-deps': 'warn', | ||
}, | ||
}, | ||
pluginJs.configs.recommended, | ||
...tsEsLint.configs.recommended, | ||
pluginReact.configs.flat.recommended, | ||
eslintConfigPrettier, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { curateAndGenerateNewsletter } from './newsletter_txt_format'; | ||
import dotenv from 'dotenv'; | ||
import { ServerClient } from 'postmark'; | ||
|
||
dotenv.config(); | ||
|
||
const send_mail = true; | ||
|
||
// Calls the curateAndGenerateNewsletter function with right parameters then sends the mail | ||
// TODO : Add all the dynamic part (newsletter parameters, email params) | ||
|
||
async function runNewsletter() { | ||
const { markdown, html } = await curateAndGenerateNewsletter(); | ||
|
||
// Sending email : | ||
if ( | ||
process.env.POSTMARK_SERVER_API_KEY && | ||
process.env.POSTMARK_DEFAULT_MAIL && | ||
send_mail | ||
) { | ||
const client = new ServerClient( | ||
process.env.POSTMARK_SERVER_API_KEY as string | ||
); | ||
const mail = process.env.POSTMARK_DEFAULT_MAIL as string; | ||
|
||
client.sendEmail({ | ||
From: mail, | ||
To: mail, | ||
Subject: 'Your weekly newsletter', | ||
HtmlBody: html, | ||
TextBody: markdown, | ||
MessageStream: 'outbound', | ||
}); | ||
|
||
console.log('Newsletter email sent'); | ||
} else { | ||
if ( | ||
!process.env.POSTMARK_SERVER_API_KEY || | ||
!process.env.POSTMARK_DEFAULT_MAIL | ||
) { | ||
console.error( | ||
'Make sure to define POSTMARK_SERVER_API_KEY and POSTMARK_DEFAULT_MAIL if you want to send mail' | ||
); | ||
} | ||
|
||
console.log('Here is the current result :'); | ||
console.log(markdown); | ||
} | ||
} | ||
|
||
runNewsletter(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { curate } from '../curate'; | ||
|
||
import { Summary } from '../types'; | ||
|
||
// links to curate from (TODO : make it based on user preference) | ||
|
||
const links = [ | ||
'https://www.fromjason.xyz/p/notebook/where-have-all-the-websites-gone/', | ||
'https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/', | ||
'https://biomejs.dev/blog/biome-v1-5/', | ||
'https://birtles.blog/2024/01/06/weird-things-engineers-believe-about-development/', | ||
'https://julesblom.com/writing/flushsync', | ||
]; | ||
|
||
// Formats the newsletter in markdown | ||
// articles : Summary[] | ||
// | ||
// return newsletter : String | ||
|
||
function formatNewsletterMarkdown(articles: Summary[]) { | ||
// Newletter's header | ||
|
||
let newsletter = 'Newsletter\n\n'; | ||
newsletter += 'Hello everyone! Here are the latest news!\n\n'; | ||
|
||
// Formatting for each article | ||
|
||
articles.forEach(article => { | ||
const title = article.title || 'Title not available'; | ||
const author = article.author || 'Unknown author'; | ||
const summary = article.summary || 'Summary not available.'; | ||
const link = article.link || '#'; | ||
|
||
newsletter += `### ${title}\n`; | ||
newsletter += `*by ${author}*\n`; | ||
newsletter += `🔗 [Read the full article](${link})\n\n`; | ||
newsletter += `> ${summary}\n\n`; | ||
}); | ||
|
||
// Footer | ||
|
||
newsletter += "\nThat's all for now! See you soon for more news!\n"; | ||
|
||
return newsletter; | ||
} | ||
|
||
// Formats the newsletter in html with style | ||
// articles : Summary[] | ||
// | ||
// return newsletter : String | ||
|
||
function formatNewsletterHtmlWithCSS(articles: Summary[]) { | ||
// Header | ||
|
||
let htmlNewsletter = ` | ||
<div style="font-family: Arial, sans-serif; background-color: #f9f9f9; color: #333; padding: 20px; border-radius: 10px; max-width: 800px; margin: 0 auto;"> | ||
<h1 style="color: #4CAF50; text-align: center; font-size: 32px;">Newsletter</h1> | ||
<p style="font-size: 18px; text-align: center;">Hello everyone! Here are the latest news!</p> | ||
`; | ||
|
||
// Formatting each article | ||
|
||
articles.forEach(article => { | ||
const title = article.title || 'Title not available'; | ||
const author = article.author || 'Unknown author'; | ||
const summary = article.summary || 'Summary not available.'; | ||
const link = article.link || '#'; | ||
|
||
htmlNewsletter += ` | ||
<div style="margin-bottom: 30px; padding: 20px; background-color: #fff; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);"> | ||
<h3 style="color: #FF5722; font-size: 24px;">${title}</h3> | ||
<p style="font-size: 16px; font-weight: bold; color: #555;">by ${author}</p> | ||
<p style="font-size: 16px;"><a href="${link}" style="color: #1E88E5; text-decoration: none; font-weight: bold;">Read the full article</a></p> | ||
<blockquote style="font-size: 16px; color: #777; border-left: 4px solid #ddd; padding-left: 15px;">${summary}</blockquote> | ||
</div> | ||
`; | ||
}); | ||
|
||
htmlNewsletter += '</div>'; | ||
return htmlNewsletter; | ||
} | ||
|
||
// Uses the curate function to generate newsletter data and formats output to be used in mail | ||
// TODO : add parameters based on user (links,interests?,maxArticles?,maxContentSize?) | ||
|
||
export function curateAndGenerateNewsletter(): Promise<{ | ||
markdown: string; | ||
html: string; | ||
}> { | ||
return curate({ | ||
links, | ||
interests: ['react', 'ai'], | ||
max: 5, | ||
}) | ||
.then((curatedLinks: Summary[]) => { | ||
// Generate the formatted string when promise completed | ||
|
||
const markdown = formatNewsletterMarkdown(curatedLinks); | ||
const html = formatNewsletterHtmlWithCSS(curatedLinks); | ||
|
||
// Returns raw json and formatted newletters | ||
|
||
return { markdown, html }; | ||
}) | ||
.catch((err: unknown) => { | ||
// Will most likely be instance of Error TODO : check if there are different error cases | ||
|
||
if (err instanceof Error) { | ||
console.error('Error during link curation: ', err.message); | ||
} else { | ||
console.error('Unknown error occurred during link curation'); | ||
} | ||
return { markdown: '', html: '' }; | ||
}); | ||
} |
Oops, something went wrong.