Skip to content

Commit

Permalink
Merge pull request #4 from PotLock/feat/config
Browse files Browse the repository at this point in the history
Upgrade to use curate.config.json
  • Loading branch information
elliotBraem authored Jan 14, 2025
2 parents 108286f + f21301d commit 6e05fc0
Show file tree
Hide file tree
Showing 24 changed files with 1,198 additions and 675 deletions.
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

0 comments on commit 6e05fc0

Please sign in to comment.