this repo has no description
atproto
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();