diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..383b291 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +build +coverage +node_modules +.turbo +.next +.docusaurus \ No newline at end of file diff --git a/backend/src/__tests__/mocks/db-service.mock.ts b/backend/src/__tests__/mocks/db-service.mock.ts index 7d48402..3b950f0 100644 --- a/backend/src/__tests__/mocks/db-service.mock.ts +++ b/backend/src/__tests__/mocks/db-service.mock.ts @@ -11,32 +11,42 @@ const storage = { }; export const mockDb = { - upsertFeed: mock<(feed: { id: string; name: string; description?: string }) => void>(() => {}), - + upsertFeed: mock< + (feed: { id: string; name: string; description?: string }) => void + >(() => {}), + getDailySubmissionCount: mock<(userId: string) => number>((userId) => { return storage.dailySubmissionCounts.get(userId) || 0; }), - - saveSubmission: mock<(submission: TwitterSubmission) => void>((submission) => { - storage.submissions.set(submission.tweetId, submission); - }), - - saveSubmissionToFeed: mock<(submissionId: string, feedId: string) => void>((submissionId, feedId) => { - const feeds = storage.submissionFeeds.get(submissionId) || new Set(); - feeds.add(feedId); - storage.submissionFeeds.set(submissionId, feeds); - }), - + + saveSubmission: mock<(submission: TwitterSubmission) => void>( + (submission) => { + storage.submissions.set(submission.tweetId, submission); + }, + ), + + saveSubmissionToFeed: mock<(submissionId: string, feedId: string) => void>( + (submissionId, feedId) => { + const feeds = storage.submissionFeeds.get(submissionId) || new Set(); + feeds.add(feedId); + storage.submissionFeeds.set(submissionId, feeds); + }, + ), + incrementDailySubmissionCount: mock<(userId: string) => void>((userId) => { const currentCount = storage.dailySubmissionCounts.get(userId) || 0; storage.dailySubmissionCounts.set(userId, currentCount + 1); }), - - updateSubmissionAcknowledgment: mock<(tweetId: string, acknowledgmentTweetId: string) => void>((tweetId, ackId) => { + + updateSubmissionAcknowledgment: mock< + (tweetId: string, acknowledgmentTweetId: string) => void + >((tweetId, ackId) => { storage.acknowledgments.set(tweetId, ackId); }), - - getSubmissionByAcknowledgmentTweetId: mock<(acknowledgmentTweetId: string) => TwitterSubmission | null>((ackId) => { + + getSubmissionByAcknowledgmentTweetId: mock< + (acknowledgmentTweetId: string) => TwitterSubmission | null + >((ackId) => { for (const [tweetId, storedAckId] of storage.acknowledgments.entries()) { if (storedAckId === ackId) { return storage.submissions.get(tweetId) || null; @@ -44,23 +54,33 @@ export const mockDb = { } return null; }), - + saveModerationAction: mock<(moderation: any) => void>(() => {}), - - updateSubmissionStatus: mock<(tweetId: string, status: "approved" | "rejected", responseTweetId: string) => void>((tweetId, status, responseId) => { + + updateSubmissionStatus: mock< + ( + tweetId: string, + status: "approved" | "rejected", + responseTweetId: string, + ) => void + >((tweetId, status, responseId) => { const submission = storage.submissions.get(tweetId); if (submission) { submission.status = status; storage.moderationResponses.set(tweetId, responseId); } }), - - getFeedsBySubmission: mock<(submissionId: string) => Array<{ feedId: string }>>((submissionId) => { + + getFeedsBySubmission: mock< + (submissionId: string) => Array<{ feedId: string }> + >((submissionId) => { const feeds = storage.submissionFeeds.get(submissionId) || new Set(); - return Array.from(feeds).map(feedId => ({ feedId })); + return Array.from(feeds).map((feedId) => ({ feedId })); }), - - removeFromSubmissionFeed: mock<(submissionId: string, feedId: string) => void>((submissionId, feedId) => { + + removeFromSubmissionFeed: mock< + (submissionId: string, feedId: string) => void + >((submissionId, feedId) => { const feeds = storage.submissionFeeds.get(submissionId); if (feeds) { feeds.delete(feedId); @@ -70,7 +90,7 @@ export const mockDb = { // Helper to reset all mock functions and storage export const resetMockDb = () => { - Object.values(mockDb).forEach(mockFn => mockFn.mockReset()); + Object.values(mockDb).forEach((mockFn) => mockFn.mockReset()); storage.submissions.clear(); storage.submissionFeeds.clear(); storage.dailySubmissionCounts.clear(); diff --git a/backend/src/__tests__/mocks/distribution-service.mock.ts b/backend/src/__tests__/mocks/distribution-service.mock.ts index 043db3d..3037bd0 100644 --- a/backend/src/__tests__/mocks/distribution-service.mock.ts +++ b/backend/src/__tests__/mocks/distribution-service.mock.ts @@ -8,7 +8,7 @@ export class MockDistributionService { async processStreamOutput( feedId: string, submissionId: string, - content: string + content: string, ): Promise { this.processedSubmissions.push({ feedId, submissionId, content }); } diff --git a/backend/src/__tests__/mocks/drizzle.mock.ts b/backend/src/__tests__/mocks/drizzle.mock.ts index c927091..82b48e2 100644 --- a/backend/src/__tests__/mocks/drizzle.mock.ts +++ b/backend/src/__tests__/mocks/drizzle.mock.ts @@ -1,38 +1,65 @@ -import { mock } from 'bun:test'; -import { TwitterSubmission } from '../../types/twitter'; +import { mock } from "bun:test"; +import { TwitterSubmission } from "../../types/twitter"; // Define the database interface to match our schema interface DbInterface { - upsertFeed: (feed: { id: string; name: string; description?: string }) => void; + upsertFeed: (feed: { + id: string; + name: string; + description?: string; + }) => void; getDailySubmissionCount: (userId: string) => number; saveSubmission: (submission: TwitterSubmission) => void; saveSubmissionToFeed: (submissionId: string, feedId: string) => void; incrementDailySubmissionCount: (userId: string) => void; - updateSubmissionAcknowledgment: (tweetId: string, acknowledgmentTweetId: string) => void; - getSubmissionByAcknowledgmentTweetId: (acknowledgmentTweetId: string) => Promise; + updateSubmissionAcknowledgment: ( + tweetId: string, + acknowledgmentTweetId: string, + ) => void; + getSubmissionByAcknowledgmentTweetId: ( + acknowledgmentTweetId: string, + ) => Promise; saveModerationAction: (moderation: any) => void; - updateSubmissionStatus: (tweetId: string, status: "approved" | "rejected", responseTweetId: string) => void; - getFeedsBySubmission: (submissionId: string) => Promise>; + updateSubmissionStatus: ( + tweetId: string, + status: "approved" | "rejected", + responseTweetId: string, + ) => void; + getFeedsBySubmission: ( + submissionId: string, + ) => Promise>; removeFromSubmissionFeed: (submissionId: string, feedId: string) => void; } // Create mock functions for each database operation export const drizzleMock = { - upsertFeed: mock(() => {}), - getDailySubmissionCount: mock(() => 0), - saveSubmission: mock(() => {}), - saveSubmissionToFeed: mock(() => {}), - incrementDailySubmissionCount: mock(() => {}), - updateSubmissionAcknowledgment: mock(() => {}), - getSubmissionByAcknowledgmentTweetId: mock(() => Promise.resolve(null)), - saveModerationAction: mock(() => {}), - updateSubmissionStatus: mock(() => {}), - getFeedsBySubmission: mock(() => Promise.resolve([])), - removeFromSubmissionFeed: mock(() => {}), + upsertFeed: mock(() => {}), + getDailySubmissionCount: mock( + () => 0, + ), + saveSubmission: mock(() => {}), + saveSubmissionToFeed: mock(() => {}), + incrementDailySubmissionCount: mock< + DbInterface["incrementDailySubmissionCount"] + >(() => {}), + updateSubmissionAcknowledgment: mock< + DbInterface["updateSubmissionAcknowledgment"] + >(() => {}), + getSubmissionByAcknowledgmentTweetId: mock< + DbInterface["getSubmissionByAcknowledgmentTweetId"] + >(() => Promise.resolve(null)), + saveModerationAction: mock(() => {}), + updateSubmissionStatus: mock(() => {}), + getFeedsBySubmission: mock(() => + Promise.resolve([]), + ), + removeFromSubmissionFeed: mock( + () => {}, + ), }; // Mock the db module -import { db } from '../../services/db'; +import { db } from "../../services/db"; Object.assign(db, drizzleMock); export default drizzleMock; diff --git a/backend/src/__tests__/mocks/twitter-service.mock.ts b/backend/src/__tests__/mocks/twitter-service.mock.ts index f93bbc4..8de32ad 100644 --- a/backend/src/__tests__/mocks/twitter-service.mock.ts +++ b/backend/src/__tests__/mocks/twitter-service.mock.ts @@ -42,12 +42,12 @@ export class MockTwitterService { } async getTweet(tweetId: string): Promise { - return this.mockTweets.find(t => t.id === tweetId) || null; + return this.mockTweets.find((t) => t.id === tweetId) || null; } async fetchAllNewMentions(lastCheckedId: string | null): Promise { if (!lastCheckedId) return this.mockTweets; - const index = this.mockTweets.findIndex(t => t.id === lastCheckedId); + const index = this.mockTweets.findIndex((t) => t.id === lastCheckedId); if (index === -1) return this.mockTweets; return this.mockTweets.slice(index + 1); } diff --git a/backend/src/__tests__/submission.service.test.ts b/backend/src/__tests__/submission.service.test.ts index 6c4770f..08ee1a6 100644 --- a/backend/src/__tests__/submission.service.test.ts +++ b/backend/src/__tests__/submission.service.test.ts @@ -10,12 +10,12 @@ describe("SubmissionService", () => { let submissionService: SubmissionService; let mockTwitterService: MockTwitterService; let mockDistributionService: MockDistributionService; - + const mockConfig: AppConfig = { global: { botId: "test_bot", maxSubmissionsPerUser: 5, - defaultStatus: "pending" + defaultStatus: "pending", }, feeds: [ { @@ -24,15 +24,15 @@ describe("SubmissionService", () => { description: "Test feed for unit tests", moderation: { approvers: { - twitter: ["admin1"] - } + twitter: ["admin1"], + }, }, outputs: { stream: { enabled: true, - distribute: [] - } - } + distribute: [], + }, + }, }, { id: "test2", @@ -40,31 +40,31 @@ describe("SubmissionService", () => { description: "Second test feed", moderation: { approvers: { - twitter: ["admin1"] - } + twitter: ["admin1"], + }, }, outputs: { stream: { enabled: true, - distribute: [] - } - } - } + distribute: [], + }, + }, + }, ], - plugins: {} as PluginsConfig + plugins: {} as PluginsConfig, }; beforeEach(async () => { // Reset drizzle mock - Object.values(drizzleMock).forEach(mockFn => mockFn.mockReset()); - + Object.values(drizzleMock).forEach((mockFn) => mockFn.mockReset()); + // Create fresh instances mockTwitterService = new MockTwitterService(); mockDistributionService = new MockDistributionService(); submissionService = new SubmissionService( mockTwitterService as any, mockDistributionService as any, - mockConfig + mockConfig, ); // Setup admin user ID @@ -92,7 +92,7 @@ describe("SubmissionService", () => { photos: [], urls: [], videos: [], - thread: [] + thread: [], }; const submissionTweet: Tweet = { @@ -107,7 +107,7 @@ describe("SubmissionService", () => { photos: [], urls: [], videos: [], - thread: [] + thread: [], }; mockTwitterService.addMockTweet(originalTweet); @@ -117,12 +117,12 @@ describe("SubmissionService", () => { await submissionService.startMentionsCheck(); // Wait for processing - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); // Verify submission was saved expect(drizzleMock.saveSubmission).toHaveBeenCalledTimes(1); expect(drizzleMock.saveSubmissionToFeed).toHaveBeenCalledTimes(1); - + const savedSubmissionCall = drizzleMock.saveSubmission.mock.calls[0]; expect(savedSubmissionCall[0].tweetId).toBe("original1"); expect(savedSubmissionCall[0].status).toBe("pending"); @@ -141,7 +141,7 @@ describe("SubmissionService", () => { photos: [], urls: [], videos: [], - thread: [] + thread: [], }; const submissionTweet: Tweet = { @@ -156,14 +156,14 @@ describe("SubmissionService", () => { photos: [], urls: [], videos: [], - thread: [] + thread: [], }; // First process the submission mockTwitterService.addMockTweet(originalTweet); mockTwitterService.addMockTweet(submissionTweet); await submissionService.startMentionsCheck(); - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); // Clear tweets and add moderation tweet mockTwitterService.clearMockTweets(); @@ -179,7 +179,7 @@ describe("SubmissionService", () => { photos: [], urls: [], videos: [], - thread: [] + thread: [], }; // Setup mocks for moderation @@ -191,22 +191,24 @@ describe("SubmissionService", () => { status: "pending", moderationHistory: [], createdAt: new Date().toISOString(), - submittedAt: new Date().toISOString() + submittedAt: new Date().toISOString(), }); - drizzleMock.getFeedsBySubmission.mockResolvedValue([ - { feedId: "test" } - ]); + drizzleMock.getFeedsBySubmission.mockResolvedValue([{ feedId: "test" }]); // Process moderation mockTwitterService.addMockTweet(moderationTweet); await submissionService.startMentionsCheck(); - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); // Verify first distribution expect(mockDistributionService.processedSubmissions.length).toBe(1); - expect(mockDistributionService.processedSubmissions[0].submissionId).toBe("original1"); - expect(mockDistributionService.processedSubmissions[0].feedId).toBe("test"); + expect(mockDistributionService.processedSubmissions[0].submissionId).toBe( + "original1", + ); + expect(mockDistributionService.processedSubmissions[0].feedId).toBe( + "test", + ); // Now simulate moving back lastCheckedId and reprocessing mockTwitterService.clearMockTweets(); @@ -218,7 +220,7 @@ describe("SubmissionService", () => { mockTwitterService.addMockTweet(submissionTweet); mockTwitterService.addMockTweet(moderationTweet); await submissionService.startMentionsCheck(); - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); // Verify no new distributions occurred expect(mockDistributionService.processedSubmissions.length).toBe(0); @@ -237,7 +239,7 @@ describe("SubmissionService", () => { photos: [], urls: [], videos: [], - thread: [] + thread: [], }; const submissionTweet: Tweet = { @@ -252,7 +254,7 @@ describe("SubmissionService", () => { photos: [], urls: [], videos: [], - thread: [] + thread: [], }; mockTwitterService.addMockTweet(originalTweet); @@ -260,7 +262,7 @@ describe("SubmissionService", () => { // Process submission await submissionService.startMentionsCheck(); - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); // Verify submission was saved to both feeds expect(drizzleMock.saveSubmissionToFeed).toHaveBeenCalledTimes(2); diff --git a/backend/src/config/config.ts b/backend/src/config/config.ts index 6cfc4c6..4dd7e04 100644 --- a/backend/src/config/config.ts +++ b/backend/src/config/config.ts @@ -1,5 +1,5 @@ -import { ConfigService } from '../services/config'; -import { AppConfig } from '../types/config'; +import { ConfigService } from "../services/config"; +import { AppConfig } from "../types/config"; export function validateEnv() { // Validate required Twitter credentials diff --git a/backend/src/external/gpt-transform.ts b/backend/src/external/gpt-transform.ts index ed25e60..85b963a 100644 --- a/backend/src/external/gpt-transform.ts +++ b/backend/src/external/gpt-transform.ts @@ -1,7 +1,7 @@ -import { TransformerPlugin } from '../types/plugin'; +import { TransformerPlugin } from "../types/plugin"; interface Message { - role: 'system' | 'user' | 'assistant'; + role: "system" | "user" | "assistant"; content: string; } @@ -14,16 +14,16 @@ interface OpenRouterResponse { } export default class GPTTransformer implements TransformerPlugin { - name = 'gpt-transform'; - private prompt: string = ''; - private apiKey: string = ''; + name = "gpt-transform"; + private prompt: string = ""; + private apiKey: string = ""; async initialize(config: Record): Promise { if (!config.prompt) { - throw new Error('GPT transformer requires a prompt configuration'); + throw new Error("GPT transformer requires a prompt configuration"); } if (!config.apiKey) { - throw new Error('GPT transformer requires an OpenRouter API key'); + throw new Error("GPT transformer requires an OpenRouter API key"); } this.prompt = config.prompt; this.apiKey = config.apiKey; @@ -32,40 +32,44 @@ export default class GPTTransformer implements TransformerPlugin { async transform(content: string): Promise { try { const messages: Message[] = [ - { role: 'system', content: this.prompt }, - { role: 'user', content } + { 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' + 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, + }), }, - 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; - + const result = (await response.json()) as OpenRouterResponse; + if (!result.choices?.[0]?.message?.content) { - throw new Error('Invalid response from OpenRouter API'); + 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'; + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; throw new Error(`GPT transformation failed: ${errorMessage}`); } } diff --git a/backend/src/index.ts b/backend/src/index.ts index 4c56477..053c156 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -49,7 +49,7 @@ export async function main() { const twitterService = new TwitterService({ username: process.env.TWITTER_USERNAME!, password: process.env.TWITTER_PASSWORD!, - email: process.env.TWITTER_EMAIL! + email: process.env.TWITTER_EMAIL!, }); await twitterService.initialize(); succeedSpinner("twitter-init", "Twitter service initialized"); @@ -66,7 +66,7 @@ export async function main() { const submissionService = new SubmissionService( twitterService, distributionService, - config + config, ); await submissionService.initialize(); succeedSpinner("submission-init", "Submission service initialized"); @@ -82,13 +82,13 @@ export async function main() { open: (ws) => { activeConnections.add(ws); logger.debug( - `WebSocket client connected. Total connections: ${activeConnections.size}` + `WebSocket client connected. Total connections: ${activeConnections.size}`, ); }, close: (ws) => { activeConnections.delete(ws); logger.debug( - `WebSocket client disconnected. Total connections: ${activeConnections.size}` + `WebSocket client disconnected. Total connections: ${activeConnections.size}`, ); }, message: () => { @@ -109,23 +109,27 @@ export async function main() { return { success: true }; }) .get("/api/submissions", ({ query }) => { - const status = query?.status as "pending" | "approved" | "rejected" | null; + const status = query?.status as + | "pending" + | "approved" + | "rejected" + | null; return status ? db.getSubmissionsByStatus(status) : db.getAllSubmissions(); }) .get("/api/feed/:hashtagId", ({ params: { hashtagId } }) => { const config = configService.getConfig(); - const feed = config.feeds.find(f => f.id === hashtagId); + const feed = config.feeds.find((f) => f.id === hashtagId); if (!feed) { throw new Error(`Feed not found: ${hashtagId}`); } - return db.getSubmissionsByFeed(hashtagId) + return db.getSubmissionsByFeed(hashtagId); }) .get("/api/submissions/:hashtagId", ({ params: { hashtagId } }) => { const config = configService.getConfig(); - const feed = config.feeds.find(f => f.id === hashtagId); + const feed = config.feeds.find((f) => f.id === hashtagId); if (!feed) { throw new Error(`Feed not found: ${hashtagId}`); } @@ -144,7 +148,7 @@ export async function main() { }) .get("/api/config/:feedId", ({ params: { feedId } }) => { const config = configService.getConfig(); - const feed = config.feeds.find(f => f.id === feedId); + const feed = config.feeds.find((f) => f.id === feedId); if (!feed) { throw new Error(`Feed not found: ${feedId}`); } @@ -153,14 +157,16 @@ export async function main() { .post("/api/feeds/:feedId/process", async ({ params: { feedId } }) => { // Get feed config const config = configService.getConfig(); - const feed = config.feeds.find(f => f.id === feedId); + const feed = config.feeds.find((f) => f.id === feedId); if (!feed) { throw new Error(`Feed not found: ${feedId}`); } // Get approved submissions for this feed - const submissions = db.getSubmissionsByFeed(feedId).filter(sub => sub.status === "approved"); - + const submissions = db + .getSubmissionsByFeed(feedId) + .filter((sub) => sub.status === "approved"); + if (submissions.length === 0) { return { processed: 0 }; } @@ -169,10 +175,17 @@ export async function main() { let processed = 0; for (const submission of submissions) { try { - await distributionService.processStreamOutput(feedId, submission.tweetId, submission.content); + await distributionService.processStreamOutput( + feedId, + submission.tweetId, + submission.content, + ); processed++; } catch (error) { - logger.error(`Error processing submission ${submission.tweetId}:`, error); + logger.error( + `Error processing submission ${submission.tweetId}:`, + error, + ); } } @@ -184,14 +197,14 @@ export async function main() { const url = new URL(request.url); const filePath = url.pathname === "/" ? "/index.html" : url.pathname; const file = Bun.file( - path.join(__dirname, "../../frontend/dist", filePath) + path.join(__dirname, "../../frontend/dist", filePath), ); if (await file.exists()) { return new Response(file); } // Fallback to index.html for client-side routing return new Response( - Bun.file(path.join(__dirname, "../../frontend/dist/index.html")) + Bun.file(path.join(__dirname, "../../frontend/dist/index.html")), ); } throw new Error("Not found"); @@ -199,13 +212,17 @@ export async function main() { .onError(({ error }) => { logger.error("Request error:", error); return new Response( - JSON.stringify({ - error: error instanceof Error ? error.message : "Internal server error" - }), - { - status: error instanceof Error && error.message === "Not found" ? 404 : 500, - headers: { "Content-Type": "application/json" } - } + JSON.stringify({ + error: + error instanceof Error ? error.message : "Internal server error", + }), + { + status: + error instanceof Error && error.message === "Not found" + ? 404 + : 500, + headers: { "Content-Type": "application/json" }, + }, ); }) .listen(PORT); @@ -219,7 +236,7 @@ export async function main() { await Promise.all([ twitterService.stop(), submissionService.stop(), - distributionService.shutdown() + distributionService.shutdown(), ]); succeedSpinner("shutdown", "Shutdown complete"); process.exit(0); diff --git a/backend/src/services/config/config.service.ts b/backend/src/services/config/config.service.ts index aa52944..93b6acc 100644 --- a/backend/src/services/config/config.service.ts +++ b/backend/src/services/config/config.service.ts @@ -1,7 +1,7 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { AppConfig } from '../../types/config'; -import { hydrateConfigValues } from '../../utils/config'; +import fs from "fs/promises"; +import path from "path"; +import { AppConfig } from "../../types/config"; +import { hydrateConfigValues } from "../../utils/config"; export class ConfigService { private static instance: ConfigService; @@ -10,7 +10,7 @@ export class ConfigService { private constructor() { // Default to local config file path - this.configPath = path.resolve(process.cwd(), '../curate.config.json'); + this.configPath = path.resolve(process.cwd(), "../curate.config.json"); } public static getInstance(): ConfigService { @@ -23,7 +23,7 @@ export class ConfigService { public async loadConfig(): Promise { try { // This could be replaced with an API call in the future - const configFile = await fs.readFile(this.configPath, 'utf-8'); + const configFile = await fs.readFile(this.configPath, "utf-8"); const parsedConfig = JSON.parse(configFile) as AppConfig; const hydratedConfig = hydrateConfigValues(parsedConfig); this.config = hydratedConfig; @@ -36,7 +36,7 @@ export class ConfigService { public getConfig(): AppConfig { if (!this.config) { - throw new Error('Config not loaded. Call loadConfig() first.'); + throw new Error("Config not loaded. Call loadConfig() first."); } return this.config; } @@ -45,7 +45,7 @@ export class ConfigService { this.configPath = path; } - // Switch to a different config (if saving locally, wouldn't work in fly.io container) + // Switch to a different config (if saving locally, wouldn't work in fly.io container) public async updateConfig(newConfig: AppConfig): Promise { // saving this for later try { diff --git a/backend/src/services/config/index.ts b/backend/src/services/config/index.ts index 456e19e..766b0cc 100644 --- a/backend/src/services/config/index.ts +++ b/backend/src/services/config/index.ts @@ -1 +1 @@ -export { ConfigService } from './config.service'; +export { ConfigService } from "./config.service"; diff --git a/backend/src/services/db/index.ts b/backend/src/services/db/index.ts index 569ae7d..45974fe 100644 --- a/backend/src/services/db/index.ts +++ b/backend/src/services/db/index.ts @@ -40,12 +40,14 @@ export class DatabaseService { status: TwitterSubmission["status"], moderationResponseTweetId: string, ): void { - queries.updateSubmissionStatus( - this.db, - tweetId, - status, - moderationResponseTweetId, - ).run(); + queries + .updateSubmissionStatus( + this.db, + tweetId, + status, + moderationResponseTweetId, + ) + .run(); this.notifyUpdate(); } @@ -87,11 +89,9 @@ export class DatabaseService { tweetId: string, acknowledgmentTweetId: string, ): void { - queries.updateSubmissionAcknowledgment( - this.db, - tweetId, - acknowledgmentTweetId, - ).run(); + queries + .updateSubmissionAcknowledgment(this.db, tweetId, acknowledgmentTweetId) + .run(); this.notifyUpdate(); } diff --git a/backend/src/services/db/migrations/meta/0000_snapshot.json b/backend/src/services/db/migrations/meta/0000_snapshot.json index 175050d..b5471ac 100644 --- a/backend/src/services/db/migrations/meta/0000_snapshot.json +++ b/backend/src/services/db/migrations/meta/0000_snapshot.json @@ -105,16 +105,12 @@ "indexes": { "moderation_history_tweet_idx": { "name": "moderation_history_tweet_idx", - "columns": [ - "tweet_id" - ], + "columns": ["tweet_id"], "isUnique": false }, "moderation_history_admin_idx": { "name": "moderation_history_admin_idx", - "columns": [ - "admin_id" - ], + "columns": ["admin_id"], "isUnique": false } }, @@ -123,12 +119,8 @@ "name": "moderation_history_tweet_id_submissions_tweet_id_fk", "tableFrom": "moderation_history", "tableTo": "submissions", - "columnsFrom": [ - "tweet_id" - ], - "columnsTo": [ - "tweet_id" - ], + "columnsFrom": ["tweet_id"], + "columnsTo": ["tweet_id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -180,9 +172,7 @@ "indexes": { "submission_counts_date_idx": { "name": "submission_counts_date_idx", - "columns": [ - "last_reset_date" - ], + "columns": ["last_reset_date"], "isUnique": false } }, @@ -226,9 +216,7 @@ "indexes": { "submission_feeds_feed_idx": { "name": "submission_feeds_feed_idx", - "columns": [ - "feed_id" - ], + "columns": ["feed_id"], "isUnique": false } }, @@ -237,12 +225,8 @@ "name": "submission_feeds_submission_id_submissions_tweet_id_fk", "tableFrom": "submission_feeds", "tableTo": "submissions", - "columnsFrom": [ - "submission_id" - ], - "columnsTo": [ - "tweet_id" - ], + "columnsFrom": ["submission_id"], + "columnsTo": ["tweet_id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -250,22 +234,15 @@ "name": "submission_feeds_feed_id_feeds_id_fk", "tableFrom": "submission_feeds", "tableTo": "feeds", - "columnsFrom": [ - "feed_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["feed_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": { "submission_feeds_submission_id_feed_id_pk": { - "columns": [ - "submission_id", - "feed_id" - ], + "columns": ["submission_id", "feed_id"], "name": "submission_feeds_submission_id_feed_id_pk" } }, @@ -357,37 +334,27 @@ "indexes": { "submissions_acknowledgment_tweet_id_unique": { "name": "submissions_acknowledgment_tweet_id_unique", - "columns": [ - "acknowledgment_tweet_id" - ], + "columns": ["acknowledgment_tweet_id"], "isUnique": true }, "submissions_user_id_idx": { "name": "submissions_user_id_idx", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "isUnique": false }, "submissions_status_idx": { "name": "submissions_status_idx", - "columns": [ - "status" - ], + "columns": ["status"], "isUnique": false }, "submissions_acknowledgment_idx": { "name": "submissions_acknowledgment_idx", - "columns": [ - "acknowledgment_tweet_id" - ], + "columns": ["acknowledgment_tweet_id"], "isUnique": false }, "submissions_submitted_at_idx": { "name": "submissions_submitted_at_idx", - "columns": [ - "submitted_at" - ], + "columns": ["submitted_at"], "isUnique": false } }, @@ -407,4 +374,4 @@ "internal": { "indexes": {} } -} \ No newline at end of file +} diff --git a/backend/src/services/db/migrations/meta/_journal.json b/backend/src/services/db/migrations/meta/_journal.json index c3a1b9c..b6ca7ce 100644 --- a/backend/src/services/db/migrations/meta/_journal.json +++ b/backend/src/services/db/migrations/meta/_journal.json @@ -10,4 +10,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/backend/src/services/db/queries.ts b/backend/src/services/db/queries.ts index 239ab5f..140a9d9 100644 --- a/backend/src/services/db/queries.ts +++ b/backend/src/services/db/queries.ts @@ -1,13 +1,20 @@ import { and, eq, sql } from "drizzle-orm"; import { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; -import { moderationHistory, submissionCounts, submissions, feeds, submissionFeeds } from "./schema"; +import { + moderationHistory, + submissionCounts, + submissions, + feeds, + submissionFeeds, +} from "./schema"; import { Moderation, TwitterSubmission } from "types/twitter"; export function upsertFeed( db: BunSQLiteDatabase, - feed: { id: string; name: string; description?: string } + feed: { id: string; name: string; description?: string }, ) { - return db.insert(feeds) + return db + .insert(feeds) .values({ id: feed.id, name: feed.name, @@ -26,9 +33,10 @@ export function upsertFeed( export function saveSubmissionToFeed( db: BunSQLiteDatabase, submissionId: string, - feedId: string + feedId: string, ) { - return db.insert(submissionFeeds) + return db + .insert(submissionFeeds) .values({ submissionId, feedId, @@ -38,14 +46,15 @@ export function saveSubmissionToFeed( export function getFeedsBySubmission( db: BunSQLiteDatabase, - submissionId: string + submissionId: string, ) { - return db.select({ - feedId: submissionFeeds.feedId, - }) - .from(submissionFeeds) - .where(eq(submissionFeeds.submissionId, submissionId)) - .all(); + return db + .select({ + feedId: submissionFeeds.feedId, + }) + .from(submissionFeeds) + .where(eq(submissionFeeds.submissionId, submissionId)) + .all(); } export function saveSubmission( @@ -321,9 +330,9 @@ export function cleanupOldSubmissionCounts( db: BunSQLiteDatabase, date: string, ) { - return db.delete(submissionCounts).where( - sql`${submissionCounts.lastResetDate} < ${date}`, - ); + return db + .delete(submissionCounts) + .where(sql`${submissionCounts.lastResetDate} < ${date}`); } export function getDailySubmissionCount( @@ -351,7 +360,8 @@ export function incrementDailySubmissionCount( ) { const today = new Date().toISOString().split("T")[0]; - return db.insert(submissionCounts) + return db + .insert(submissionCounts) .values({ userId, count: 1, @@ -374,8 +384,9 @@ export function updateSubmissionAcknowledgment( tweetId: string, acknowledgmentTweetId: string, ) { - return db.update(submissions) - .set({ + return db + .update(submissions) + .set({ acknowledgmentTweetId, updatedAt: new Date().toISOString(), }) @@ -387,12 +398,13 @@ export function removeFromSubmissionFeed( submissionId: string, feedId: string, ) { - return db.delete(submissionFeeds) + return db + .delete(submissionFeeds) .where( and( eq(submissionFeeds.submissionId, submissionId), - eq(submissionFeeds.feedId, feedId) - ) + eq(submissionFeeds.feedId, feedId), + ), ); } @@ -417,7 +429,7 @@ export function getSubmissionsByFeed( .from(submissions) .innerJoin( submissionFeeds, - eq(submissions.tweetId, submissionFeeds.submissionId) + eq(submissions.tweetId, submissionFeeds.submissionId), ) .leftJoin( moderationHistory, diff --git a/backend/src/services/db/schema.ts b/backend/src/services/db/schema.ts index 3dd831d..ec37371 100644 --- a/backend/src/services/db/schema.ts +++ b/backend/src/services/db/schema.ts @@ -1,8 +1,16 @@ -import { index, integer, primaryKey, sqliteTable as table, text } from "drizzle-orm/sqlite-core"; +import { + index, + integer, + primaryKey, + sqliteTable as table, + text, +} from "drizzle-orm/sqlite-core"; // Reusable timestamp columns const timestamps = { - createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString()), + createdAt: text("created_at") + .notNull() + .$defaultFn(() => new Date().toISOString()), updatedAt: text("updated_at").$defaultFn(() => new Date().toISOString()), }; @@ -12,7 +20,8 @@ export const SubmissionStatus = { REJECTED: "rejected", } as const; -export type SubmissionStatus = typeof SubmissionStatus[keyof typeof SubmissionStatus]; +export type SubmissionStatus = + (typeof SubmissionStatus)[keyof typeof SubmissionStatus]; // Feeds Table // Builds according to feeds in curate.config.json @@ -40,14 +49,14 @@ export const submissions = table( submittedAt: text("submitted_at"), ...timestamps, }, - (submissions) => ([ + (submissions) => [ index("submissions_user_id_idx").on(submissions.userId), index("submissions_status_idx").on(submissions.status), index("submissions_acknowledgment_idx").on( - submissions.acknowledgmentTweetId + submissions.acknowledgmentTweetId, ), index("submissions_submitted_at_idx").on(submissions.submittedAt), - ]) + ], ); export const submissionFeeds = table( @@ -61,10 +70,10 @@ export const submissionFeeds = table( .references(() => feeds.id, { onDelete: "cascade" }), ...timestamps, }, - (table) => ([ + (table) => [ primaryKey({ columns: [table.submissionId, table.feedId] }), - index("submission_feeds_feed_idx").on(table.feedId) - ]) + index("submission_feeds_feed_idx").on(table.feedId), + ], ); export const moderationHistory = table( @@ -79,10 +88,10 @@ export const moderationHistory = table( note: text("note"), ...timestamps, }, - (table) => ([ + (table) => [ index("moderation_history_tweet_idx").on(table.tweetId), index("moderation_history_admin_idx").on(table.adminId), - ]) + ], ); export const submissionCounts = table( @@ -93,7 +102,5 @@ export const submissionCounts = table( lastResetDate: text("last_reset_date").notNull(), ...timestamps, }, - (table) => ([ - index("submission_counts_date_idx").on(table.lastResetDate), - ]) + (table) => [index("submission_counts_date_idx").on(table.lastResetDate)], ); diff --git a/backend/src/services/distribution/distribution.service.ts b/backend/src/services/distribution/distribution.service.ts index 6e4e386..7b2d467 100644 --- a/backend/src/services/distribution/distribution.service.ts +++ b/backend/src/services/distribution/distribution.service.ts @@ -20,12 +20,12 @@ export class DistributionService { private async loadPlugin(name: string, config: PluginConfig): Promise { try { // Dynamic import of plugin from URL - const module = await import(config.url) as PluginModule; + const module = (await import(config.url)) as PluginModule; const plugin = new module.default(); - + // Store the plugin instance this.plugins.set(name, plugin); - + logger.info(`Successfully loaded plugin: ${name}`); } catch (error) { logger.error(`Error loading plugin ${name}:`, error); @@ -33,9 +33,13 @@ export class DistributionService { } } - async transformContent(pluginName: string, content: string, config: { prompt: string }): Promise { + async transformContent( + pluginName: string, + content: string, + config: { prompt: string }, + ): Promise { const plugin = this.plugins.get(pluginName); - if (!plugin || !('transform' in plugin)) { + if (!plugin || !("transform" in plugin)) { throw new Error(`Transformer plugin ${pluginName} not found or invalid`); } @@ -43,14 +47,21 @@ export class DistributionService { await plugin.initialize(config); return await plugin.transform(content); } catch (error) { - logger.error(`Error transforming content with plugin ${pluginName}:`, error); + logger.error( + `Error transforming content with plugin ${pluginName}:`, + error, + ); throw error; } } - async distributeContent(pluginName: string, content: string, config: Record): Promise { + async distributeContent( + pluginName: string, + content: string, + config: Record, + ): Promise { const plugin = this.plugins.get(pluginName); - if (!plugin || !('distribute' in plugin)) { + if (!plugin || !("distribute" in plugin)) { throw new Error(`Distributor plugin ${pluginName} not found or invalid`); } @@ -58,14 +69,21 @@ export class DistributionService { await plugin.initialize(config); await plugin.distribute(content); } catch (error) { - logger.error(`Error distributing content with plugin ${pluginName}:`, error); + logger.error( + `Error distributing content with plugin ${pluginName}:`, + error, + ); throw error; } } - async processStreamOutput(feedId: string, submissionId: string, content: string): Promise { + async processStreamOutput( + feedId: string, + submissionId: string, + content: string, + ): Promise { const config = await this.getConfig(); - const feed = config.feeds.find(f => f.id === feedId); + const feed = config.feeds.find((f) => f.id === feedId); if (!feed?.outputs.stream?.enabled) { return; } @@ -74,7 +92,9 @@ export class DistributionService { // Stream must have at least one distribution configured if (!distribute?.length) { - throw new Error(`Stream output for feed ${feedId} requires at least one distribution configuration`); + throw new Error( + `Stream output for feed ${feedId} requires at least one distribution configuration`, + ); } // Transform content if configured @@ -83,17 +103,13 @@ export class DistributionService { processedContent = await this.transformContent( transform.plugin, content, - transform.config + transform.config, ); } // Distribute to all configured outputs for (const dist of distribute) { - await this.distributeContent( - dist.plugin, - processedContent, - dist.config - ); + await this.distributeContent(dist.plugin, processedContent, dist.config); } if (!feed?.outputs.recap?.enabled) { // Remove from submission feed after successful distribution if no recap @@ -107,7 +123,7 @@ export class DistributionService { // Then clear queue async processRecapOutput(feedId: string): Promise { const config = await this.getConfig(); - const feed = config.feeds.find(f => f.id === feedId); + const feed = config.feeds.find((f) => f.id === feedId); if (!feed?.outputs.recap?.enabled) { return; } @@ -115,11 +131,15 @@ export class DistributionService { const { transform, distribute } = feed.outputs.recap; if (!distribute?.length) { - throw new Error(`Recap output for feed ${feedId} requires distribution configuration`); + throw new Error( + `Recap output for feed ${feedId} requires distribution configuration`, + ); } if (!transform) { - throw new Error(`Recap output for feed ${feedId} requires transform configuration`); + throw new Error( + `Recap output for feed ${feedId} requires transform configuration`, + ); } // TODO: adjust recap, needs to be called from cron job. @@ -133,16 +153,12 @@ export class DistributionService { const processedContent = await this.transformContent( transform.plugin, content, - transform.config + transform.config, ); // Distribute to all configured outputs for (const dist of distribute) { - await this.distributeContent( - dist.plugin, - processedContent, - dist.config - ); + await this.distributeContent(dist.plugin, processedContent, dist.config); } // Remove from submission feed after successful recap @@ -150,7 +166,7 @@ export class DistributionService { } private async getConfig(): Promise { - const { ConfigService } = await import('../config'); + const { ConfigService } = await import("../config"); return ConfigService.getInstance().getConfig(); } diff --git a/backend/src/services/submissions/submission.service.ts b/backend/src/services/submissions/submission.service.ts index 9388838..1ffbd0e 100644 --- a/backend/src/services/submissions/submission.service.ts +++ b/backend/src/services/submissions/submission.service.ts @@ -1,4 +1,4 @@ -import { DistributionService } from './../distribution/distribution.service'; +import { DistributionService } from "./../distribution/distribution.service"; import { Tweet } from "agent-twitter-client"; import { AppConfig } from "../../types/config"; import { TwitterService } from "../twitter/client"; @@ -15,8 +15,8 @@ export class SubmissionService { constructor( private readonly twitterService: TwitterService, private readonly DistributionService: DistributionService, - private readonly config: AppConfig - ) { } + private readonly config: AppConfig, + ) {} async initialize(): Promise { // Initialize feeds and admin cache from config @@ -31,11 +31,15 @@ export class SubmissionService { // Cache admin IDs for (const handle of feed.moderation.approvers.twitter) { try { - const userId = await this.twitterService.getUserIdByScreenName(handle); + const userId = + await this.twitterService.getUserIdByScreenName(handle); this.adminIdCache.set(userId, handle); logger.info(`Cached admin ID for @${handle}: ${userId}`); } catch (error) { - logger.error(`Failed to fetch ID for admin handle @${handle}:`, error); + logger.error( + `Failed to fetch ID for admin handle @${handle}:`, + error, + ); } } } @@ -60,7 +64,9 @@ export class SubmissionService { private async checkMentions(): Promise { try { logger.info("Checking mentions..."); - const newTweets = await this.twitterService.fetchAllNewMentions(this.lastCheckedTweetId); + const newTweets = await this.twitterService.fetchAllNewMentions( + this.lastCheckedTweetId, + ); if (newTweets.length === 0) { logger.info("No new mentions"); @@ -111,7 +117,9 @@ export class SubmissionService { // this could be determined by a flag (using it for feature or bug requests) const inReplyToId = tweet.inReplyToStatusId; if (!inReplyToId) { - logger.error(`Submission tweet ${tweet.id} is not a reply to another tweet`); + logger.error( + `Submission tweet ${tweet.id} is not a reply to another tweet`, + ); return; } @@ -123,7 +131,7 @@ export class SubmissionService { if (dailyCount >= maxSubmissions) { await this.twitterService.replyToTweet( tweet.id, - "You've reached your daily submission limit. Please try again tomorrow." + "You've reached your daily submission limit. Please try again tomorrow.", ); logger.info(`User ${userId} has reached limit, replied to submission.`); return; @@ -137,15 +145,15 @@ export class SubmissionService { } // Extract feed IDs from hashtags - const feedIds = (tweet.hashtags || []).filter(tag => - this.config.feeds.some(feed => feed.id === tag.toLowerCase()) + const feedIds = (tweet.hashtags || []).filter((tag) => + this.config.feeds.some((feed) => feed.id === tag.toLowerCase()), ); // If no feeds specified, reject submission if (feedIds.length === 0) { await this.twitterService.replyToTweet( tweet.id, - "Please specify at least one valid feed using hashtags (e.g. #grants, #ethereum, #near)" + "Please specify at least one valid feed using hashtags (e.g. #grants, #ethereum, #near)", ); // ${this.config.feeds.map((it) => `#{it.id}`).join(", ")} return; @@ -158,9 +166,13 @@ export class SubmissionService { username: originalTweet.username!, content: originalTweet.text || "", description: this.extractDescription(tweet), - status: this.config.global.defaultStatus as "pending" | "approved" | "rejected", + status: this.config.global.defaultStatus as + | "pending" + | "approved" + | "rejected", moderationHistory: [], - createdAt: originalTweet.timeParsed?.toISOString() || new Date().toISOString(), + createdAt: + originalTweet.timeParsed?.toISOString() || new Date().toISOString(), submittedAt: new Date().toISOString(), }; @@ -174,12 +186,17 @@ export class SubmissionService { // Send acknowledgment const acknowledgmentTweetId = await this.twitterService.replyToTweet( tweet.id, - "Successfully submitted!" + "Successfully submitted!", ); if (acknowledgmentTweetId) { - db.updateSubmissionAcknowledgment(originalTweet.id!, acknowledgmentTweetId); - logger.info(`Successfully submitted. Sent reply: ${acknowledgmentTweetId}`); + db.updateSubmissionAcknowledgment( + originalTweet.id!, + acknowledgmentTweetId, + ); + logger.info( + `Successfully submitted. Sent reply: ${acknowledgmentTweetId}`, + ); } } catch (error) { logger.error(`Error handling submission for tweet ${tweet.id}:`, error); @@ -202,7 +219,8 @@ export class SubmissionService { if (!inReplyToId) return; const submission = db.getSubmissionByAcknowledgmentTweetId(inReplyToId); - if (!submission || submission.status !== this.config.global.defaultStatus) return; + if (!submission || submission.status !== this.config.global.defaultStatus) + return; const action = this.getModerationAction(tweet); if (!action) return; @@ -232,24 +250,40 @@ export class SubmissionService { } } - private async processApproval(tweet: Tweet, submission: TwitterSubmission): Promise { + private async processApproval( + tweet: Tweet, + submission: TwitterSubmission, + ): Promise { const responseTweetId = await this.twitterService.replyToTweet( tweet.id!, - "Your submission has been approved!" + "Your submission has been approved!", ); if (responseTweetId) { - db.updateSubmissionStatus(submission.tweetId, "approved", responseTweetId); + db.updateSubmissionStatus( + submission.tweetId, + "approved", + responseTweetId, + ); // Process through distribution service for each associated feed try { const submissionFeeds = db.getFeedsBySubmission(submission.tweetId); for (const { feedId } of submissionFeeds) { - const feed = this.config.feeds.find(f => f.id === feedId); - if (feed && feed.moderation.approvers.twitter.includes(this.adminIdCache.get(tweet.userId!) || '')) { + const feed = this.config.feeds.find((f) => f.id === feedId); + if ( + feed && + feed.moderation.approvers.twitter.includes( + this.adminIdCache.get(tweet.userId!) || "", + ) + ) { if (feed?.outputs.stream?.enabled) { - await this.DistributionService.processStreamOutput(feedId, submission.tweetId, submission.content); + await this.DistributionService.processStreamOutput( + feedId, + submission.tweetId, + submission.content, + ); } } } @@ -259,14 +293,21 @@ export class SubmissionService { } } - private async processRejection(tweet: Tweet, submission: TwitterSubmission): Promise { + private async processRejection( + tweet: Tweet, + submission: TwitterSubmission, + ): Promise { const responseTweetId = await this.twitterService.replyToTweet( tweet.id!, - "Your submission has been reviewed and was not accepted." + "Your submission has been reviewed and was not accepted.", ); if (responseTweetId) { - db.updateSubmissionStatus(submission.tweetId, "rejected", responseTweetId); + db.updateSubmissionStatus( + submission.tweetId, + "rejected", + responseTweetId, + ); } } @@ -275,7 +316,7 @@ export class SubmissionService { } private getModerationAction(tweet: Tweet): "approve" | "reject" | null { - const hashtags = tweet.hashtags?.map(tag => tag.toLowerCase()) || []; + const hashtags = tweet.hashtags?.map((tag) => tag.toLowerCase()) || []; if (hashtags.includes("approve")) return "approve"; if (hashtags.includes("reject")) return "reject"; return null; @@ -290,19 +331,23 @@ export class SubmissionService { } private extractDescription(tweet: Tweet): string | undefined { - return tweet.text - ?.replace(/!submit\s+@\w+/i, "") - .replace(new RegExp(`@${tweet.username}`, 'i'), "") - .replace(/#\w+/g, "") - .trim() || undefined; + return ( + tweet.text + ?.replace(/!submit\s+@\w+/i, "") + .replace(new RegExp(`@${tweet.username}`, "i"), "") + .replace(/#\w+/g, "") + .trim() || undefined + ); } private extractNote(tweet: Tweet): string | undefined { - return tweet.text - ?.replace(/#\w+/g, "") - .replace(new RegExp(`@${this.config.global.botId}`, 'i'), "") - .replace(new RegExp(`@${tweet.username}`, 'i'), "") - .trim() || undefined; + return ( + tweet.text + ?.replace(/#\w+/g, "") + .replace(new RegExp(`@${this.config.global.botId}`, "i"), "") + .replace(new RegExp(`@${tweet.username}`, "i"), "") + .trim() || undefined + ); } private async setLastCheckedTweetId(tweetId: string) { diff --git a/backend/src/services/transformers/transformation.service.ts b/backend/src/services/transformers/transformation.service.ts index ed06a3b..3d18ff4 100644 --- a/backend/src/services/transformers/transformation.service.ts +++ b/backend/src/services/transformers/transformation.service.ts @@ -19,7 +19,7 @@ export class DistributionService { private async loadPlugin(name: string, config: PluginConfig): Promise { try { // Dynamic import of plugin from URL - const module = await import(config.url) as PluginModule; + const module = (await import(config.url)) as PluginModule; const plugin = new module.default(); // Store the plugin instance @@ -32,9 +32,13 @@ export class DistributionService { } } - async transformContent(pluginName: string, content: string, config: { prompt: string }): Promise { + async transformContent( + pluginName: string, + content: string, + config: { prompt: string }, + ): Promise { const plugin = this.plugins.get(pluginName); - if (!plugin || !('transform' in plugin)) { + if (!plugin || !("transform" in plugin)) { throw new Error(`Transformer plugin ${pluginName} not found or invalid`); } @@ -42,7 +46,10 @@ export class DistributionService { await plugin.initialize(config); return await plugin.transform(content); } catch (error) { - logger.error(`Error transforming content with plugin ${pluginName}:`, error); + logger.error( + `Error transforming content with plugin ${pluginName}:`, + error, + ); throw error; } } diff --git a/backend/src/services/twitter/client.ts b/backend/src/services/twitter/client.ts index 2016487..ad790fc 100644 --- a/backend/src/services/twitter/client.ts +++ b/backend/src/services/twitter/client.ts @@ -19,7 +19,7 @@ export class TwitterService { username: string; password: string; email: string; - } + }, ) { this.client = new Scraper(); this.twitterUsername = config.username; diff --git a/backend/src/types/config.ts b/backend/src/types/config.ts index 17994d2..77802c3 100644 --- a/backend/src/types/config.ts +++ b/backend/src/types/config.ts @@ -5,7 +5,7 @@ export interface GlobalConfig { } export interface PluginConfig { - type: 'distributor' | 'transformer'; + type: "distributor" | "transformer"; url: string; } diff --git a/backend/src/utils/config.ts b/backend/src/utils/config.ts index d2a1f3d..7c7aeab 100644 --- a/backend/src/utils/config.ts +++ b/backend/src/utils/config.ts @@ -1,12 +1,14 @@ /** * Recursively processes a config object, replacing environment variable placeholders * with their actual values. - * + * * Format: "{ENV_VAR_NAME}" will be replaced with process.env.ENV_VAR_NAME */ -export function hydrateConfigValues>(config: T): T { +export function hydrateConfigValues>( + config: T, +): T { const processValue = (value: any): any => { - if (typeof value === 'string') { + if (typeof value === "string") { // Match strings like "{SOME_ENV_VAR}" const match = value.match(/^\{([A-Z_][A-Z0-9_]*)\}$/); if (match) { @@ -19,17 +21,17 @@ export function hydrateConfigValues>(config: T): T } return value; } - + if (Array.isArray(value)) { - return value.map(item => processValue(item)); + return value.map((item) => processValue(item)); } - - if (value && typeof value === 'object') { + + if (value && typeof value === "object") { return Object.fromEntries( - Object.entries(value).map(([k, v]) => [k, processValue(v)]) + Object.entries(value).map(([k, v]) => [k, processValue(v)]), ); } - + return value; }; diff --git a/landing-page/vercel.json b/landing-page/vercel.json index 7f01de5..9d23797 100644 --- a/landing-page/vercel.json +++ b/landing-page/vercel.json @@ -5,4 +5,4 @@ "use": "@vercel/next" } ] -} \ No newline at end of file +}