Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to use curate.config.json #4

Merged
merged 3 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"packageManager": "bun@1.0.27",
"type": "module",
"scripts": {
"build": "bun build ./src/index.ts --target=bun --outdir=dist --format=esm --external './src/services/exports/external' && cp -r src/services/exports/external dist/external/",
"build": "bun build ./src/index.ts --target=bun --outdir=dist --format=esm --external './src/external' && cp -r src/external dist/external/",
"start": "bun run dist/index.js",
"dev": "bun run --watch src/index.ts",
"test": "jest",
Expand Down
6 changes: 0 additions & 6 deletions backend/src/config/admins.ts

This file was deleted.

66 changes: 7 additions & 59 deletions backend/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,5 @@
import { AppConfig, ExportConfig } from "../types";
import path from "path";

// Configure export services
const exports: ExportConfig[] = [];

// Add Telegram export if configured
if (process.env.TELEGRAM_ENABLED === "true") {
exports.push({
type: "telegram",
enabled: true,
module: "telegram",
botToken: process.env.TELEGRAM_BOT_TOKEN!,
channelId: process.env.TELEGRAM_CHANNEL_ID!,
});
}

// Add RSS export if configured
if (process.env.RSS_ENABLED === "true") {
exports.push({
type: "rss",
enabled: true,
module: "rss",
title: process.env.RSS_TITLE || "Public Goods News",
description:
process.env.RSS_DESCRIPTION || "Latest approved public goods submissions",
feedPath:
process.env.RSS_FEED_PATH ||
path.join(process.cwd(), "public", "feed.xml"),
maxItems: process.env.RSS_MAX_ITEMS
? parseInt(process.env.RSS_MAX_ITEMS)
: 100,
});
}

const config: AppConfig = {
twitter: {
username: process.env.TWITTER_USERNAME!,
password: process.env.TWITTER_PASSWORD!,
email: process.env.TWITTER_EMAIL!,
},
environment:
(process.env.NODE_ENV as "development" | "production" | "test") ||
"development",
exports,
};
import { ConfigService } from '../services/config';
import { AppConfig } from '../types/config';

export function validateEnv() {
// Validate required Twitter credentials
Expand All @@ -56,20 +12,12 @@ export function validateEnv() {
"Missing required Twitter credentials. Please ensure TWITTER_USERNAME, TWITTER_PASSWORD, and TWITTER_EMAIL are set in your environment variables.",
);
}
}

// Validate Telegram config if enabled
if (process.env.TELEGRAM_ENABLED === "true") {
if (!process.env.TELEGRAM_BOT_TOKEN || !process.env.TELEGRAM_CHANNEL_ID) {
throw new Error(
"Telegram export is enabled but missing required configuration. Please ensure TELEGRAM_BOT_TOKEN and TELEGRAM_CHANNEL_ID are set in your environment variables.",
);
}
}
const configService = ConfigService.getInstance();

// Validate RSS config if enabled
if (process.env.RSS_ENABLED === "true") {
// RSS has reasonable defaults, so no validation needed
}
export function getConfig(): AppConfig {
return configService.getConfig();
}

export default config;
export default configService;
76 changes: 76 additions & 0 deletions backend/src/external/gpt-transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { TransformerPlugin } from '../types/plugin';

interface Message {
role: 'system' | 'user' | 'assistant';
content: string;
}

interface OpenRouterResponse {
choices: {
message: {
content: string;
};
}[];
}

export default class GPTTransformer implements TransformerPlugin {
name = 'gpt-transform';
private prompt: string = '';
private apiKey: string = '';

async initialize(config: Record<string, string>): Promise<void> {
if (!config.prompt) {
throw new Error('GPT transformer requires a prompt configuration');
}
if (!config.apiKey) {
throw new Error('GPT transformer requires an OpenRouter API key');
}
this.prompt = config.prompt;
this.apiKey = config.apiKey;
}

async transform(content: string): Promise<string> {
try {
const messages: Message[] = [
{ role: 'system', content: this.prompt },
{ role: 'user', content }
];

const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'HTTP-Referer': 'https://curate.fun',
'X-Title': 'CurateDotFun'
},
body: JSON.stringify({
model: 'openai/gpt-3.5-turbo', // Default to GPT-3.5-turbo for cost efficiency
messages,
temperature: 0.7,
max_tokens: 1000
})
});

if (!response.ok) {
const error = await response.text();
throw new Error(`OpenRouter API error: ${error}`);
}

const result = await response.json() as OpenRouterResponse;

if (!result.choices?.[0]?.message?.content) {
throw new Error('Invalid response from OpenRouter API');
}

return result.choices[0].message.content;
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new Error(`GPT transformation failed: ${errorMessage}`);
}
}

async shutdown(): Promise<void> {
// Cleanup any resources if needed
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ExportService, RssConfig } from "../types";
import { TwitterSubmission } from "../../../types";
import { writeFile, readFile, mkdir } from "fs/promises";
import { existsSync } from "fs";
import path from "path";
import { DistributorPlugin } from "types/plugin";

export class RssExportService implements ExportService {
export class RssPlugin implements DistributorPlugin {
name = "rss";
private config: RssConfig;
private title: string | null = null;
private path: string | null = null;
private maxItems: number = 100;
private items: Array<{
title: string;
description: string;
Expand All @@ -15,18 +16,21 @@ export class RssExportService implements ExportService {
guid: string;
}> = [];

constructor(config: RssConfig) {
if (!config.enabled) {
throw new Error("RSS export service is not enabled");
async initialize(config: Record<string, string>): Promise<void> {
if (!config.title || !config.path) {
throw new Error("RSS plugin requires title and path");
}

this.title = config.title;
this.path = config.path;
if (config.maxItems) {
this.maxItems = parseInt(config.maxItems);
}
this.config = config;
}

async initialize(): Promise<void> {
try {
// Load existing RSS items if file exists
if (existsSync(this.config.feedPath)) {
const content = await readFile(this.config.feedPath, "utf-8");
if (existsSync(this.path)) {
const content = await readFile(this.path, "utf-8");
const match = content.match(/<item>[\s\S]*?<\/item>/g);
if (match) {
this.items = match.map((item) => {
Expand All @@ -40,49 +44,39 @@ export class RssExportService implements ExportService {
});
}
}
console.info("RSS export service initialized");
console.info("RSS plugin initialized");
} catch (error) {
console.error("Failed to initialize RSS export service:", error);
console.error("Failed to initialize RSS plugin:", error);
throw error;
}
}

async handleApprovedSubmission(submission: TwitterSubmission): Promise<void> {
try {
const item = {
title: this.formatTitle(submission),
description: submission.content,
link: `https://twitter.com/user/status/${submission.tweetId}`,
pubDate: new Date(submission.createdAt).toUTCString(),
guid: submission.tweetId,
};
async distribute(content: string): Promise<void> {
if (!this.title || !this.path) {
throw new Error("RSS plugin not initialized");
}

this.items.unshift(item);
if (this.config.maxItems) {
this.items = this.items.slice(0, this.config.maxItems);
}
const item = {
title: "New Update",
description: content,
link: "https://twitter.com/", // TODO: Update with actual link
pubDate: new Date().toUTCString(),
guid: Date.now().toString(),
};

await this.updateFeed();
console.info(`Exported submission ${submission.tweetId} to RSS`);
} catch (error) {
console.error("Failed to export submission to RSS:", error);
throw error;
}
}
this.items.unshift(item);
this.items = this.items.slice(0, this.maxItems);

private formatTitle(submission: TwitterSubmission): string {
const categories = submission.categories?.length
? ` [${submission.categories.join(", ")}]`
: "";
return `New Public Good by @${submission.username}${categories}`;
await this.updateFeed();
}

private async updateFeed(): Promise<void> {
if (!this.title || !this.path) return;

const feed = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>${this.config.title}</title>
<description>${this.config.description}</description>
<title>${this.title}</title>
<link>https://twitter.com/</link>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${this.items
Expand All @@ -101,9 +95,9 @@ export class RssExportService implements ExportService {
</rss>`;

// Ensure directory exists
const dir = path.dirname(this.config.feedPath);
const dir = path.dirname(this.path);
await mkdir(dir, { recursive: true });
await writeFile(this.config.feedPath, feed, "utf-8");
await writeFile(this.path, feed, "utf-8");
}

private escapeXml(unsafe: string): string {
Expand Down
67 changes: 67 additions & 0 deletions backend/src/external/telegram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { DistributorPlugin } from "../types/plugin";

export class TelegramPlugin implements DistributorPlugin {
name = "telegram";
private botToken: string | null = null;
private channelId: string | null = null;

async initialize(config: Record<string, string>): Promise<void> {
// Validate required config
if (!config.botToken || !config.channelId) {
throw new Error("Telegram plugin requires botToken and channelId");
}

this.botToken = config.botToken;
this.channelId = config.channelId;

try {
// Validate credentials
const response = await fetch(
`https://api.telegram.org/bot${this.botToken}/getChat?chat_id=${this.channelId}`,
);
if (!response.ok) {
throw new Error("Failed to validate Telegram credentials");
}
console.info("Telegram plugin initialized");
} catch (error) {
console.error("Failed to initialize Telegram plugin:", error);
throw error;
}
}

async distribute(content: string): Promise<void> {
if (!this.botToken || !this.channelId) {
throw new Error("Telegram plugin not initialized");
}

const message = this.formatMessage(content);
await this.sendMessage(message);
}

private formatMessage(content: string): string {
// TODO
return content;
}

private async sendMessage(text: string): Promise<void> {
const response = await fetch(
`https://api.telegram.org/bot${this.botToken}/sendMessage`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_id: this.channelId,
text,
parse_mode: "HTML",
}),
},
);

if (!response.ok) {
const error = await response.json();
throw new Error(`Telegram API error: ${JSON.stringify(error)}`);
}
}
}
Loading