Skip to content

Commit

Permalink
Merge pull request #7 from theosli/feature/newsletter_mail
Browse files Browse the repository at this point in the history
Send default newsletter mail
  • Loading branch information
benoitchazoule authored Jan 13, 2025
2 parents 4faf9d8 + c8a24d2 commit d81a4f9
Show file tree
Hide file tree
Showing 8 changed files with 1,867 additions and 1,144 deletions.
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POSTMARK_SERVER_API_KEY=
POSTMARK_DEFAULT_MAIL=
OPENAI_API_KEY=
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ install: ## Install the dependencies

webpage: ## Run the webpage localy
cd website && npm run start


send_mail: ## Send newsletter mail
cp -n .env.sample .env
npx ts-node curator/src/mail_agent/newsletter_script.ts

build: ## Compile the project
npm run build
npm run format
Expand Down
26 changes: 26 additions & 0 deletions curator/README.md
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.
32 changes: 32 additions & 0 deletions curator/eslint.config.mjs
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,
];
16 changes: 15 additions & 1 deletion curator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,23 @@
"src"
],
"devDependencies": {
"@eslint/js": "^9.16.0",
"@types/cli-progress": "^3.11.5",
"@types/jsdom": "^21.1.6",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.6.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"globals": "^15.13.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"typescript-eslint": "^8.18.0"
},
"dependencies": {
"@mozilla/readability": "^0.5.0",
Expand All @@ -31,6 +44,7 @@
"dotenv": "^16.3.1",
"jsdom": "^24.0.0",
"openai": "^4.24.3",
"postmark": "^4.0.5",
"rss-parser": "^3.13.0"
},
"scripts": {
Expand Down
51 changes: 51 additions & 0 deletions curator/src/mail_agent/newsletter_script.ts
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();
115 changes: 115 additions & 0 deletions curator/src/mail_agent/newsletter_txt_format.ts
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: '' };
});
}
Loading

0 comments on commit d81a4f9

Please sign in to comment.