Skip to content

Conversation

ginzahatemi
Copy link
Collaborator

@ginzahatemi ginzahatemi commented Oct 4, 2025

A new file redditAccountManager.ts has been added in services with a Reddit account manager implementation

Summary by CodeRabbit

  • New Features
    • Added Reddit account management with secure storage and automatic usage tracking.
    • Displays per-account usage details, including total requests and last activity timestamp.
    • Improves reliability by skipping invalid accounts and surfacing only usable ones.
    • Enables centralized access to all Reddit accounts and their current usage status.

Copy link

coderabbitai bot commented Oct 4, 2025

Walkthrough

Adds a new RedditAccountManager service that loads encrypted Reddit account records from Redis, decrypts credentials, derives account IDs, retrieves per-account usage stats, and exposes methods to list accounts with usage and to track usage updates. Exports the RedditAccount interface, the RedditAccountManager class, and a singleton instance.

Changes

Cohort / File(s) Summary
Reddit account management service
src/services/redditAccountManager.ts
Introduces RedditAccount interface; implements RedditAccountManager extending BaseAccountManager to load/decrypt accounts from Redis, compute accountId, fetch and update usage via per-account Redis hashes, and expose getAllAccountsUsage and tracking methods; exports singleton redditAccountManager.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant Manager as RedditAccountManager
  participant Redis as Redis
  participant Crypto as Decryption

  App->>Manager: getAllAccountsUsage()
  activate Manager
  Manager->>Redis: connect (if needed)
  Manager->>Redis: HGETALL reddit-accounts
  Redis-->>Manager: Encrypted account map
  loop for each account
    Manager->>Crypto: decrypt(credentials)
    Crypto-->>Manager: plaintext credentials
    Manager->>Redis: HGET reddit_accounts:<accountId> {total_requests,last_request}
    Redis-->>Manager: usage fields
    Note right of Manager: Derive accountId=<code>reddit_<username></code><br/>Assemble RedditAccount object
  end
  Manager-->>App: RedditAccount[] (with usage)
  deactivate Manager
Loading
sequenceDiagram
  autonumber
  actor App
  participant Manager as RedditAccountManager
  participant Redis as Redis

  App->>Manager: trackApiKeyUsageLocal(accountId)
  activate Manager
  Manager->>Redis: HINCRBY reddit_accounts:<accountId> total_requests 1
  Manager->>Redis: HSET reddit_accounts:<accountId> last_request=<timestamp>
  Manager-->>App: ack
  deactivate Manager
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I twitch my whiskers, tap the keys—
New burrows in Redis, stored with ease.
Decrypt, hop through, accounts in line,
Track every nibble, request by time.
With carrot-logs and token cheer,
The Reddit warren’s crystal clear.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the main change by indicating the addition of a RedditAccountManager feature without unnecessary detail, and it follows conventional formatting for a concise feature description.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/reddit-manager

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/services/redditAccountManager.ts (2)

61-61: Consider sanitizing username for accountId generation.

The username is used directly in the accountId without sanitization. If a Reddit username contains special characters (e.g., underscores, hyphens), this could create confusing Redis keys or potential conflicts.

Consider applying this diff to sanitize the username:

-        const accountId = `reddit_${credentials.REDDIT_USERNAME}`;
+        const accountId = `reddit_${credentials.REDDIT_USERNAME.toLowerCase().replace(/[^a-z0-9_-]/g, '_')}`;

107-107: Consider removing redundant account_id field.

The account_id is stored in the hash on line 107, but it's already part of the Redis key name. This redundancy wastes storage space. Unless there's a specific need to query this field independently, consider removing it.

Apply this diff if the field is not needed:

-      .hSet(key, { last_request: now, account_id: accountId })
+      .hSet(key, { last_request: now })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acf1fb4 and 5073124.

📒 Files selected for processing (1)
  • src/services/redditAccountManager.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/services/redditAccountManager.ts (2)
src/services/BaseAccountManager.ts (1)
  • BaseAccount (3-8)
src/lib/encryption.ts (1)
  • decrypt (27-39)
🔇 Additional comments (3)
src/services/redditAccountManager.ts (3)

16-26: LGTM!

The class declaration and constructor are correctly implemented. The protected properties follow the expected pattern for extending BaseAccountManager.


111-116: LGTM!

The method correctly delegates to fetchAllAccounts(), which already includes usage statistics. The separate method name improves API clarity for callers who specifically want usage data.


119-120: LGTM!

Exporting a singleton instance is a common and appropriate pattern for service managers like this.

Comment on lines +4 to +14
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;
}
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.

Comment on lines +48 to +75
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;
}
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;
}
}

Comment on lines +88 to +98
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
};
}
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.

Comment on lines +100 to +109
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();
}
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.

@tasin2610 tasin2610 merged commit 1682cbf into main Oct 4, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants