Skip to content

Commit

Permalink
better logging
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotBraem committed Dec 17, 2024
1 parent 5dd4a79 commit a40260f
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 33 deletions.
74 changes: 68 additions & 6 deletions src/services/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export class DatabaseService {
category TEXT,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending',
acknowledgment_tweet_id TEXT,
moderation_response_tweet_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
Expand Down Expand Up @@ -71,13 +73,19 @@ export class DatabaseService {
CREATE INDEX IF NOT EXISTS idx_submission_counts_date
ON submission_counts(last_reset_date)
`);

// Add index on acknowledgment_tweet_id for faster lookups
this.db.run(`
CREATE INDEX IF NOT EXISTS idx_acknowledgment_tweet_id
ON submissions(acknowledgment_tweet_id)
`);
}

saveSubmission(submission: TwitterSubmission): void {
const stmt = this.db.prepare(`
INSERT INTO submissions (
tweet_id, user_id, content, hashtags, category, description, status
) VALUES (?, ?, ?, ?, ?, ?, ?)
tweet_id, user_id, content, hashtags, category, description, status, acknowledgment_tweet_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);

stmt.run(
Expand All @@ -87,7 +95,8 @@ export class DatabaseService {
JSON.stringify(submission.hashtags),
submission.category || null,
submission.description || null,
submission.status
submission.status,
submission.acknowledgmentTweetId || null
);
}

Expand All @@ -104,13 +113,15 @@ export class DatabaseService {
moderation.action,
moderation.timestamp.toISOString()
);
}

// Update submission status
updateSubmissionStatus(tweetId: string, status: TwitterSubmission['status'], moderationResponseTweetId: string): void {
this.db.prepare(`
UPDATE submissions
SET status = ?
SET status = ?,
moderation_response_tweet_id = ?
WHERE tweet_id = ?
`).run(moderation.action === 'approve' ? 'approved' : 'rejected', moderation.tweetId);
`).run(status, moderationResponseTweetId, tweetId);
}

getSubmission(tweetId: string): TwitterSubmission | null {
Expand Down Expand Up @@ -139,6 +150,45 @@ export class DatabaseService {
category: submission.category,
description: submission.description,
status: submission.status,
acknowledgmentTweetId: submission.acknowledgment_tweet_id,
moderationResponseTweetId: submission.moderation_response_tweet_id,
moderationHistory: submission.moderation_history
? JSON.parse(`[${submission.moderation_history}]`).map((m: any) => ({
...m,
timestamp: new Date(m.timestamp)
}))
: []
};
}

getSubmissionByAcknowledgmentTweetId(acknowledgmentTweetId: string): TwitterSubmission | null {
const submission = this.db.prepare(`
SELECT s.*, GROUP_CONCAT(
json_object(
'adminId', m.admin_id,
'action', m.action,
'timestamp', m.timestamp,
'tweetId', m.tweet_id
)
) as moderation_history
FROM submissions s
LEFT JOIN moderation_history m ON s.tweet_id = m.tweet_id
WHERE s.acknowledgment_tweet_id = ?
GROUP BY s.tweet_id
`).get(acknowledgmentTweetId) as any;

if (!submission) return null;

return {
tweetId: submission.tweet_id,
userId: submission.user_id,
content: submission.content,
hashtags: JSON.parse(submission.hashtags),
category: submission.category,
description: submission.description,
status: submission.status,
acknowledgmentTweetId: submission.acknowledgment_tweet_id,
moderationResponseTweetId: submission.moderation_response_tweet_id,
moderationHistory: submission.moderation_history
? JSON.parse(`[${submission.moderation_history}]`).map((m: any) => ({
...m,
Expand Down Expand Up @@ -171,6 +221,8 @@ export class DatabaseService {
category: submission.category,
description: submission.description,
status: submission.status,
acknowledgmentTweetId: submission.acknowledgment_tweet_id,
moderationResponseTweetId: submission.moderation_response_tweet_id,
moderationHistory: submission.moderation_history
? JSON.parse(`[${submission.moderation_history}]`).map((m: any) => ({
...m,
Expand Down Expand Up @@ -204,6 +256,8 @@ export class DatabaseService {
category: submission.category,
description: submission.description,
status: submission.status,
acknowledgmentTweetId: submission.acknowledgment_tweet_id,
moderationResponseTweetId: submission.moderation_response_tweet_id,
moderationHistory: submission.moderation_history
? JSON.parse(`[${submission.moderation_history}]`).map((m: any) => ({
...m,
Expand Down Expand Up @@ -267,6 +321,14 @@ export class DatabaseService {
updated_at = CURRENT_TIMESTAMP
`).run(tweetId, tweetId);
}

updateSubmissionAcknowledgment(tweetId: string, acknowledgmentTweetId: string): void {
this.db.prepare(`
UPDATE submissions
SET acknowledgment_tweet_id = ?
WHERE tweet_id = ?
`).run(acknowledgmentTweetId, tweetId);
}
}

// Export a singleton instance
Expand Down
70 changes: 43 additions & 27 deletions src/services/twitter/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class TwitterService {
for (const handle of ADMIN_ACCOUNTS) {
try {
const userId = await this.client.getUserIdByScreenName(handle);
this.adminIdCache.set(handle, userId);
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);
Expand All @@ -49,7 +49,7 @@ export class TwitterService {
}

private isAdmin(userId: string): boolean {
return Array.from(this.adminIdCache.values()).includes(userId);
return this.adminIdCache.has(userId);
}

async initialize() {
Expand Down Expand Up @@ -172,10 +172,10 @@ export class TwitterService {

try {
if (this.isSubmission(tweet)) {
logger.info("Received new submission.");
logger.info(`Received new submission: ${this.getTweetLink(tweet.id, tweet.username)}`);
await this.handleSubmission(tweet);
} else if (this.isModeration(tweet)) {
logger.info("Received new moderation.");
logger.info(`Received new moderation: ${this.getTweetLink(tweet.id, tweet.username)}`);
await this.handleModeration(tweet);
}
} catch (error) {
Expand Down Expand Up @@ -235,45 +235,47 @@ export class TwitterService {
// Increment submission count in database
db.incrementDailySubmissionCount(userId);

await this.replyToTweet(
// Send acknowledgment and save its ID
const acknowledgmentTweetId = await this.replyToTweet(
tweet.id,
"Successfully submitted to publicgoods.news!"
);
logger.info(`Successfully submitted. Replied to User: ${userId}.`)

if (acknowledgmentTweetId) {
db.updateSubmissionAcknowledgment(tweet.id, acknowledgmentTweetId);
logger.info(`Successfully submitted. Sent reply: ${this.getTweetLink(acknowledgmentTweetId)}`)
} else {
logger.error(`Failed to acknowledge submission: ${this.getTweetLink(tweet.id, tweet.username)}`)
}
}

private async handleModeration(tweet: Tweet): Promise<void> {
const userId = tweet.userId;
if (!userId || !tweet.id) return;

logger.info(`Handling moderation for ${JSON.stringify(tweet)}`);

// Verify admin status using cached ID
if (!this.isAdmin(userId)) {
logger.info(`User ${userId} is not admin.`)
return; // Silently ignore non-admin moderation attempts
}

// Get the original submission tweet this is in response to
// Get the tweet this is in response to (should be our acknowledgment tweet)
const inReplyToId = tweet.inReplyToStatusId;
logger.info(`It was a reply to ${tweet.inReplyToStatusId}`);
if (!inReplyToId) return;

// Get submission from database
const submission = db.getSubmission(inReplyToId);
logger.info(`Got the original submission: ${JSON.stringify(submission)}`);
// Get submission by acknowledgment tweet ID
const submission = db.getSubmissionByAcknowledgmentTweetId(inReplyToId);
if (!submission) return;

// Check if submission has already been moderated by any admin
if (submission.moderationHistory.length > 0) {
logger.info(`Submission ${submission.tweetId} has already been moderated, ignoring new moderation attempt.`);
return;
}

const action = this.getModerationAction(tweet);
logger.info(`Determined the action: ${action}`);
if (!action) return;

// Check if this admin has already moderated this submission
const hasModerated = submission.moderationHistory.some(
(mod) => mod.adminId === userId
);
if (hasModerated) return;

// Add moderation to database
const moderation: Moderation = {
adminId: userId,
Expand All @@ -285,27 +287,33 @@ export class TwitterService {

// Process the moderation action
if (action === "approve") {
logger.info(`Received review from User ${userId}, processing approval.`)
logger.info(`Received review from Admin ${this.adminIdCache.get(userId)}, processing approval.`)
await this.processApproval(submission);
} else {
logger.info(`Received review from User ${userId}, processing rejection.`)
logger.info(`Received review from Admin ${this.adminIdCache.get(userId)}, processing rejection.`)
await this.processRejection(submission);
}
}

private async processApproval(submission: TwitterSubmission): Promise<void> {
// TODO: Add NEAR integration here for approved submissions
await this.replyToTweet(
const responseTweetId = await this.replyToTweet(
submission.tweetId,
"Your submission has been approved and will be added to the public goods news feed!"
);
if (responseTweetId) {
db.updateSubmissionStatus(submission.tweetId, "approved", responseTweetId);
}
}

private async processRejection(submission: TwitterSubmission): Promise<void> {
await this.replyToTweet(
const responseTweetId = await this.replyToTweet(
submission.tweetId,
"Your submission has been reviewed and was not accepted for the public goods news feed."
);
if (responseTweetId) {
db.updateSubmissionStatus(submission.tweetId, "rejected", responseTweetId);
}
}

private getModerationAction(tweet: Tweet): "approve" | "reject" | null {
Expand All @@ -323,12 +331,20 @@ export class TwitterService {
return tweet.text?.toLowerCase().includes("!submit") || false;
}

private async replyToTweet(tweetId: string, message: string): Promise<void> {
private async replyToTweet(tweetId: string, message: string): Promise<string | null> {
try {
await this.client.sendTweet(message, tweetId); // Second parameter is the tweet to reply to
const response = await this.client.sendTweet(message, tweetId);
const responseData = await response.json() as any;
// Extract tweet ID from response
const replyTweetId = responseData?.data?.create_tweet?.tweet_results?.result?.rest_id;
return replyTweetId || null;
} catch (error) {
logger.error('Error replying to tweet:', error);
throw error;
return null;
}
}

private getTweetLink(tweetId: string, username: string = this.twitterUsername): string {
return `https://x.com/${username}/status/${tweetId}`;
}
}
2 changes: 2 additions & 0 deletions src/types/twitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface TwitterSubmission {
description?: string;
status: "pending" | "approved" | "rejected";
moderationHistory: Moderation[];
acknowledgmentTweetId?: string;
moderationResponseTweetId?: string;
}

export interface Moderation {
Expand Down

0 comments on commit a40260f

Please sign in to comment.