Skip to main content
Glama

Tinder API MCP Server

rate-limiter.ts13.2 kB
/** * Rate Limiter Service * Manages rate limiting for API requests */ import logger from '../utils/logger'; import { ApiError } from '../utils/error-handler'; import { ErrorCodes } from '../types'; import { UserRateLimits, GlobalRateLimits, ValidationFailureTracking, ValidationRateLimits } from '../types'; /** * Rate Limiter class * Manages rate limits for API requests */ class RateLimiter { // Store rate limit information per user and endpoint private userLimits: Map<string, UserRateLimits>; private globalLimits: GlobalRateLimits; // Store validation failure tracking private validationFailures: Map<string, ValidationFailureTracking>; private validationRateLimits: ValidationRateLimits; constructor() { this.userLimits = new Map<string, UserRateLimits>(); this.globalLimits = { requestsPerMinute: 100, currentCount: 0, windowStart: Date.now() }; this.validationFailures = new Map<string, ValidationFailureTracking>(); this.validationRateLimits = { maxFailuresPerMinute: 10, maxFailuresPerHour: 30, blockDurationMs: 15 * 60 * 1000 // 15 minutes }; logger.info('Rate limiter initialized'); } /** * Check if request would exceed rate limits * @param endpoint - API endpoint * @param userId - User ID (optional) * @throws {ApiError} If rate limit is exceeded */ // Lock object for synchronizing counter resets private resetLock = { isResetting: false, lastResetTime: Date.now() }; public async checkRateLimit(endpoint: string, userId?: string): Promise<void> { // SECURITY FIX: Fix race condition in counter reset logic // Use atomic operations with a lock mechanism to prevent race conditions const now = Date.now(); const windowExpired = now - this.globalLimits.windowStart > 60000; // Safely reset counter if window has passed if (windowExpired && !this.resetLock.isResetting) { try { // Set lock to prevent other threads from resetting simultaneously this.resetLock.isResetting = true; // Double-check that another thread hasn't reset in the meantime if (now - this.resetLock.lastResetTime > 60000) { this.globalLimits.currentCount = 0; this.globalLimits.windowStart = now; this.resetLock.lastResetTime = now; } } finally { // Always release the lock this.resetLock.isResetting = false; } } // Check global rate limit if (this.globalLimits.currentCount >= this.globalLimits.requestsPerMinute) { logger.warn(`Global rate limit exceeded for endpoint: ${endpoint}`); throw new ApiError( ErrorCodes.RATE_LIMIT_EXCEEDED, 'Global rate limit exceeded', { resetAt: this.globalLimits.windowStart + 60000 }, 429 ); } // Increment counter (atomic operation) this.globalLimits.currentCount++; // Check user-specific limits if (userId && this.userLimits.has(userId)) { const userLimit = this.userLimits.get(userId)!; // Check like limit (matches GET /like/{user_id}) if (endpoint.match(/^\/like\/[^\/]+$/) && userLimit.likes.remaining <= 0 && Date.now() < userLimit.likes.resetAt) { logger.warn(`Like rate limit exceeded for user: ${userId}`); throw new ApiError( ErrorCodes.RATE_LIMIT_EXCEEDED, 'Like rate limit exceeded', { resetAt: userLimit.likes.resetAt }, 429 ); } // Check super like limit (matches POST /like/{user_id}/super) if (endpoint.match(/^\/like\/[^\/]+\/super$/) && userLimit.superLikes.remaining <= 0 && Date.now() < userLimit.superLikes.resetAt) { logger.warn(`Super like rate limit exceeded for user: ${userId}`); throw new ApiError( ErrorCodes.RATE_LIMIT_EXCEEDED, 'Super like rate limit exceeded', { resetAt: userLimit.superLikes.resetAt }, 429 ); } // Check boost limit (matches POST /boost) if (endpoint === '/boost' && userLimit.boosts.remaining <= 0 && Date.now() < userLimit.boosts.resetAt) { logger.warn(`Boost rate limit exceeded for user: ${userId}`); throw new ApiError( ErrorCodes.RATE_LIMIT_EXCEEDED, 'Boost rate limit exceeded', { resetAt: userLimit.boosts.resetAt }, 429 ); } } } /** * Update rate limit information based on API response * @param endpoint - API endpoint * @param response - API response * @param userId - User ID (optional) */ public updateRateLimits(endpoint: string, response: any, userId?: string): void { if (!response || !response.data) return; // Extract user ID from response if not provided const extractedUserId = userId || this.extractUserId(response); if (!extractedUserId) return; // Initialize user limits if not exists if (!this.userLimits.has(extractedUserId)) { this.userLimits.set(extractedUserId, { likes: { remaining: 100, resetAt: Date.now() + 12 * 60 * 60 * 1000 }, superLikes: { remaining: 5, resetAt: Date.now() + 24 * 60 * 60 * 1000 }, boosts: { remaining: 1, resetAt: Date.now() + 30 * 24 * 60 * 60 * 1000 } }); } const userLimit = this.userLimits.get(extractedUserId)!; // Update like limits (matches GET /like/{user_id}) if (endpoint.match(/^\/like\/[^\/]+$/) && response.data.likes_remaining !== undefined) { userLimit.likes.remaining = response.data.likes_remaining; if (response.data.rate_limited_until) { userLimit.likes.resetAt = response.data.rate_limited_until; } } // Update super like limits (matches POST /like/{user_id}/super) if (endpoint.match(/^\/like\/[^\/]+\/super$/) && response.data.super_likes) { userLimit.superLikes.remaining = response.data.super_likes.remaining; if (response.data.super_likes.resets_at) { userLimit.superLikes.resetAt = new Date(response.data.super_likes.resets_at).getTime(); } } // Update boost limits (matches POST /boost) if (endpoint === '/boost') { userLimit.boosts.remaining = response.data.remaining || 0; if (response.data.resets_at) { userLimit.boosts.resetAt = response.data.resets_at; } } // Save updated limits this.userLimits.set(extractedUserId, userLimit); } /** * Helper to extract user ID from response * @param response - API response * @returns User ID or null if not found */ private extractUserId(response: any): string | null { // Try to find user ID in various response formats if (response.data?._id) return response.data._id; if (response.data?.user?._id) return response.data.user._id; return null; } /** * Get rate limit information for a user * @param userId - User ID * @returns Rate limit information or null if not found */ public getUserRateLimits(userId: string): UserRateLimits | null { return this.userLimits.get(userId) || null; } /** * Get global rate limit information * @returns Global rate limit information */ public getGlobalRateLimits(): GlobalRateLimits { return { ...this.globalLimits }; } /** * Decrement rate limit counter after successful action * @param endpoint - API endpoint * @param userId - User ID */ public decrementRateLimit(endpoint: string, userId: string): void { if (!userId || !this.userLimits.has(userId)) return; const userLimit = this.userLimits.get(userId)!; // Decrement like counter (matches GET /like/{user_id}) if (endpoint.match(/^\/like\/[^\/]+$/) && userLimit.likes.remaining > 0) { userLimit.likes.remaining--; logger.debug(`Decremented like counter for user ${userId}: ${userLimit.likes.remaining} remaining`); } // Decrement super like counter (matches POST /like/{user_id}/super) if (endpoint.match(/^\/like\/[^\/]+\/super$/) && userLimit.superLikes.remaining > 0) { userLimit.superLikes.remaining--; logger.debug(`Decremented super like counter for user ${userId}: ${userLimit.superLikes.remaining} remaining`); } // Decrement boost counter (matches POST /boost) if (endpoint === '/boost' && userLimit.boosts.remaining > 0) { userLimit.boosts.remaining--; logger.debug(`Decremented boost counter for user ${userId}: ${userLimit.boosts.remaining} remaining`); } // Save updated limits this.userLimits.set(userId, userLimit); } /** * Reset rate limits for a user * @param userId - User ID */ public resetUserRateLimits(userId: string): void { this.userLimits.delete(userId); } /** * Track validation failure for rate limiting * @param identifier - User ID, IP address, or other identifier * @param endpoint - API endpoint * @returns True if the user should be blocked due to excessive failures */ public trackValidationFailure(identifier: string, endpoint: string): boolean { const key = `${identifier}:${endpoint}`; const now = Date.now(); // Get or create tracking entry let tracking = this.validationFailures.get(key); if (!tracking) { tracking = { failures: 0, lastFailure: now, endpoint }; } // Reset counter if more than an hour has passed if (now - tracking.lastFailure > 60 * 60 * 1000) { tracking.failures = 0; } // Increment failure count and update timestamp tracking.failures++; tracking.lastFailure = now; // Store updated tracking this.validationFailures.set(key, tracking); // Check if rate limit is exceeded const minuteAgo = now - 60 * 1000; const hourAgo = now - 60 * 60 * 1000; // Count recent failures const recentFailures = Array.from(this.validationFailures.values()) .filter(t => t.lastFailure > minuteAgo && (t.endpoint === endpoint || identifier.includes(identifier))) .length; const hourlyFailures = Array.from(this.validationFailures.values()) .filter(t => t.lastFailure > hourAgo && (t.endpoint === endpoint || identifier.includes(identifier))) .length; // Log excessive failures if (recentFailures >= this.validationRateLimits.maxFailuresPerMinute) { logger.warn(`Validation rate limit exceeded for ${identifier} on ${endpoint}: ${recentFailures} failures in the last minute`); return true; } if (hourlyFailures >= this.validationRateLimits.maxFailuresPerHour) { logger.warn(`Validation rate limit exceeded for ${identifier} on ${endpoint}: ${hourlyFailures} failures in the last hour`); return true; } return false; } /** * Check if validation is rate limited for an identifier * @param identifier - User ID, IP address, or other identifier * @param endpoint - API endpoint * @returns True if validation is rate limited */ public isValidationRateLimited(identifier: string, endpoint: string): boolean { const key = `${identifier}:${endpoint}`; const tracking = this.validationFailures.get(key); if (!tracking) return false; const now = Date.now(); const minuteAgo = now - 60 * 1000; const hourAgo = now - 60 * 60 * 1000; // Count recent failures const recentFailures = Array.from(this.validationFailures.values()) .filter(t => t.lastFailure > minuteAgo && (t.endpoint === endpoint || identifier.includes(identifier))) .length; const hourlyFailures = Array.from(this.validationFailures.values()) .filter(t => t.lastFailure > hourAgo && (t.endpoint === endpoint || identifier.includes(identifier))) .length; return recentFailures >= this.validationRateLimits.maxFailuresPerMinute || hourlyFailures >= this.validationRateLimits.maxFailuresPerHour; } /** * Get validation failure tracking for an identifier * @param identifier - User ID, IP address, or other identifier * @param endpoint - API endpoint * @returns Validation failure tracking or null if not found */ public getValidationFailureTracking(identifier: string, endpoint: string): ValidationFailureTracking | null { const key = `${identifier}:${endpoint}`; return this.validationFailures.get(key) || null; } /** * Reset validation failure tracking for an identifier * @param identifier - User ID, IP address, or other identifier * @param endpoint - API endpoint */ public resetValidationFailureTracking(identifier: string, endpoint: string): void { const key = `${identifier}:${endpoint}`; this.validationFailures.delete(key); } /** * Get validation rate limits configuration * @returns Validation rate limits configuration */ public getValidationRateLimits(): ValidationRateLimits { return { ...this.validationRateLimits }; } } // Export singleton instance export default new RateLimiter();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/glassBead-tc/tinder-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server