Skip to content
Merged
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
120 changes: 120 additions & 0 deletions src/services/redditAccountManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { BaseAccountManager, BaseAccount } from './BaseAccountManager';
import { decrypt } from '../lib/encryption';

export interface RedditAccount extends BaseAccount {
accountId: string;
credentials: {
REDDIT_CLIENT_ID: string;
REDDIT_CLIENT_SECRET: string;
REDDIT_REFRESH_TOKEN: string;
REDDIT_USERNAME: string;
};
lastUsed?: string;
totalRequests?: number;
}
Comment on lines +4 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove redundant field declarations.

The accountId, lastUsed, and totalRequests fields are already defined in the BaseAccount interface (see src/services/BaseAccountManager.ts). Redeclaring them here is unnecessary and may confuse maintainers about which definition applies.

Apply this diff to remove the redundant declarations:

 export interface RedditAccount extends BaseAccount {
-  accountId: string;
   credentials: {
     REDDIT_CLIENT_ID: string;
     REDDIT_CLIENT_SECRET: string;
     REDDIT_REFRESH_TOKEN: string;
     REDDIT_USERNAME: string;
   };
-  lastUsed?: string;
-  totalRequests?: number;
 }
🤖 Prompt for AI Agents
In src/services/redditAccountManager.ts around lines 4 to 14, the RedditAccount
interface is re-declaring accountId, lastUsed, and totalRequests which are
already provided by BaseAccount; remove those three redundant field declarations
from the RedditAccount interface so it only adds the credentials block (and any
fields not present on BaseAccount), keeping the interface as an extension of
BaseAccount with just the Reddit-specific properties.


/**
* Manages Reddit accounts stored in Redis, including decryption and usage tracking.
*/
export class RedditAccountManager extends BaseAccountManager<RedditAccount> {
protected platform = 'reddit';
protected accountKey = 'reddit-accounts';
protected usageKeyPrefix = 'reddit_accounts';

constructor(redisUrl?: string) {
super(redisUrl);
}

/**
* Fetch all Reddit accounts from Redis and decrypt their credentials
*/
protected async fetchAllAccounts(): Promise<RedditAccount[]> {
await this.ensureConnected();

const raw = await this.redisClient.get(this.accountKey);
if (!raw) {
throw new Error('No Reddit accounts found in Redis');
}

let encryptedAccounts: Record<string, string>[];
try {
encryptedAccounts = JSON.parse(raw);
} catch (e) {
throw new Error('Failed to parse Reddit accounts from Redis');
}

const accounts: RedditAccount[] = [];

for (let i = 0; i < encryptedAccounts.length; i++) {
const encryptedAccount = encryptedAccounts[i];

try {
// Decrypt credentials
const credentials = {
REDDIT_CLIENT_ID: decrypt(encryptedAccount.REDDIT_CLIENT_ID),
REDDIT_CLIENT_SECRET: decrypt(encryptedAccount.REDDIT_CLIENT_SECRET),
REDDIT_REFRESH_TOKEN: decrypt(encryptedAccount.REDDIT_REFRESH_TOKEN),
REDDIT_USERNAME: decrypt(encryptedAccount.REDDIT_USERNAME)
};

// Generate account ID from username (for uniqueness)
const accountId = `reddit_${credentials.REDDIT_USERNAME}`;

// Get usage statistics from Redis
const usage = await this.getApiKeyUsageLocal(accountId);

accounts.push({
accountId,
credentials,
lastUsed: usage.last_request || undefined,
totalRequests: usage.total_requests
});
} catch (e) {
console.warn(`Failed to decrypt Reddit account ${i + 1}:`, e);
continue;
}
Comment on lines +48 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add validation before accessing encrypted account fields.

The code accesses properties like encryptedAccount.REDDIT_CLIENT_ID without first verifying they exist. If the Redis data is malformed or missing required fields, this will throw unhelpful errors.

Apply this diff to add field validation:

     for (let i = 0; i < encryptedAccounts.length; i++) {
       const encryptedAccount = encryptedAccounts[i];
 
       try {
+        // Validate required fields exist
+        const requiredFields = ['REDDIT_CLIENT_ID', 'REDDIT_CLIENT_SECRET', 'REDDIT_REFRESH_TOKEN', 'REDDIT_USERNAME'];
+        const missingFields = requiredFields.filter(field => !encryptedAccount[field]);
+        if (missingFields.length > 0) {
+          throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
+        }
+
         // Decrypt credentials
         const credentials = {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (let i = 0; i < encryptedAccounts.length; i++) {
const encryptedAccount = encryptedAccounts[i];
try {
// Decrypt credentials
const credentials = {
REDDIT_CLIENT_ID: decrypt(encryptedAccount.REDDIT_CLIENT_ID),
REDDIT_CLIENT_SECRET: decrypt(encryptedAccount.REDDIT_CLIENT_SECRET),
REDDIT_REFRESH_TOKEN: decrypt(encryptedAccount.REDDIT_REFRESH_TOKEN),
REDDIT_USERNAME: decrypt(encryptedAccount.REDDIT_USERNAME)
};
// Generate account ID from username (for uniqueness)
const accountId = `reddit_${credentials.REDDIT_USERNAME}`;
// Get usage statistics from Redis
const usage = await this.getApiKeyUsageLocal(accountId);
accounts.push({
accountId,
credentials,
lastUsed: usage.last_request || undefined,
totalRequests: usage.total_requests
});
} catch (e) {
console.warn(`Failed to decrypt Reddit account ${i + 1}:`, e);
continue;
}
for (let i = 0; i < encryptedAccounts.length; i++) {
const encryptedAccount = encryptedAccounts[i];
try {
// Validate required fields exist
const requiredFields = ['REDDIT_CLIENT_ID', 'REDDIT_CLIENT_SECRET', 'REDDIT_REFRESH_TOKEN', 'REDDIT_USERNAME'];
const missingFields = requiredFields.filter(field => !encryptedAccount[field]);
if (missingFields.length > 0) {
throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
}
// Decrypt credentials
const credentials = {
REDDIT_CLIENT_ID: decrypt(encryptedAccount.REDDIT_CLIENT_ID),
REDDIT_CLIENT_SECRET: decrypt(encryptedAccount.REDDIT_CLIENT_SECRET),
REDDIT_REFRESH_TOKEN: decrypt(encryptedAccount.REDDIT_REFRESH_TOKEN),
REDDIT_USERNAME: decrypt(encryptedAccount.REDDIT_USERNAME)
};
// Generate account ID from username (for uniqueness)
const accountId = `reddit_${credentials.REDDIT_USERNAME}`;
// Get usage statistics from Redis
const usage = await this.getApiKeyUsageLocal(accountId);
accounts.push({
accountId,
credentials,
lastUsed: usage.last_request || undefined,
totalRequests: usage.total_requests
});
} catch (e) {
console.warn(`Failed to decrypt Reddit account ${i + 1}:`, e);
continue;
}
}

}

if (accounts.length === 0) {
throw new Error('No valid Reddit accounts could be decrypted');
}

return accounts;
}

/**
* Local usage read for Reddit accounts (using the same Redis client)
*/
private async getApiKeyUsageLocal(
accountId: string
): Promise<{ total_requests: number; last_request: string | null }> {
await this.ensureConnected();
const key = `reddit_accounts:${accountId}`;
const data = await this.redisClient.hGetAll(key);
return {
total_requests: data?.total_requests ? parseInt(data.total_requests, 10) : 0,
last_request: data?.last_request ?? null
};
}
Comment on lines +88 to +98
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use this.usageKeyPrefix instead of hardcoded string.

Line 92 hardcodes 'reddit_accounts' when constructing the Redis key, but the class defines this.usageKeyPrefix = 'reddit_accounts' on line 22. This creates inconsistency and makes the code harder to maintain if the prefix ever needs to change.

Apply this diff to use the class property:

   private async getApiKeyUsageLocal(
     accountId: string
   ): Promise<{ total_requests: number; last_request: string | null }> {
     await this.ensureConnected();
-    const key = `reddit_accounts:${accountId}`;
+    const key = `${this.usageKeyPrefix}:${accountId}`;
     const data = await this.redisClient.hGetAll(key);
🤖 Prompt for AI Agents
In src/services/redditAccountManager.ts around lines 88 to 98, replace the
hardcoded Redis key prefix 'reddit_accounts' with the class property
this.usageKeyPrefix when building the key variable; ensure the key is
constructed as `${this.usageKeyPrefix}:${accountId}` (or equivalent string
concat) so it uses the configured prefix consistently, leaving the rest of the
function unchanged.


protected async trackApiKeyUsageLocal(accountId: string): Promise<void> {
await this.ensureConnected();
const key = `reddit_accounts:${accountId}`;
const now = new Date().toISOString();
await this.redisClient
.multi()
.hIncrBy(key, 'total_requests', 1)
.hSet(key, { last_request: now, account_id: accountId })
.exec();
}
Comment on lines +100 to +109
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use this.usageKeyPrefix instead of hardcoded string.

Line 102 has the same issue as line 92: it hardcodes 'reddit_accounts' instead of using this.usageKeyPrefix.

Apply this diff:

   protected async trackApiKeyUsageLocal(accountId: string): Promise<void> {
     await this.ensureConnected();
-    const key = `reddit_accounts:${accountId}`;
+    const key = `${this.usageKeyPrefix}:${accountId}`;
     const now = new Date().toISOString();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected async trackApiKeyUsageLocal(accountId: string): Promise<void> {
await this.ensureConnected();
const key = `reddit_accounts:${accountId}`;
const now = new Date().toISOString();
await this.redisClient
.multi()
.hIncrBy(key, 'total_requests', 1)
.hSet(key, { last_request: now, account_id: accountId })
.exec();
}
protected async trackApiKeyUsageLocal(accountId: string): Promise<void> {
await this.ensureConnected();
const key = `${this.usageKeyPrefix}:${accountId}`;
const now = new Date().toISOString();
await this.redisClient
.multi()
.hIncrBy(key, 'total_requests', 1)
.hSet(key, { last_request: now, account_id: accountId })
.exec();
}
🤖 Prompt for AI Agents
In src/services/redditAccountManager.ts around lines 100 to 109, the code builds
the Redis key using a hardcoded 'reddit_accounts' string; replace that with
this.usageKeyPrefix so the key becomes `${this.usageKeyPrefix}:${accountId}`
(preserving the colon and accountId), ensuring consistency with line 92; keep
the rest of the logic (ensureConnected, hIncrBy, hSet, exec) unchanged.


/**
* Get usage statistics for all accounts
*/
async getAllAccountsUsage(): Promise<RedditAccount[]> {
return await this.fetchAllAccounts();
}
}

// Export singleton instance
export const redditAccountManager = new RedditAccountManager();