this repo has no description
atproto
at main 446 lines 15 kB view raw
1import { Agent, CredentialSession } from "@atproto/api"; 2import { CronJob } from "cron"; 3import * as dotenv from "dotenv"; 4import { existsSync, readFileSync, writeFileSync } from "fs"; 5import * as smartcar from "smartcar"; 6import type { Record } from "./lexiconTypes/types/net/mmatt/vitals/car"; 7 8// Load environment variables 9dotenv.config(); 10 11// Smartcar API configuration 12const SMARTCAR_CLIENT_ID = process.env.SMARTCAR_CLIENT_ID; 13const SMARTCAR_CLIENT_SECRET = process.env.SMARTCAR_CLIENT_SECRET; 14const SMARTCAR_REDIRECT_URI = 15 process.env.SMARTCAR_REDIRECT_URI || "http://localhost:3000/callback"; 16const VEHICLE_ID = process.env.VEHICLE_ID; 17 18// Initialize Smartcar Auth Client 19const client = new smartcar.AuthClient({ 20 clientId: SMARTCAR_CLIENT_ID!, 21 clientSecret: SMARTCAR_CLIENT_SECRET!, 22 redirectUri: SMARTCAR_REDIRECT_URI, 23}); 24 25const manager = new CredentialSession(new URL("https://evil.gay")); 26const agent = new Agent(manager); 27await manager.login({ 28 identifier: process.env.BSKY_DID!, 29 password: process.env.BSKY_PASS!, 30}); 31 32// Token file path 33const TOKENS_FILE = "./smartcar_tokens.json"; 34 35// Initialize token storage 36function initTokenStorage(): void { 37 try { 38 if (!existsSync(TOKENS_FILE)) { 39 writeFileSync(TOKENS_FILE, JSON.stringify({}, null, 2)); 40 console.log("✅ Token storage initialized"); 41 } 42 } catch (error) { 43 console.error("❌ Token storage initialization failed:", error); 44 } 45} 46 47// Token storage functions 48function saveTokens(accessToken: string, refreshToken: string): void { 49 try { 50 const tokenData = { 51 access_token: accessToken, 52 refresh_token: refreshToken, 53 updated_at: new Date().toISOString(), 54 }; 55 writeFileSync(TOKENS_FILE, JSON.stringify(tokenData, null, 2)); 56 console.log("💾 Tokens saved to file"); 57 } catch (error) { 58 console.error("❌ Failed to save tokens:", error); 59 } 60} 61 62function loadTokens(): { 63 accessToken: string; 64 refreshToken: string; 65} | null { 66 try { 67 if (!existsSync(TOKENS_FILE)) { 68 return null; 69 } 70 const tokenData = JSON.parse(readFileSync(TOKENS_FILE, "utf8")); 71 if (tokenData.access_token && tokenData.refresh_token) { 72 console.log("📂 Tokens loaded from file"); 73 return { 74 accessToken: tokenData.access_token, 75 refreshToken: tokenData.refresh_token, 76 }; 77 } 78 return null; 79 } catch (error) { 80 console.error("❌ Failed to load tokens:", error); 81 return null; 82 } 83} 84 85function clearTokens(): void { 86 try { 87 if (existsSync(TOKENS_FILE)) { 88 writeFileSync(TOKENS_FILE, JSON.stringify({}, null, 2)); 89 } 90 console.log("🗑️ Tokens cleared from file"); 91 } catch (error) { 92 console.error("❌ Failed to clear tokens:", error); 93 } 94} 95 96// Initialize token storage on startup 97initTokenStorage(); 98 99// Token variables 100let accessToken: string | null = null; 101let refreshToken: string | null = null; 102 103// Ensure we have a valid access token 104async function ensureValidToken(): Promise<string> { 105 // Load tokens from database if not in memory 106 if (!accessToken || !refreshToken) { 107 const savedTokens = loadTokens(); 108 if (savedTokens) { 109 accessToken = savedTokens.accessToken; 110 refreshToken = savedTokens.refreshToken; 111 } 112 } 113 114 if (!accessToken) { 115 if (!refreshToken) { 116 throw new Error("No valid tokens available. Please re-authenticate."); 117 } 118 119 console.log("🔄 Refreshing access token..."); 120 const tokens = await client.exchangeRefreshToken(refreshToken); 121 accessToken = tokens.accessToken; 122 refreshToken = tokens.refreshToken; // SDK provides new refresh token 123 124 // Save new tokens to database 125 saveTokens(accessToken!, refreshToken!); 126 console.log("✅ Access token refreshed and saved"); 127 } 128 129 return accessToken!; 130} 131 132// Refresh token when API calls fail with authentication errors 133async function refreshTokenIfNeeded(): Promise<void> { 134 if (!refreshToken) { 135 const savedTokens = loadTokens(); 136 if (savedTokens) { 137 refreshToken = savedTokens.refreshToken; 138 } 139 } 140 141 if (!refreshToken) { 142 throw new Error("No refresh token available. Please re-authenticate."); 143 } 144 145 console.log("🔄 Token expired, refreshing access token..."); 146 try { 147 const tokens = await client.exchangeRefreshToken(refreshToken); 148 accessToken = tokens.accessToken; 149 refreshToken = tokens.refreshToken; 150 151 // Save new tokens to database 152 saveTokens(accessToken!, refreshToken!); 153 console.log("✅ Access token refreshed and saved"); 154 } catch (error) { 155 console.error("❌ Failed to refresh token:", error); 156 // Clear invalid tokens 157 clearTokens(); 158 accessToken = null; 159 refreshToken = null; 160 throw new Error("Token refresh failed. Please re-authenticate."); 161 } 162} 163 164// Main API function - now uses Smartcar SDK 165async function makeApiRequest(): Promise<void> { 166 try { 167 console.log(`[${new Date().toISOString()}] Making Smartcar API request...`); 168 169 // Check if we have required environment variables 170 if (!SMARTCAR_CLIENT_ID || !SMARTCAR_CLIENT_SECRET) { 171 throw new Error( 172 "Missing Smartcar credentials. Please set SMARTCAR_CLIENT_ID and SMARTCAR_CLIENT_SECRET" 173 ); 174 } 175 176 if (!VEHICLE_ID) { 177 throw new Error( 178 "Missing VEHICLE_ID. Please set your vehicle ID in environment variables" 179 ); 180 } 181 182 // Ensure we have a valid access token 183 const token = await ensureValidToken(); 184 185 // Create vehicle instance using the SDK 186 let vehicle = new smartcar.Vehicle(VEHICLE_ID, token, { 187 unitSystem: "imperial", // Use imperial units for better readability 188 }); 189 190 let vehicleAttributes: any; 191 let retryCount = 0; 192 const maxRetries = 1; 193 194 // Try to get vehicle attributes with token refresh on auth failure 195 while (retryCount <= maxRetries) { 196 try { 197 vehicleAttributes = await vehicle.attributes(); 198 break; // Success, exit retry loop 199 } catch (error: any) { 200 if ( 201 error.message && 202 error.message.includes("AUTHENTICATION") && 203 retryCount < maxRetries 204 ) { 205 console.log("🔄 Authentication error detected, refreshing token..."); 206 await refreshTokenIfNeeded(); 207 const newToken = await ensureValidToken(); 208 vehicle = new smartcar.Vehicle(VEHICLE_ID, newToken, { 209 unitSystem: "imperial", 210 }); 211 retryCount++; 212 } else { 213 throw error; // Re-throw if not auth error or max retries reached 214 } 215 } 216 } 217 218 // Get additional vehicle data 219 let odometer: any = null; 220 let fuelLevel: any = null; 221 222 try { 223 odometer = await vehicle.odometer(); 224 console.log( 225 `📏 Odometer: ${odometer.distance} ${odometer.unit || "miles"}` 226 ); 227 } catch (odometerError) { 228 console.log("📏 Odometer: Not available"); 229 console.log(odometerError); 230 } 231 232 try { 233 fuelLevel = await vehicle.fuel(); 234 console.log(`🔋 Fuel Range: ${fuelLevel.range} miles`); 235 console.log( 236 `🔋 Fuel Percentage: ${Math.floor(fuelLevel.percentRemaining * 100)}%` 237 ); 238 } catch (fuelLevelError) { 239 console.log("🔋 Fuel Level: Not available"); 240 console.log(fuelLevelError); 241 } 242 243 console.log("✅ Smartcar API request completed successfully"); 244 245 // Create car data record if we have both fuel and odometer data 246 if (fuelLevel && odometer) { 247 const data: Record = { 248 $type: "net.mmatt.vitals.car", 249 createdAt: new Date().toISOString(), 250 carFuelRange: fuelLevel.range || 0, 251 carPercentFuelRemaining: `${(fuelLevel.percentRemaining || 0) * 100}`, 252 amountRemaining: `${fuelLevel.amountRemaining || 0}`, 253 carTraveledDistance: odometer.distance || 0, 254 carMake: vehicleAttributes.make, 255 carModel: vehicleAttributes.model, 256 carYear: vehicleAttributes.year, 257 }; 258 259 await agent.com.atproto.repo.createRecord({ 260 collection: "net.mmatt.vitals.car", 261 record: data, 262 repo: process.env.BSKY_DID!, 263 }); 264 265 console.log("📊 Car data record created:", data); 266 } else { 267 console.log("⚠️ Missing fuel or odometer data, skipping record creation"); 268 } 269 } catch (error) { 270 console.error("❌ Smartcar API request failed:", error); 271 272 // If it's an auth error, provide helpful guidance 273 if ( 274 error instanceof Error && 275 (error.message.includes("AUTHENTICATION") || 276 error.message.includes("401")) 277 ) { 278 console.log("\n🔐 Authentication Error - You may need to:"); 279 console.log(" 1. Set up your environment variables (.env file)"); 280 console.log(" 2. Run the OAuth flow to get initial tokens"); 281 console.log(" 3. Check if your tokens have expired"); 282 console.log( 283 " 4. Clear tokens and re-authenticate: bun run index.ts --clear-tokens" 284 ); 285 } 286 } 287} 288 289// Create cron jobs for 16 requests spread throughout the day (8am-8pm) 290// Avoiding overnight hours and spreading requests evenly 291 292const cronJobs: CronJob[] = [ 293 // Morning (8am-11am): 4 requests 294 new CronJob("0 8 * * *", makeApiRequest, null, false, "America/Chicago"), // 8:00 AM 295 new CronJob("30 9 * * *", makeApiRequest, null, false, "America/Chicago"), // 9:30 AM 296 new CronJob("0 10 * * *", makeApiRequest, null, false, "America/Chicago"), // 10:00 AM 297 new CronJob("45 11 * * *", makeApiRequest, null, false, "America/Chicago"), // 11:45 AM 298 299 // Midday (12pm-3pm): 4 requests 300 new CronJob("15 12 * * *", makeApiRequest, null, false, "America/Chicago"), // 12:15 PM 301 new CronJob("0 13 * * *", makeApiRequest, null, false, "America/Chicago"), // 1:00 PM 302 new CronJob("40 14 * * *", makeApiRequest, null, false, "America/Chicago"), // 2:40 PM 303 new CronJob("0 15 * * *", makeApiRequest, null, false, "America/Chicago"), // 3:00 PM 304 305 // Afternoon (4pm-7pm): 4 requests 306 new CronJob("25 16 * * *", makeApiRequest, null, false, "America/Chicago"), // 4:25 PM 307 new CronJob("10 17 * * *", makeApiRequest, null, false, "America/Chicago"), // 5:10 PM 308 new CronJob("35 18 * * *", makeApiRequest, null, false, "America/Chicago"), // 6:35 PM 309 new CronJob("5 19 * * *", makeApiRequest, null, false, "America/Chicago"), // 7:05 PM 310 311 // Evening (8pm): 4 requests 312 new CronJob("0 20 * * *", makeApiRequest, null, false, "America/Chicago"), // 8:00 PM 313 new CronJob("20 20 * * *", makeApiRequest, null, false, "America/Chicago"), // 8:20 PM 314 new CronJob("40 20 * * *", makeApiRequest, null, false, "America/Chicago"), // 8:40 PM 315 new CronJob("0 21 * * *", makeApiRequest, null, false, "America/Chicago"), // 9:00 PM (just before 8pm cutoff) 316]; 317 318// Graceful shutdown 319process.on("SIGINT", () => { 320 console.log("\n🛑 Shutting down cron jobs..."); 321 cronJobs.forEach((job, index) => { 322 job.stop(); 323 console.log(`⏹️ Job ${index + 1} stopped`); 324 }); 325 326 console.log("👋 Goodbye!"); 327 process.exit(0); 328}); 329// OAuth setup function 330async function setupOAuth(): Promise<void> { 331 if (!SMARTCAR_CLIENT_ID || !SMARTCAR_CLIENT_SECRET) { 332 console.log("❌ Missing Smartcar credentials!"); 333 console.log("Please set up your .env file with:"); 334 console.log(" SMARTCAR_CLIENT_ID=your_client_id"); 335 console.log(" SMARTCAR_CLIENT_SECRET=your_client_secret"); 336 console.log(" VEHICLE_ID=your_vehicle_id"); 337 console.log("\nGet these from: https://console.smartcar.com/"); 338 return; 339 } 340 341 if (!VEHICLE_ID) { 342 console.log("❌ Missing VEHICLE_ID!"); 343 console.log("Please set VEHICLE_ID in your .env file"); 344 return; 345 } 346 347 console.log("🔐 Smartcar OAuth Setup"); 348 console.log("1. Visit this URL to authorize your app:"); 349 const authUrl = client.getAuthUrl([ 350 "read_vehicle_info", 351 "read_odometer", 352 "read_fuel", 353 "read_climate", 354 ]); 355 console.log(` ${authUrl}`); 356 console.log( 357 "\n2. After authorization, you'll get a code in the callback URL" 358 ); 359 console.log("3. Use the code with the exchangeCode method"); 360 console.log("\n💡 Tip: You can also run this with a specific auth code:"); 361 console.log(" bun run index.ts --auth-code=YOUR_CODE_HERE"); 362} 363 364// Check command line arguments 365const args = process.argv.slice(2); 366const authCodeArg = args.find((arg) => arg.startsWith("--auth-code=")); 367const clearTokensArg = args.find((arg) => arg === "--clear-tokens"); 368 369if (clearTokensArg) { 370 console.log("🗑️ Clearing stored tokens..."); 371 try { 372 clearTokens(); 373 console.log("✅ Tokens cleared successfully!"); 374 process.exit(0); 375 } catch (error) { 376 console.error("❌ Failed to clear tokens:", error); 377 process.exit(1); 378 } 379} else if (authCodeArg) { 380 const authCode = authCodeArg.split("=")[1]; 381 if (!authCode) { 382 console.error("❌ No auth code provided"); 383 process.exit(1); 384 } 385 console.log("🔄 Exchanging authorization code for tokens..."); 386 387 client 388 .exchangeCode(authCode) 389 .then(async (tokens) => { 390 accessToken = tokens.accessToken; 391 refreshToken = tokens.refreshToken; 392 393 // Save tokens to database 394 saveTokens(accessToken!, refreshToken!); 395 396 console.log("✅ Tokens obtained successfully!"); 397 console.log(` Access token: ${accessToken!.substring(0, 20)}...`); 398 console.log(` Refresh token: ${refreshToken!.substring(0, 20)}...`); 399 console.log("\n🚀 Starting cron scheduler..."); 400 startCronScheduler(); 401 }) 402 .catch((error) => { 403 console.error("❌ Token exchange failed:", error); 404 process.exit(1); 405 }); 406} else { 407 // Try to load tokens from database on startup 408 try { 409 const savedTokens = loadTokens(); 410 if (savedTokens) { 411 accessToken = savedTokens.accessToken; 412 refreshToken = savedTokens.refreshToken; 413 console.log("🔐 Tokens loaded from database. Starting cron scheduler..."); 414 startCronScheduler(); 415 } else { 416 console.log("🔐 No tokens found. Setting up OAuth..."); 417 setupOAuth(); 418 } 419 } catch (error) { 420 console.error("❌ Failed to load tokens:", error); 421 console.log("🔐 Setting up OAuth..."); 422 setupOAuth(); 423 } 424} 425 426function startCronScheduler(): void { 427 console.log("🕐 Starting cron jobs for Smartcar API requests..."); 428 console.log("📊 Schedule: 16 requests per day (8am-8pm, avoiding overnight)"); 429 console.log("⏰ Timezone: America/Chicago"); 430 431 cronJobs.forEach((job, index) => { 432 job.start(); 433 console.log(`✅ Job ${index + 1} started: ${job.cronTime.toString()}`); 434 }); 435 436 // Keep the process running 437 console.log("🚀 Cron scheduler is running. Press Ctrl+C to stop."); 438 console.log("📝 Schedule summary:"); 439 console.log(" • 8am-11am: 4 requests (8:00, 9:30, 10:00, 11:45)"); 440 console.log(" • 12pm-3pm: 4 requests (12:15, 1:00, 2:40, 3:00)"); 441 console.log(" • 4pm-7pm: 4 requests (4:25, 5:10, 6:35, 7:05)"); 442 console.log(" • 8pm: 4 requests (8:00, 8:20, 8:40, 9:00)"); 443 console.log(" • Total: 16 requests/day (8am-8pm, 0 overnight)"); 444} 445 446await makeApiRequest();