package server
import (
"fmt"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"social-api/instagram"
)
// sessionCookies extracts session cookies from the session_id query parameter.
func sessionCookies(r *http.Request) map[string]string {
cookies := make(map[string]string)
if sessionID := r.URL.Query().Get("session_id"); sessionID != "" {
cookies["sessionid"] = sessionID
if parts := strings.SplitN(sessionID, ":", 2); len(parts) >= 1 && parts[0] != "" {
cookies["ds_user_id"] = parts[0]
}
}
return cookies
}
// ───── Followers ─────
func (s *Server) handleFollowers(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
if username == "" {
errorJSON(w, http.StatusBadRequest, "username is required")
return
}
ctx := r.Context()
user, err := s.client.ExtractUserDetails(ctx, username)
if err != nil {
errorJSON(w, http.StatusInternalServerError, err.Error())
return
}
if user.UserID == "" {
writeJSON(w, http.StatusNotFound, map[string]interface{}{"details": "User not found.", "status": 404})
return
}
apiURL := fmt.Sprintf("https://www.instagram.com/api/v1/friendships/%s/followers/", user.UserID)
totalAvailable := user.FollowersCount
target := totalAvailable
if target > 500 {
target = 500
}
if target <= 0 {
writeJSON(w, http.StatusOK, map[string]interface{}{"followers": []interface{}{}})
return
}
cookies := sessionCookies(r)
collected := make([]interface{}, 0, target)
seen := make(map[string]bool)
var nextMaxID string
for len(collected) < target {
params := map[string]string{
"count": "12",
"search_surface": "follow_list_page",
}
if nextMaxID != "" {
params["max_id"] = nextMaxID
}
result, err := s.client.DoWithRetry(ctx, instagram.RequestOption{
Method: "GET",
URL: apiURL,
Headers: map[string]string{"x-ig-app-id": s.cfg.IGAppID},
Params: params,
Cookies: cookies,
UseProxy: true,
})
if err != nil {
break
}
if errMsg, ok := result["error"]; ok {
fmt.Printf("Failed to fetch followers: %v\n", errMsg)
break
}
users, _ := result["users"].([]interface{})
for _, u := range users {
um, ok := u.(map[string]interface{})
if !ok {
continue
}
pkID := fmt.Sprintf("%v", um["pk_id"])
if pkID != "" && pkID != "<nil>" && !seen[pkID] {
seen[pkID] = true
collected = append(collected, um)
if len(collected) >= target {
break
}
}
}
hasMore, _ := result["has_more"].(bool)
nm, _ := result["next_max_id"]
nextMaxID = fmt.Sprintf("%v", nm)
restricted := result["should_limit_list_of_followers"] != nil || result["use_clickable_see_more"] != nil
if !hasMore || nextMaxID == "" || nextMaxID == "<nil>" || restricted {
break
}
}
writeJSON(w, http.StatusOK, map[string]interface{}{"followers": collected})
}
// ───── Following ─────
func (s *Server) handleFollowing(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
if username == "" {
errorJSON(w, http.StatusBadRequest, "username is required")
return
}
ctx := r.Context()
user, err := s.client.ExtractUserDetails(ctx, username)
if err != nil {
errorJSON(w, http.StatusInternalServerError, err.Error())
return
}
if user.UserID == "" {
writeJSON(w, http.StatusNotFound, map[string]interface{}{"details": "User not found.", "status": 404})
return
}
apiURL := fmt.Sprintf("https://www.instagram.com/api/v1/friendships/%s/following/", user.UserID)
totalAvailable := user.FollowingCount
target := totalAvailable
if target > 500 {
target = 500
}
if target <= 0 {
writeJSON(w, http.StatusOK, map[string]interface{}{"following": []interface{}{}})
return
}
cookies := sessionCookies(r)
collected := make([]interface{}, 0, target)
seen := make(map[string]bool)
var nextMaxID string
for len(collected) < target {
params := map[string]string{"count": "12"}
if nextMaxID != "" {
params["max_id"] = nextMaxID
}
result, err := s.client.DoWithRetry(ctx, instagram.RequestOption{
Method: "GET",
URL: apiURL,
Headers: map[string]string{"x-ig-app-id": s.cfg.IGAppID},
Params: params,
Cookies: cookies,
UseProxy: true,
})
if err != nil {
break
}
if errMsg, ok := result["error"]; ok {
fmt.Printf("Failed to fetch following: %v\n", errMsg)
break
}
users, _ := result["users"].([]interface{})
for _, u := range users {
um, ok := u.(map[string]interface{})
if !ok {
continue
}
pkID := fmt.Sprintf("%v", um["pk_id"])
if pkID != "" && pkID != "<nil>" && !seen[pkID] {
seen[pkID] = true
collected = append(collected, um)
if len(collected) >= target {
break
}
}
}
hasMore, _ := result["has_more"].(bool)
nm, _ := result["next_max_id"]
nextMaxID = fmt.Sprintf("%v", nm)
restricted := result["should_limit_list_of_followers"] != nil || result["use_clickable_see_more"] != nil
if !hasMore || nextMaxID == "" || nextMaxID == "<nil>" || restricted {
break
}
}
writeJSON(w, http.StatusOK, map[string]interface{}{"following": collected})
}
// ───── User Details ─────
func (s *Server) handleUserDetails(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
if username == "" {
errorJSON(w, http.StatusBadRequest, "username is required")
return
}
ctx := r.Context()
result, err := s.client.DoWithRetry(ctx, instagram.RequestOption{
Method: "GET",
URL: fmt.Sprintf("https://i.instagram.com/api/v1/users/web_profile_info/?username=%s", username),
Headers: map[string]string{"x-ig-app-id": s.cfg.IGAppID},
UseProxy: true,
})
if err != nil {
errorJSON(w, http.StatusInternalServerError, err.Error())
return
}
if errMsg, ok := result["error"]; ok {
status := http.StatusInternalServerError
if sc, ok := result["status_code"].(float64); ok {
status = int(sc)
}
errorJSON(w, status, fmt.Sprintf("Failed to fetch user details: %v", errMsg))
return
}
writeJSON(w, http.StatusOK, result)
}
// ───── Latest Posts ─────
func (s *Server) handleLatestPosts(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
if username == "" {
errorJSON(w, http.StatusBadRequest, "username is required")
return
}
ctx := r.Context()
result, err := s.client.DoWithRetry(ctx, instagram.RequestOption{
Method: "GET",
URL: fmt.Sprintf("https://i.instagram.com/api/v1/users/web_profile_info/?username=%s", username),
Headers: map[string]string{"x-ig-app-id": s.cfg.IGAppID},
UseProxy: true,
})
if err != nil {
errorJSON(w, http.StatusInternalServerError, err.Error())
return
}
if errMsg, ok := result["error"]; ok {
errorJSON(w, http.StatusInternalServerError, fmt.Sprintf("Failed to fetch user details: %v", errMsg))
return
}
// Navigate: data -> user -> edge_owner_to_timeline_media -> edges
data, _ := result["data"].(map[string]interface{})
user, _ := data["user"].(map[string]interface{})
timeline, _ := user["edge_owner_to_timeline_media"].(map[string]interface{})
edges, _ := timeline["edges"].([]interface{})
// Filter out pinned posts
filtered := make([]interface{}, 0, len(edges))
for _, e := range edges {
edge, ok := e.(map[string]interface{})
if !ok {
continue
}
if isPinned(edge) {
continue
}
filtered = append(filtered, edge)
}
writeJSON(w, http.StatusOK, filtered)
}
func isPinned(edge map[string]interface{}) bool {
node, _ := edge["node"].(map[string]interface{})
if node == nil {
return false
}
if pinned, ok := node["pinned_for_users"].([]interface{}); ok && len(pinned) > 0 {
return true
}
if pinned, ok := node["timeline_pinned_user_ids"].([]interface{}); ok && len(pinned) > 0 {
return true
}
return false
}
// ───── Reels (all) ─────
func (s *Server) handleReels(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
if username == "" {
errorJSON(w, http.StatusBadRequest, "username is required")
return
}
ctx := r.Context()
user, err := s.client.ExtractUserDetails(ctx, username)
if err != nil {
errorJSON(w, http.StatusInternalServerError, err.Error())
return
}
if user.UserID == "" {
writeJSON(w, http.StatusNotFound, map[string]interface{}{"details": "User not found.", "status": 404})
return
}
cookies := sessionCookies(r)
// Scrape CSRF token like the Python version does
csrfToken := ""
body, _, scrapeErr := s.client.DoRaw(ctx, instagram.RequestOption{
Method: "GET",
URL: "https://www.instagram.com/",
Headers: postHeaders(),
UseProxy: true,
})
if scrapeErr == nil && body != nil {
csrfToken = extractCSRFFromHTML(string(body))
}
reelsHeaders := map[string]string{"x-ig-app-id": s.cfg.IGAppID}
if csrfToken != "" {
reelsHeaders["X-CSRFToken"] = csrfToken
}
var allEdges []interface{}
var endCursor string
hasNextPage := true
pagesFetched := 0
for page := 0; page < 10; page++ {
var formData map[string]string
if page == 0 {
formData = map[string]string{
"variables": fmt.Sprintf(`{"data":{"include_feed_video":true,"page_size":12,"target_user_id":"%s"}}`, user.UserID),
"doc_id": "24127588873492897",
}
} else {
if !hasNextPage || endCursor == "" {
break
}
formData = map[string]string{
"variables": fmt.Sprintf(`{"after":"%s","before":null,"data":{"include_feed_video":true,"page_size":12,"target_user_id":"%s"},"first":3,"last":null}`, endCursor, user.UserID),
"doc_id": "24127588873492897",
}
}
result, err := s.client.DoWithRetry(ctx, instagram.RequestOption{
Method: "POST",
URL: "https://www.instagram.com/graphql/query",
Headers: reelsHeaders,
FormData: formData,
Cookies: cookies,
UseProxy: true,
})
if err != nil {
errorJSON(w, http.StatusInternalServerError, fmt.Sprintf("Failed on page %d: %v", page+1, err))
return
}
if errMsg, ok := result["error"]; ok {
errorJSON(w, http.StatusInternalServerError, fmt.Sprintf("Failed on page %d: %v", page+1, errMsg))
return
}
data, _ := result["data"].(map[string]interface{})
connection, _ := data["xdt_api__v1__clips__user__connection_v2"].(map[string]interface{})
edges, _ := connection["edges"].([]interface{})
pageInfo, _ := connection["page_info"].(map[string]interface{})
allEdges = append(allEdges, edges...)
pagesFetched = page + 1
endCursor, _ = pageInfo["end_cursor"].(string)
hasNextPage, _ = pageInfo["has_next_page"].(bool)
if !hasNextPage {
break
}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"username": username,
"user_id": user.UserID,
"pages_fetched": pagesFetched,
"total_reels": len(allEdges),
"edges": allEdges,
})
}
// ───── Single Reel Info ─────
func (s *Server) handleReelInfo(w http.ResponseWriter, r *http.Request) {
rawURL := r.URL.Query().Get("url")
if rawURL == "" {
errorJSON(w, http.StatusBadRequest, "url is required")
return
}
shortcode := instagram.ExtractShortcode(rawURL)
if shortcode == "" {
errorJSON(w, http.StatusBadRequest, "Could not extract shortcode from URL")
return
}
mediaPK := instagram.ShortcodeToPK(shortcode)
apiURL := fmt.Sprintf("https://i.instagram.com/api/v1/media/%s/info/", mediaPK)
ctx := r.Context()
reelHeaders := map[string]string{
"User-Agent": "Instagram 364.0.0.35.86 Android (26/8.0.0; 480dpi; 1080x1920; OnePlus; 6T Dev; devitron; qcom; en_US; 374010953)",
"Accept-Language": "en-US",
"Accept-Encoding": "gzip, deflate",
"X-IG-App-ID": "567067343352427",
"X-IG-Capabilities": "3brTv10=",
"X-IG-Connection-Type": "WIFI",
"X-IG-Timezone-Offset": "0",
"X-Pigeon-Rawclienttime": fmt.Sprintf("%.3f", float64(time.Now().UnixMilli())/1000),
"X-IG-Bandwidth-Speed-KBPS": strconv.Itoa(2500 + rand.Intn(500)),
"Host": "i.instagram.com",
"Connection": "keep-alive",
}
cookies := sessionCookies(r)
result, err := s.client.DoWithRetry(ctx, instagram.RequestOption{
Method: "GET",
URL: apiURL,
Headers: reelHeaders,
Cookies: cookies,
UseProxy: true,
})
if err != nil {
errorJSON(w, http.StatusServiceUnavailable, fmt.Sprintf("Failed after retries: %v", err))
return
}
if errMsg, ok := result["error"]; ok {
status := http.StatusInternalServerError
if sc, ok := result["status_code"].(float64); ok {
status = int(sc)
}
errorJSON(w, status, fmt.Sprintf("%v", errMsg))
return
}
items, _ := result["items"].([]interface{})
if len(items) == 0 {
errorJSON(w, http.StatusNotFound, "No items returned")
return
}
writeJSON(w, http.StatusOK, items[0])
}
// ───── Post by Shortcode ─────
func (s *Server) handlePost(w http.ResponseWriter, r *http.Request) {
shortcode := r.URL.Query().Get("shortcode")
if shortcode == "" {
errorJSON(w, http.StatusBadRequest, "shortcode is required")
return
}
ctx := r.Context()
// Step 1: Visit instagram.com to get CSRF token (like the Python version)
csrfToken := ""
body, _, err := s.client.DoRaw(ctx, instagram.RequestOption{
Method: "GET",
URL: "https://www.instagram.com/",
Headers: postHeaders(),
UseProxy: true,
})
if err == nil && body != nil {
// Try to extract csrftoken from Set-Cookie-like patterns in response
// For simplicity, use a basic approach - the Go http.Client handles cookies
// but we need to extract from raw response
csrfToken = extractCSRFFromHTML(string(body))
}
// Step 2: Make the GraphQL query
formData := map[string]string{
"variables": fmt.Sprintf(`{"shortcode":"%s"}`, shortcode),
"doc_id": "8845758582119845",
"server_timestamps": "true",
}
hdrs := postHeaders()
if csrfToken != "" {
hdrs["X-CSRFToken"] = csrfToken
}
result, err := s.client.DoWithRetry(ctx, instagram.RequestOption{
Method: "POST",
URL: "https://www.instagram.com/graphql/query",
Headers: hdrs,
FormData: formData,
UseProxy: true,
})
if err != nil {
errorJSON(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, result)
}
func postHeaders() map[string]string {
return map[string]string{
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.8",
"Referer": "https://www.instagram.com/",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
"Accept": "*/*",
}
}
func extractCSRFFromHTML(html string) string {
// Look for csrftoken in various patterns
patterns := []string{
`"csrf_token":"`,
`"csrfToken":"`,
`csrftoken=`,
}
for _, p := range patterns {
idx := indexOf(html, p)
if idx < 0 {
continue
}
start := idx + len(p)
end := start
for end < len(html) && html[end] != '"' && html[end] != ';' && html[end] != '&' {
end++
}
if end > start {
return html[start:end]
}
}
return ""
}
func indexOf(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}