Chrunos commited on
Commit
59e1f2d
·
verified ·
1 Parent(s): d0e1f2a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +642 -236
app.py CHANGED
@@ -1,5 +1,7 @@
1
- from fastapi import FastAPI, HTTPException, status, BackgroundTasks
2
- from fastapi.responses import StreamingResponse, JSONResponse
 
 
3
  import requests
4
  import os
5
  import time
@@ -7,9 +9,11 @@ import logging
7
  import random
8
  import json
9
  from datetime import datetime, timedelta
10
- import hashlib
11
  import re
12
  from urllib.parse import quote
 
 
 
13
 
14
  # Configure logging
15
  logging.basicConfig(
@@ -18,32 +22,271 @@ logging.basicConfig(
18
  )
19
  logger = logging.getLogger(__name__)
20
 
21
- app = FastAPI(title="Instagram Stories API", docs_url=None, redoc_url=None)
22
 
23
- # Environment variables
24
- INSTAGRAM_USERNAME = os.getenv('INSTAGRAM_USERNAME')
25
- INSTAGRAM_PASSWORD = os.getenv('INSTAGRAM_PASSWORD')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  # Configuration
28
  CACHE_DIR = "/tmp/instagram_cache"
29
- COOKIE_FILE = "/tmp/instagram_cookies.json"
 
30
  os.makedirs(CACHE_DIR, exist_ok=True)
31
 
32
- # Cache state
 
 
 
33
  STORY_CACHE = {}
34
  CACHE_EXPIRY = {}
35
- RATE_LIMITS = {}
36
-
37
- # Session state
38
- SESSION = None
39
- SESSION_LAST_REFRESH = None
40
 
41
  # User agents
42
  USER_AGENTS = [
43
- "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
44
- "Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
45
- "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/116.0.5845.0 Mobile/15E148 Safari/604.1",
46
- "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
47
  ]
48
 
49
  def get_cache_key(username):
@@ -81,85 +324,72 @@ def save_to_cache(username, data, minutes=30):
81
  except Exception as e:
82
  logger.warning(f"Failed to save cache: {str(e)}")
83
 
84
- def is_rate_limited(username):
85
- """Check if we should rate limit this request"""
86
- # Check rate limits
87
  key = username.lower()
88
- if key in RATE_LIMITS:
89
- if datetime.now() < RATE_LIMITS[key]["until"]:
90
- seconds = (RATE_LIMITS[key]["until"] - datetime.now()).total_seconds()
91
- logger.warning(f"Rate limited {username} for {int(seconds)}s")
 
 
92
  return True
93
- else:
94
- # Expired rate limit
95
- RATE_LIMITS.pop(key)
96
  return False
97
 
98
- def set_rate_limit(username, minutes=15):
99
- """Set rate limiting for a username"""
100
- key = username.lower()
101
- RATE_LIMITS[key] = {
102
- "until": datetime.now() + timedelta(minutes=minutes),
103
- "requests": 0
104
- }
105
- logger.warning(f"Setting rate limit for {username} for {minutes} minutes")
106
-
107
  def get_instagram_session():
108
- """Get or create an Instagram session"""
109
- global SESSION, SESSION_LAST_REFRESH
110
 
111
- # Create new session if none exists or it's older than 30 minutes
112
- if (SESSION is None or
113
- SESSION_LAST_REFRESH is None or
114
- (datetime.now() - SESSION_LAST_REFRESH).total_seconds() > 1800):
115
-
116
- SESSION = requests.Session()
117
- SESSION.headers.update({
118
- 'User-Agent': random.choice(USER_AGENTS),
119
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
120
- 'Accept-Language': 'en-US,en;q=0.5',
121
- 'Accept-Encoding': 'gzip, deflate, br',
122
- 'Connection': 'keep-alive',
123
- 'Upgrade-Insecure-Requests': '1',
124
- 'Sec-Fetch-Dest': 'document',
125
- 'Sec-Fetch-Mode': 'navigate',
126
- 'Sec-Fetch-Site': 'none',
127
- 'Sec-Fetch-User': '?1',
128
- 'TE': 'trailers',
129
- })
130
-
131
- # Load cookies if available
132
- if os.path.exists(COOKIE_FILE):
133
  try:
134
- with open(COOKIE_FILE, 'r') as f:
135
- cookies = json.load(f)
136
 
137
- for cookie in cookies:
138
- SESSION.cookies.set(cookie['name'], cookie['value'])
139
 
140
- logger.info("Loaded cookies from file")
141
- except Exception as e:
142
- logger.warning(f"Failed to load cookies: {str(e)}")
143
-
144
- SESSION_LAST_REFRESH = datetime.now()
145
-
146
- # Try to log in if we have credentials
147
- if not any(c.name == 'sessionid' for c in SESSION.cookies) and INSTAGRAM_USERNAME and INSTAGRAM_PASSWORD:
148
- try:
149
- login_instagram()
 
 
 
 
 
 
 
150
  except Exception as e:
151
- logger.error(f"Login failed: {str(e)}")
152
 
153
- return SESSION
154
-
155
- def save_cookies():
156
- """Save session cookies to file"""
157
- if SESSION is None:
158
- return
159
 
 
 
 
 
 
 
 
 
 
 
160
  try:
161
  cookies = []
162
- for cookie in SESSION.cookies:
163
  cookies.append({
164
  'name': cookie.name,
165
  'value': cookie.value,
@@ -167,166 +397,253 @@ def save_cookies():
167
  'path': cookie.path
168
  })
169
 
170
- with open(COOKIE_FILE, 'w') as f:
171
- json.dump(cookies, f)
 
 
 
 
172
 
173
- logger.info("Saved cookies to file")
 
174
  except Exception as e:
175
- logger.warning(f"Failed to save cookies: {str(e)}")
 
176
 
177
- def login_instagram():
178
- """Log in to Instagram"""
179
- if not INSTAGRAM_USERNAME or not INSTAGRAM_PASSWORD:
180
- logger.error("Instagram credentials not configured")
181
- raise Exception("Instagram credentials required")
182
-
183
- session = get_instagram_session()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- # First get the login page to get the CSRF token
186
  try:
187
- resp = session.get('https://www.instagram.com/accounts/login/')
188
- time.sleep(random.uniform(1, 3))
 
 
 
189
 
190
- # Extract CSRF token
191
  csrf_token = None
192
- match = re.search(r'"csrf_token":"(.*?)"', resp.text)
193
  if match:
194
  csrf_token = match.group(1)
195
-
196
  if not csrf_token:
197
- raise Exception("Failed to get CSRF token")
198
 
199
- # Prepare login data
200
- login_data = {
201
- 'username': INSTAGRAM_USERNAME,
202
- 'enc_password': f'#PWD_INSTAGRAM_BROWSER:0:{int(time.time())}:{INSTAGRAM_PASSWORD}',
203
- 'queryParams': {},
204
- 'optIntoOneTap': 'false',
205
- 'csrfmiddlewaretoken': csrf_token
206
- }
207
-
208
- # Update headers for the login request
209
  session.headers.update({
210
  'X-CSRFToken': csrf_token,
 
211
  'X-Requested-With': 'XMLHttpRequest',
 
212
  'Referer': 'https://www.instagram.com/accounts/login/',
213
- 'Origin': 'https://www.instagram.com'
214
  })
215
 
216
- # Wait a bit before login
217
- time.sleep(random.uniform(2, 4))
 
218
 
219
- # Submit login
220
- login_resp = session.post(
 
 
 
 
 
 
 
 
 
 
221
  'https://www.instagram.com/accounts/login/ajax/',
222
  data=login_data
223
  )
224
 
225
- login_json = login_resp.json()
226
-
227
- # Check login status
228
- if login_json.get('authenticated') and login_json.get('status') == 'ok':
229
- logger.info("Successfully logged in to Instagram")
230
- save_cookies()
231
- return True
232
- else:
233
- logger.error(f"Login failed: {login_json}")
234
- if 'two_factor_required' in login_json:
235
- raise Exception("Two-factor authentication required")
236
- else:
237
- raise Exception("Login failed")
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  except Exception as e:
240
  logger.error(f"Login error: {str(e)}")
241
- raise
 
 
 
242
 
243
- def get_user_stories(username):
244
- """Get stories for a username using the mobile API"""
245
- if is_rate_limited(username):
246
- cached = get_cached_stories(username)
247
- if cached:
248
- return cached
249
- raise Exception(f"Rate limited for {username}")
250
-
251
  session = get_instagram_session()
 
 
 
252
 
253
  try:
254
- # First, get the user ID
255
- user_id = None
256
- encoded_username = quote(username)
257
-
258
- # Using the web API to get user info
259
- user_info_url = f"https://www.instagram.com/api/v1/users/web_profile_info/?username={encoded_username}"
260
 
261
  session.headers.update({
262
- 'X-IG-App-ID': '936619743392459', # Common App ID
 
 
 
263
  'X-ASBD-ID': '198387',
264
  'X-IG-WWW-Claim': '0',
265
- 'X-Requested-With': 'XMLHttpRequest',
 
266
  'Referer': f'https://www.instagram.com/{username}/',
 
 
 
 
267
  })
268
 
269
- # Random delay to look more like a real user
270
- time.sleep(random.uniform(1, 2))
271
 
272
- user_response = session.get(user_info_url)
273
- user_data = user_response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- if user_data.get('status') != 'ok':
276
- if 'message' in user_data and 'wait' in user_data['message'].lower():
277
- set_rate_limit(username, 30)
278
- raise Exception(f"Rate limited: {user_data.get('message')}")
279
- else:
280
- raise Exception(f"Failed to get user info: {user_data}")
281
 
282
- user_id = user_data['data']['user']['id']
283
- logger.info(f"Got user ID for {username}: {user_id}")
284
 
285
  # Now get the stories
286
- # Small delay between requests
287
- time.sleep(random.uniform(2, 3))
288
-
289
- # Get the stories
290
- stories_url = f"https://i.instagram.com/api/v1/feed/user/{user_id}/story/"
291
 
292
  stories_response = session.get(stories_url)
293
- stories_data = stories_response.json()
294
 
295
- if stories_data.get('reel') is None:
296
- raise Exception("No stories found or private account")
 
 
 
297
 
298
- # Process the stories
 
 
 
 
 
299
  stories = []
300
- items = stories_data.get('reel', {}).get('items', [])
301
-
302
- for item in items:
303
  story = {
304
- "id": item.get('id', ''),
305
  "type": "video" if item.get('media_type') == 2 else "image",
306
- "timestamp": datetime.fromtimestamp(item.get('taken_at')).isoformat()
307
  }
308
 
309
  # Get media URL
310
- if story["type"] == "video":
311
- # Find the best video version
312
- if 'video_versions' in item:
313
- videos = item['video_versions']
314
- if videos:
315
- # Get highest quality video
316
- story["url"] = videos[0]['url']
317
- # Add view count if available
318
- if 'view_count' in item:
319
- story["views"] = item['view_count']
320
  else:
321
- # Find the best image version
322
- if 'image_versions2' in item:
323
- images = item['image_versions2']['candidates']
324
- if images:
325
- # Get highest quality image
326
- story["url"] = images[0]['url']
327
-
328
  stories.append(story)
329
-
 
 
 
 
330
  result = {
331
  "data": stories,
332
  "count": len(stories),
@@ -335,19 +652,77 @@ def get_user_stories(username):
335
  }
336
 
337
  return result
338
-
339
  except Exception as e:
340
  logger.error(f"Error getting stories: {str(e)}")
341
- if 'rate' in str(e).lower() or 'wait' in str(e).lower():
342
- set_rate_limit(username, 30)
343
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
  @app.get("/stories/{username}")
346
- async def get_stories(username: str, cached: bool = False):
347
  """Get Instagram stories for a user"""
348
  logger.info(f"Request for @{username} stories")
349
 
350
- # Return from cache if requested or available
351
  if cached:
352
  cached_result = get_cached_stories(username)
353
  if cached_result:
@@ -355,75 +730,99 @@ async def get_stories(username: str, cached: bool = False):
355
 
356
  try:
357
  # Check for rate limiting
358
- if is_rate_limited(username):
359
  cached_result = get_cached_stories(username)
360
  if cached_result:
361
  return {**cached_result, "from_cache": True, "rate_limited": True}
362
  else:
363
  raise HTTPException(
364
  status_code=status.HTTP_429_TOO_MANY_REQUESTS,
365
- detail="Rate limit exceeded for this username. Please try again later."
366
  )
367
 
368
- # Get the stories
369
- result = get_user_stories(username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
- # Cache the successful result
372
  save_to_cache(username, result)
373
 
374
  return result
375
-
 
 
 
376
  except Exception as e:
377
- error_message = str(e)
378
- logger.error(f"Error getting stories: {error_message}")
379
 
380
  # Check for cached version if there's an error
381
  cached_result = get_cached_stories(username)
382
  if cached_result:
383
- logger.info(f"Returning cached result due to error")
384
  return {**cached_result, "from_cache": True, "error_occurred": True}
385
-
386
- # Handle specific errors
387
- if 'rate' in error_message.lower() or 'wait' in error_message.lower():
388
- raise HTTPException(
389
- status_code=status.HTTP_429_TOO_MANY_REQUESTS,
390
- detail="Instagram rate limit exceeded. Please try again later."
391
- )
392
- elif 'private' in error_message.lower() or 'no stories' in error_message.lower():
393
- raise HTTPException(
394
- status_code=status.HTTP_404_NOT_FOUND,
395
- detail="No stories found or private account"
396
- )
397
- elif 'login' in error_message.lower() or 'credentials' in error_message.lower():
398
- raise HTTPException(
399
- status_code=status.HTTP_401_UNAUTHORIZED,
400
- detail="Authentication error"
401
- )
402
- else:
403
- raise HTTPException(
404
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
405
- detail="Error fetching stories"
406
- )
407
 
408
  @app.get("/download/{url:path}")
409
  async def download_media(url: str):
410
  """Download and proxy media content"""
411
- logger.info(f"Download request for URL")
412
 
413
  try:
414
  # Validate URL
415
- if not url.startswith(("https://instagram", "https://scontent")):
416
  raise HTTPException(
417
  status_code=status.HTTP_400_BAD_REQUEST,
418
  detail="Invalid URL format"
419
  )
420
 
421
  # Configure request
422
- session = get_instagram_session()
423
  headers = {
424
  "User-Agent": random.choice(USER_AGENTS),
425
- "Referer": "https://www.instagram.com/",
426
  "Accept": "*/*",
 
 
 
 
427
  }
428
 
429
  # Get media
@@ -456,9 +855,10 @@ async def download_media(url: str):
456
  detail="Failed to download media"
457
  )
458
 
459
- # Load cache from disk at startup
460
  @app.on_event("startup")
461
- def load_cache_from_disk():
 
462
  try:
463
  count = 0
464
  for filename in os.listdir(CACHE_DIR):
@@ -481,13 +881,19 @@ def load_cache_from_disk():
481
  logger.info(f"Loaded {count} items from cache")
482
  except Exception as e:
483
  logger.error(f"Cache loading error: {e}")
 
 
 
484
 
485
  # Health check endpoint
486
  @app.get("/health")
487
  async def health_check():
 
488
  return {
489
  "status": "ok",
490
- "timestamp": datetime.now().isoformat(),
491
- "cache_size": len(STORY_CACHE),
492
- "rate_limits": len(RATE_LIMITS)
 
 
493
  }
 
1
+ from fastapi import FastAPI, HTTPException, status, BackgroundTasks, Request, Form, Cookie, Depends
2
+ from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse, RedirectResponse
3
+ from fastapi.templating import Jinja2Templates
4
+ from fastapi.staticfiles import StaticFiles
5
  import requests
6
  import os
7
  import time
 
9
  import random
10
  import json
11
  from datetime import datetime, timedelta
 
12
  import re
13
  from urllib.parse import quote
14
+ from typing import Optional
15
+ import secrets
16
+ from pathlib import Path
17
 
18
  # Configure logging
19
  logging.basicConfig(
 
22
  )
23
  logger = logging.getLogger(__name__)
24
 
25
+ app = FastAPI(title="Instagram Stories API")
26
 
27
+ # Create templates and static directories
28
+ templates_dir = Path("templates")
29
+ templates_dir.mkdir(exist_ok=True)
30
+ static_dir = Path("static")
31
+ static_dir.mkdir(exist_ok=True)
32
+
33
+ # Create template files
34
+ login_template = """
35
+ <!DOCTYPE html>
36
+ <html>
37
+ <head>
38
+ <title>Instagram Login</title>
39
+ <style>
40
+ body {
41
+ font-family: Arial, sans-serif;
42
+ max-width: 500px;
43
+ margin: 0 auto;
44
+ padding: 20px;
45
+ }
46
+ .container {
47
+ background-color: #fff;
48
+ border-radius: 8px;
49
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
50
+ padding: 25px;
51
+ }
52
+ h1 {
53
+ color: #262626;
54
+ text-align: center;
55
+ }
56
+ .form-group {
57
+ margin-bottom: 15px;
58
+ }
59
+ label {
60
+ display: block;
61
+ margin-bottom: 5px;
62
+ font-weight: bold;
63
+ }
64
+ input[type="text"], input[type="password"] {
65
+ width: 100%;
66
+ padding: 10px;
67
+ border: 1px solid #dbdbdb;
68
+ border-radius: 3px;
69
+ box-sizing: border-box;
70
+ }
71
+ .btn {
72
+ background-color: #0095f6;
73
+ border: none;
74
+ color: white;
75
+ padding: 10px 15px;
76
+ border-radius: 4px;
77
+ cursor: pointer;
78
+ font-weight: bold;
79
+ width: 100%;
80
+ }
81
+ .btn:hover {
82
+ background-color: #0086e0;
83
+ }
84
+ .status {
85
+ margin-top: 20px;
86
+ padding: 10px;
87
+ border-radius: 4px;
88
+ }
89
+ .success {
90
+ background-color: #e8f5e9;
91
+ color: #388e3c;
92
+ }
93
+ .error {
94
+ background-color: #ffebee;
95
+ color: #d32f2f;
96
+ }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <div class="container">
101
+ <h1>Instagram Login</h1>
102
+ <p>Enter your Instagram credentials to access the API. Your credentials are only used to establish a session and are not stored on the server.</p>
103
+
104
+ {% if message %}
105
+ <div class="status {% if success %}success{% else %}error{% endif %}">
106
+ {{ message }}
107
+ </div>
108
+ {% endif %}
109
+
110
+ <form method="post" action="/login">
111
+ <div class="form-group">
112
+ <label for="username">Username</label>
113
+ <input type="text" id="username" name="username" required>
114
+ </div>
115
+ <div class="form-group">
116
+ <label for="password">Password</label>
117
+ <input type="password" id="password" name="password" required>
118
+ </div>
119
+ <button type="submit" class="btn">Log In</button>
120
+ </form>
121
+ </div>
122
+ </body>
123
+ </html>
124
+ """
125
+
126
+ status_template = """
127
+ <!DOCTYPE html>
128
+ <html>
129
+ <head>
130
+ <title>Instagram API Status</title>
131
+ <style>
132
+ body {
133
+ font-family: Arial, sans-serif;
134
+ max-width: 700px;
135
+ margin: 0 auto;
136
+ padding: 20px;
137
+ }
138
+ .container {
139
+ background-color: #fff;
140
+ border-radius: 8px;
141
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
142
+ padding: 25px;
143
+ }
144
+ h1, h2 {
145
+ color: #262626;
146
+ }
147
+ h1 {
148
+ text-align: center;
149
+ }
150
+ .status-badge {
151
+ display: inline-block;
152
+ padding: 5px 10px;
153
+ border-radius: 4px;
154
+ font-weight: bold;
155
+ margin-left: 10px;
156
+ }
157
+ .connected {
158
+ background-color: #e8f5e9;
159
+ color: #388e3c;
160
+ }
161
+ .disconnected {
162
+ background-color: #ffebee;
163
+ color: #d32f2f;
164
+ }
165
+ .info-box {
166
+ background-color: #f5f5f5;
167
+ border-radius: 4px;
168
+ padding: 15px;
169
+ margin: 15px 0;
170
+ }
171
+ .btn {
172
+ background-color: #0095f6;
173
+ border: none;
174
+ color: white;
175
+ padding: 10px 15px;
176
+ border-radius: 4px;
177
+ text-decoration: none;
178
+ display: inline-block;
179
+ margin-right: 10px;
180
+ margin-top: 10px;
181
+ font-weight: bold;
182
+ }
183
+ .btn:hover {
184
+ background-color: #0086e0;
185
+ }
186
+ table {
187
+ width: 100%;
188
+ border-collapse: collapse;
189
+ margin-top: 15px;
190
+ }
191
+ th, td {
192
+ text-align: left;
193
+ padding: 8px;
194
+ border-bottom: 1px solid #ddd;
195
+ }
196
+ th {
197
+ background-color: #f8f9fa;
198
+ }
199
+ </style>
200
+ </head>
201
+ <body>
202
+ <div class="container">
203
+ <h1>Instagram API Status</h1>
204
+
205
+ <h2>Connection Status
206
+ {% if session_valid %}
207
+ <span class="status-badge connected">Connected</span>
208
+ {% else %}
209
+ <span class="status-badge disconnected">Disconnected</span>
210
+ {% endif %}
211
+ </h2>
212
+
213
+ <div class="info-box">
214
+ {% if session_valid %}
215
+ <p>✅ You are currently logged in as <strong>{{ username }}</strong></p>
216
+ <p>Session will expire: {{ session_expires }}</p>
217
+ {% else %}
218
+ <p>❌ No active Instagram session</p>
219
+ <p>Please login to use the API with full functionality</p>
220
+ {% endif %}
221
+ </div>
222
+
223
+ <a href="/login" class="btn">{% if session_valid %}Re-Login{% else %}Login{% endif %}</a>
224
+ {% if session_valid %}
225
+ <a href="/logout" class="btn" style="background-color: #f44336;">Logout</a>
226
+ {% endif %}
227
+
228
+ <h2>API Endpoints</h2>
229
+ <table>
230
+ <tr>
231
+ <th>Endpoint</th>
232
+ <th>Description</th>
233
+ </tr>
234
+ <tr>
235
+ <td><code>/stories/{username}</code></td>
236
+ <td>Get stories for a user</td>
237
+ </tr>
238
+ <tr>
239
+ <td><code>/download/{url}</code></td>
240
+ <td>Download media from a URL</td>
241
+ </tr>
242
+ <tr>
243
+ <td><code>/status</code></td>
244
+ <td>View API status and session info</td>
245
+ </tr>
246
+ </table>
247
+
248
+ {% if cache_info %}
249
+ <h2>Cache Status</h2>
250
+ <div class="info-box">
251
+ <p>Cached users: {{ cache_info.count }}</p>
252
+ <p>Total items: {{ cache_info.items }}</p>
253
+ </div>
254
+ {% endif %}
255
+ </div>
256
+ </body>
257
+ </html>
258
+ """
259
+
260
+ # Write templates to files
261
+ with open(templates_dir / "login.html", "w") as f:
262
+ f.write(login_template)
263
+
264
+ with open(templates_dir / "status.html", "w") as f:
265
+ f.write(status_template)
266
+
267
+ # Set up templates and static files
268
+ templates = Jinja2Templates(directory=str(templates_dir))
269
+ app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
270
 
271
  # Configuration
272
  CACHE_DIR = "/tmp/instagram_cache"
273
+ COOKIES_FILE = "/tmp/instagram_cookies.json"
274
+ SESSION_KEY = secrets.token_hex(16) # For securing session
275
  os.makedirs(CACHE_DIR, exist_ok=True)
276
 
277
+ # Session and cache state
278
+ IG_SESSION = None
279
+ IG_SESSION_USERNAME = None
280
+ IG_SESSION_EXPIRY = None
281
  STORY_CACHE = {}
282
  CACHE_EXPIRY = {}
283
+ LAST_REQUEST = {}
 
 
 
 
284
 
285
  # User agents
286
  USER_AGENTS = [
287
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
288
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
289
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 
290
  ]
291
 
292
  def get_cache_key(username):
 
324
  except Exception as e:
325
  logger.warning(f"Failed to save cache: {str(e)}")
326
 
327
+ def should_rate_limit_request(username):
328
+ """Basic rate limiting check"""
 
329
  key = username.lower()
330
+ now = datetime.now()
331
+
332
+ if key in LAST_REQUEST:
333
+ seconds_since = (now - LAST_REQUEST[key]).total_seconds()
334
+ if seconds_since < 60: # 1 minute between requests
335
+ logger.warning(f"Rate limiting {username}: {seconds_since:.1f}s since last request")
336
  return True
337
+
338
+ LAST_REQUEST[key] = now
 
339
  return False
340
 
 
 
 
 
 
 
 
 
 
341
  def get_instagram_session():
342
+ """Get current Instagram session"""
343
+ global IG_SESSION, IG_SESSION_EXPIRY
344
 
345
+ # Check if session is still valid
346
+ if IG_SESSION is None or IG_SESSION_EXPIRY is None or datetime.now() > IG_SESSION_EXPIRY:
347
+ # Try to load from file
348
+ if os.path.exists(COOKIES_FILE):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  try:
350
+ with open(COOKIES_FILE, 'r') as f:
351
+ data = json.load(f)
352
 
353
+ if 'expires' in data and datetime.fromisoformat(data['expires']) > datetime.now():
354
+ IG_SESSION = requests.Session()
355
 
356
+ # Set cookies
357
+ for cookie in data['cookies']:
358
+ IG_SESSION.cookies.set(
359
+ cookie['name'],
360
+ cookie['value'],
361
+ domain=cookie.get('domain', '.instagram.com')
362
+ )
363
+
364
+ # Set expiry
365
+ IG_SESSION_EXPIRY = datetime.fromisoformat(data['expires'])
366
+ global IG_SESSION_USERNAME
367
+ IG_SESSION_USERNAME = data.get('username', 'unknown')
368
+
369
+ logger.info(f"Loaded Instagram session for {IG_SESSION_USERNAME}")
370
+ return IG_SESSION
371
+ else:
372
+ logger.warning("Saved session expired, needs new login")
373
  except Exception as e:
374
+ logger.error(f"Failed to load cookies: {str(e)}")
375
 
376
+ # Return existing session if valid
377
+ if IG_SESSION is not None and IG_SESSION_EXPIRY is not None and datetime.now() < IG_SESSION_EXPIRY:
378
+ return IG_SESSION
 
 
 
379
 
380
+ return None
381
+
382
+ def save_instagram_session(session, username, days=7):
383
+ """Save Instagram session cookies"""
384
+ global IG_SESSION, IG_SESSION_EXPIRY, IG_SESSION_USERNAME
385
+
386
+ IG_SESSION = session
387
+ IG_SESSION_EXPIRY = datetime.now() + timedelta(days=days)
388
+ IG_SESSION_USERNAME = username
389
+
390
  try:
391
  cookies = []
392
+ for cookie in session.cookies:
393
  cookies.append({
394
  'name': cookie.name,
395
  'value': cookie.value,
 
397
  'path': cookie.path
398
  })
399
 
400
+ with open(COOKIES_FILE, 'w') as f:
401
+ json.dump({
402
+ 'cookies': cookies,
403
+ 'expires': IG_SESSION_EXPIRY.isoformat(),
404
+ 'username': username
405
+ }, f)
406
 
407
+ logger.info(f"Saved Instagram session for {username}")
408
+ return True
409
  except Exception as e:
410
+ logger.error(f"Failed to save cookies: {str(e)}")
411
+ return False
412
 
413
+ def clear_instagram_session():
414
+ """Clear Instagram session"""
415
+ global IG_SESSION, IG_SESSION_EXPIRY, IG_SESSION_USERNAME
416
+
417
+ IG_SESSION = None
418
+ IG_SESSION_EXPIRY = None
419
+ IG_SESSION_USERNAME = None
420
+
421
+ if os.path.exists(COOKIES_FILE):
422
+ try:
423
+ os.remove(COOKIES_FILE)
424
+ logger.info("Removed Instagram session cookies")
425
+ except Exception as e:
426
+ logger.error(f"Failed to remove cookies file: {str(e)}")
427
+
428
+ def login_to_instagram(username, password):
429
+ """Login to Instagram and save session"""
430
+ session = requests.Session()
431
+
432
+ # Set up headers for web request
433
+ session.headers.update({
434
+ 'User-Agent': random.choice(USER_AGENTS),
435
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
436
+ 'Accept-Language': 'en-US,en;q=0.5',
437
+ 'Accept-Encoding': 'gzip, deflate, br',
438
+ 'DNT': '1',
439
+ 'Connection': 'keep-alive',
440
+ 'Upgrade-Insecure-Requests': '1',
441
+ 'Sec-Fetch-Dest': 'document',
442
+ 'Sec-Fetch-Mode': 'navigate',
443
+ 'Sec-Fetch-Site': 'none',
444
+ 'Sec-Fetch-User': '?1',
445
+ })
446
 
 
447
  try:
448
+ # First get the login page to get CSRF token
449
+ response = session.get('https://www.instagram.com/accounts/login/')
450
+
451
+ # Wait a bit to look natural
452
+ time.sleep(random.uniform(1, 2))
453
 
454
+ # Extract CSRF token from response
455
  csrf_token = None
456
+ match = re.search(r'"csrf_token":"(.*?)"', response.text)
457
  if match:
458
  csrf_token = match.group(1)
459
+
460
  if not csrf_token:
461
+ raise Exception("Could not extract CSRF token")
462
 
463
+ # Update headers for login request
 
 
 
 
 
 
 
 
 
464
  session.headers.update({
465
  'X-CSRFToken': csrf_token,
466
+ 'X-Instagram-AJAX': '1',
467
  'X-Requested-With': 'XMLHttpRequest',
468
+ 'Origin': 'https://www.instagram.com',
469
  'Referer': 'https://www.instagram.com/accounts/login/',
470
+ 'Content-Type': 'application/x-www-form-urlencoded'
471
  })
472
 
473
+ # Prepare login data
474
+ # Convert password to Instagram's browser format
475
+ enc_password = f'#PWD_INSTAGRAM_BROWSER:0:{int(time.time())}:{password}'
476
 
477
+ login_data = {
478
+ 'username': username,
479
+ 'enc_password': enc_password,
480
+ 'queryParams': '{}',
481
+ 'optIntoOneTap': 'false'
482
+ }
483
+
484
+ # Wait a bit more to look natural
485
+ time.sleep(random.uniform(2, 3))
486
+
487
+ # Make login request
488
+ login_response = session.post(
489
  'https://www.instagram.com/accounts/login/ajax/',
490
  data=login_data
491
  )
492
 
493
+ # Check login response
494
+ try:
495
+ login_json = login_response.json()
496
+
497
+ if login_json.get('authenticated') is True:
498
+ logger.info(f"Successfully logged in as {username}")
 
 
 
 
 
 
 
499
 
500
+ # Save session
501
+ if save_instagram_session(session, username):
502
+ return {
503
+ 'success': True,
504
+ 'message': 'Login successful'
505
+ }
506
+ else:
507
+ return {
508
+ 'success': False,
509
+ 'message': 'Login successful but failed to save session'
510
+ }
511
+ else:
512
+ if 'message' in login_json:
513
+ logger.warning(f"Login failed: {login_json['message']}")
514
+ return {
515
+ 'success': False,
516
+ 'message': f"Login failed: {login_json['message']}"
517
+ }
518
+ elif 'error_type' in login_json:
519
+ logger.warning(f"Login failed: {login_json['error_type']}")
520
+ return {
521
+ 'success': False,
522
+ 'message': f"Login failed: {login_json['error_type']}"
523
+ }
524
+ else:
525
+ logger.warning("Login failed: Unknown reason")
526
+ return {
527
+ 'success': False,
528
+ 'message': 'Login failed for unknown reason'
529
+ }
530
+ except Exception as e:
531
+ logger.error(f"Failed to parse login response: {str(e)}")
532
+ return {
533
+ 'success': False,
534
+ 'message': 'Failed to parse login response'
535
+ }
536
+
537
  except Exception as e:
538
  logger.error(f"Login error: {str(e)}")
539
+ return {
540
+ 'success': False,
541
+ 'message': f"Login error: {str(e)}"
542
+ }
543
 
544
+ def get_instagram_stories(username):
545
+ """Get user's stories using the authenticated session"""
 
 
 
 
 
 
546
  session = get_instagram_session()
547
+ if not session:
548
+ logger.warning("No valid Instagram session")
549
+ return None
550
 
551
  try:
552
+ # First get the user ID
553
+ profile_url = f"https://www.instagram.com/api/v1/users/web_profile_info/?username={username}"
 
 
 
 
554
 
555
  session.headers.update({
556
+ 'User-Agent': random.choice(USER_AGENTS),
557
+ 'Accept': '*/*',
558
+ 'Accept-Language': 'en-US,en;q=0.5',
559
+ 'X-IG-App-ID': '936619743392459', # This ID is important
560
  'X-ASBD-ID': '198387',
561
  'X-IG-WWW-Claim': '0',
562
+ 'Origin': 'https://www.instagram.com',
563
+ 'Connection': 'keep-alive',
564
  'Referer': f'https://www.instagram.com/{username}/',
565
+ 'Sec-Fetch-Dest': 'empty',
566
+ 'Sec-Fetch-Mode': 'cors',
567
+ 'Sec-Fetch-Site': 'same-origin',
568
+ 'TE': 'trailers',
569
  })
570
 
571
+ # Get user profile
572
+ profile_response = session.get(profile_url)
573
 
574
+ if profile_response.status_code != 200:
575
+ logger.warning(f"Failed to get user profile: {profile_response.status_code}")
576
+
577
+ if profile_response.status_code == 401:
578
+ # Try to extract the message
579
+ try:
580
+ resp_data = profile_response.json()
581
+ if 'message' in resp_data:
582
+ logger.warning(f"API error: {resp_data['message']}")
583
+ if "wait" in resp_data['message'].lower():
584
+ return {"error": "rate_limited", "message": resp_data['message']}
585
+ except:
586
+ pass
587
+
588
+ return {"error": "unauthorized", "message": "Unauthorized access - session may be expired"}
589
+
590
+ return {"error": "profile_fetch_failed", "status": profile_response.status_code}
591
+
592
+ profile_data = profile_response.json()
593
 
594
+ if 'data' not in profile_data or 'user' not in profile_data['data']:
595
+ logger.warning("User profile data not found")
596
+ return {"error": "profile_not_found", "message": "User profile data not found"}
597
+
598
+ user_id = profile_data['data']['user']['id']
 
599
 
600
+ # Small delay
601
+ time.sleep(random.uniform(1, 2))
602
 
603
  # Now get the stories
604
+ stories_url = f"https://www.instagram.com/api/v1/feed/user/{user_id}/story/"
 
 
 
 
605
 
606
  stories_response = session.get(stories_url)
 
607
 
608
+ if stories_response.status_code != 200:
609
+ logger.warning(f"Failed to get stories: {stories_response.status_code}")
610
+ return {"error": "stories_fetch_failed", "status": stories_response.status_code}
611
+
612
+ stories_data = stories_response.json()
613
 
614
+ # Check if there are stories
615
+ if 'reel' not in stories_data or not stories_data['reel'].get('items'):
616
+ logger.info(f"No stories found for {username}")
617
+ return {"error": "no_stories", "message": "No stories found for this user"}
618
+
619
+ # Process stories
620
  stories = []
621
+ for item in stories_data['reel']['items']:
 
 
622
  story = {
623
+ "id": item.get('pk', ''),
624
  "type": "video" if item.get('media_type') == 2 else "image",
625
+ "timestamp": datetime.fromtimestamp(item.get('taken_at', 0)).isoformat()
626
  }
627
 
628
  # Get media URL
629
+ if story["type"] == "video" and 'video_versions' in item and item['video_versions']:
630
+ story["url"] = item['video_versions'][0]['url']
631
+
632
+ # Add view count if available
633
+ if 'view_count' in item:
634
+ story["views"] = item['view_count']
635
+
636
+ elif 'image_versions2' in item and 'candidates' in item['image_versions2'] and item['image_versions2']['candidates']:
637
+ story["url"] = item['image_versions2']['candidates'][0]['url']
 
638
  else:
639
+ continue # Skip if no URL
640
+
 
 
 
 
 
641
  stories.append(story)
642
+
643
+ if not stories:
644
+ logger.info(f"No valid stories found for {username}")
645
+ return {"error": "no_valid_stories", "message": "No valid stories found for this user"}
646
+
647
  result = {
648
  "data": stories,
649
  "count": len(stories),
 
652
  }
653
 
654
  return result
655
+
656
  except Exception as e:
657
  logger.error(f"Error getting stories: {str(e)}")
658
+ return {"error": "exception", "message": str(e)}
659
+
660
+ @app.get("/", response_class=HTMLResponse)
661
+ async def root(request: Request):
662
+ """Redirect to status page"""
663
+ return RedirectResponse(url="/status")
664
+
665
+ @app.get("/login", response_class=HTMLResponse)
666
+ async def login_page(request: Request, message: str = None, success: bool = False):
667
+ """Login page"""
668
+ return templates.TemplateResponse(
669
+ "login.html",
670
+ {"request": request, "message": message, "success": success}
671
+ )
672
+
673
+ @app.post("/login")
674
+ async def login_process(username: str = Form(...), password: str = Form(...)):
675
+ """Process login form"""
676
+ logger.info(f"Login attempt for {username}")
677
+
678
+ result = login_to_instagram(username, password)
679
+
680
+ if result['success']:
681
+ return RedirectResponse(url="/status", status_code=303)
682
+ else:
683
+ return templates.TemplateResponse(
684
+ "login.html",
685
+ {
686
+ "request": Request,
687
+ "message": result['message'],
688
+ "success": False
689
+ }
690
+ )
691
+
692
+ @app.get("/logout")
693
+ async def logout():
694
+ """Logout and clear session"""
695
+ clear_instagram_session()
696
+ return RedirectResponse(url="/status", status_code=303)
697
+
698
+ @app.get("/status", response_class=HTMLResponse)
699
+ async def status_page(request: Request):
700
+ """Status page"""
701
+ session = get_instagram_session()
702
+ session_valid = session is not None
703
+
704
+ cache_info = {
705
+ "count": len(STORY_CACHE),
706
+ "items": sum(len(data.get("data", [])) for data in STORY_CACHE.values())
707
+ }
708
+
709
+ return templates.TemplateResponse(
710
+ "status.html",
711
+ {
712
+ "request": request,
713
+ "session_valid": session_valid,
714
+ "username": IG_SESSION_USERNAME,
715
+ "session_expires": IG_SESSION_EXPIRY.strftime("%Y-%m-%d %H:%M:%S") if IG_SESSION_EXPIRY else None,
716
+ "cache_info": cache_info
717
+ }
718
+ )
719
 
720
  @app.get("/stories/{username}")
721
+ async def get_stories(username: str, cached: bool = False, demo: bool = False):
722
  """Get Instagram stories for a user"""
723
  logger.info(f"Request for @{username} stories")
724
 
725
+ # Return from cache if requested
726
  if cached:
727
  cached_result = get_cached_stories(username)
728
  if cached_result:
 
730
 
731
  try:
732
  # Check for rate limiting
733
+ if should_rate_limit_request(username):
734
  cached_result = get_cached_stories(username)
735
  if cached_result:
736
  return {**cached_result, "from_cache": True, "rate_limited": True}
737
  else:
738
  raise HTTPException(
739
  status_code=status.HTTP_429_TOO_MANY_REQUESTS,
740
+ detail="Rate limit exceeded. Please try again later."
741
  )
742
 
743
+ # Get stories
744
+ result = get_instagram_stories(username)
745
+
746
+ if not result:
747
+ # No session or other error
748
+ raise HTTPException(
749
+ status_code=status.HTTP_401_UNAUTHORIZED,
750
+ detail="No valid Instagram session. Please login first."
751
+ )
752
+
753
+ if "error" in result:
754
+ # Handle specific errors
755
+ if result["error"] == "rate_limited":
756
+ cached_result = get_cached_stories(username)
757
+ if cached_result:
758
+ return {**cached_result, "from_cache": True, "rate_limited": True}
759
+
760
+ raise HTTPException(
761
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
762
+ detail=result.get("message", "Rate limit exceeded. Please try again later.")
763
+ )
764
+
765
+ elif result["error"] == "unauthorized":
766
+ raise HTTPException(
767
+ status_code=status.HTTP_401_UNAUTHORIZED,
768
+ detail=result.get("message", "Unauthorized. Please login again.")
769
+ )
770
+
771
+ elif result["error"] == "no_stories":
772
+ raise HTTPException(
773
+ status_code=status.HTTP_404_NOT_FOUND,
774
+ detail=result.get("message", "No stories found for this user.")
775
+ )
776
+
777
+ else:
778
+ raise HTTPException(
779
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
780
+ detail=result.get("message", "Failed to get stories")
781
+ )
782
 
783
+ # Cache the result
784
  save_to_cache(username, result)
785
 
786
  return result
787
+
788
+ except HTTPException:
789
+ # Re-raise HTTP exceptions
790
+ raise
791
  except Exception as e:
792
+ logger.error(f"Error: {str(e)}")
 
793
 
794
  # Check for cached version if there's an error
795
  cached_result = get_cached_stories(username)
796
  if cached_result:
 
797
  return {**cached_result, "from_cache": True, "error_occurred": True}
798
+
799
+ raise HTTPException(
800
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
801
+ detail=f"Error getting stories: {str(e)}"
802
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
 
804
  @app.get("/download/{url:path}")
805
  async def download_media(url: str):
806
  """Download and proxy media content"""
807
+ logger.info(f"Download request for media")
808
 
809
  try:
810
  # Validate URL
811
+ if not url.startswith(("https://", "http://")):
812
  raise HTTPException(
813
  status_code=status.HTTP_400_BAD_REQUEST,
814
  detail="Invalid URL format"
815
  )
816
 
817
  # Configure request
818
+ session = get_instagram_session() or requests.Session()
819
  headers = {
820
  "User-Agent": random.choice(USER_AGENTS),
 
821
  "Accept": "*/*",
822
+ "Accept-Language": "en-US,en;q=0.5",
823
+ "Accept-Encoding": "gzip, deflate, br",
824
+ "Referer": "https://www.instagram.com/",
825
+ "Origin": "https://www.instagram.com"
826
  }
827
 
828
  # Get media
 
855
  detail="Failed to download media"
856
  )
857
 
858
+ # Load cache and session at startup
859
  @app.on_event("startup")
860
+ def startup_event():
861
+ # Load cache
862
  try:
863
  count = 0
864
  for filename in os.listdir(CACHE_DIR):
 
881
  logger.info(f"Loaded {count} items from cache")
882
  except Exception as e:
883
  logger.error(f"Cache loading error: {e}")
884
+
885
+ # Try to load session
886
+ get_instagram_session()
887
 
888
  # Health check endpoint
889
  @app.get("/health")
890
  async def health_check():
891
+ session = get_instagram_session()
892
  return {
893
  "status": "ok",
894
+ "authenticated": session is not None,
895
+ "username": IG_SESSION_USERNAME,
896
+ "session_expires": IG_SESSION_EXPIRY.isoformat() if IG_SESSION_EXPIRY else None,
897
+ "cache_items": len(STORY_CACHE),
898
+ "timestamp": datetime.now().isoformat()
899
  }