octoposprime / octo-bot

Discord bot for managing OctoposPrime Community
MIT License
5 stars 1 forks source link

Develop Product Hunt Parser #15

Closed Sddilora closed 4 months ago

Sddilora commented 4 months ago

Create a script to parse Product Hunt data in response to a new command (/productHuntStar) where a user can input a Product Hunt link. The script should retrieve information on who from our team has starred the product (which also requires retrieving our team's Product Hunt accounts as prerequisities), the total number of stars, and other relevant details.

Sddilora commented 4 months ago

Product Hunt provides an API, eliminating the need to parse the page. However, the request limit is quite restrictive. One potential solution is to store all team access tokens in an array, but this approach is not confirmed yet.

Sddilora commented 4 months ago

currently working script ( get if the given users voted for the given product with access token)

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

const ACCESS_TOKEN = "" // fill with an access token

type User struct {
    Username string `json:"username"`
    UserID   string `json:"userID"`
}

func main() {
    productSlug := "multi-ai-chat" // This is just an example product slug, but this product really exists.

    // Read users from JSON file
    users, err := readUsersFromFile("users.json")
    if err != nil {
        fmt.Println("Error reading users:", err)
        return
    }

    // Get product ID by slug
    productID, err := getProductIDBySlug(productSlug)
    if err != nil {
        fmt.Println("Error getting product ID:", err)
        return
    }

    // Check if users voted for the product
    for _, user := range users {
        voted, err := hasUserVoted(productID, user.UserID)
        if err != nil {
            fmt.Printf("Error checking if user %s voted: %v\n", user.Username, err)
            continue
        }

        if voted {
            fmt.Printf("User %s voted for the product.\n", user.Username)
        } else {
            fmt.Printf("User %s did not vote for the product.\n", user.Username)
        }
    }
}

func readUsersFromFile(filename string) ([]User, error) {
    var users []User

    file, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }

    err = json.Unmarshal(file, &users)
    if err != nil {
        return nil, err
    }

    return users, nil
}

func getProductIDBySlug(slug string) (string, error) {
    query := map[string]string{
        "query": fmt.Sprintf(`query { post(slug: "%s") { id } }`, slug),
    }

    queryJSON, err := json.Marshal(query)
    if err != nil {
        return "", err
    }

    req, err := http.NewRequest("POST", "https://api.producthunt.com/v2/api/graphql", bytes.NewBuffer(queryJSON))
    if err != nil {
        return "", err
    }

    req.Header.Set("Authorization", "Bearer "+ACCESS_TOKEN)
    req.Header.Set("Accept", "application/json")
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }

    var jsonResponse map[string]interface{}
    if err := json.Unmarshal(body, &jsonResponse); err != nil {
        return "", err
    }

    data, ok := jsonResponse["data"].(map[string]interface{})
    if !ok {
        return "", fmt.Errorf("unexpected response format: missing data field")
    }

    post, ok := data["post"].(map[string]interface{})
    if !ok {
        return "", fmt.Errorf("unexpected response format: missing post field")
    }

    productID, ok := post["id"].(string)
    if !ok {
        return "", fmt.Errorf("unexpected response format: missing product ID")
    }

    return productID, nil
}

func hasUserVoted(productID string, userID string) (bool, error) {
    var cursor *string

    for {
        // Prepare the cursor clause if a cursor is available
        cursorClause := ""
        if cursor != nil {
            cursorClause = fmt.Sprintf(`after: "%s", `, *cursor)
        }

        query := map[string]string{
            "query": fmt.Sprintf(`query {
                post(id: "%s") {
                    votes(%sfirst: 20) {
                        nodes {
                            userId
                        }
                        pageInfo {
                            endCursor
                            hasNextPage
                        }
                    }
                }
            }`, productID, cursorClause),
        }

        queryJSON, err := json.Marshal(query)
        if err != nil {
            return false, err
        }

        req, err := http.NewRequest("POST", "https://api.producthunt.com/v2/api/graphql", bytes.NewBuffer(queryJSON))
        if err != nil {
            return false, err
        }

        req.Header.Set("Authorization", "Bearer "+ACCESS_TOKEN)
        req.Header.Set("Accept", "application/json")
        req.Header.Set("Content-Type", "application/json")

        client := &http.Client{}
        resp, err := client.Do(req)
        if err != nil {
            return false, err
        }
        defer resp.Body.Close()

        body, err := io.ReadAll(resp.Body)
        if err != nil {
            return false, err
        }

        var jsonResponse map[string]interface{}
        if err := json.Unmarshal(body, &jsonResponse); err != nil {
            return false, err
        }

        data, ok := jsonResponse["data"].(map[string]interface{})
        if !ok {
            return false, fmt.Errorf("unexpected response format: missing data field")
        }

        post, ok := data["post"].(map[string]interface{})
        if !ok {
            return false, fmt.Errorf("unexpected response format: missing post field")
        }

        votes, ok := post["votes"].(map[string]interface{})
        if !ok {
            return false, fmt.Errorf("unexpected response format: missing votes field")
        }

        nodes, ok := votes["nodes"].([]interface{})
        if !ok {
            return false, fmt.Errorf("unexpected response format: missing nodes field")
        }

        for _, node := range nodes {
            if node.(map[string]interface{})["userId"] == userID {
                return true, nil
            }
        }

        pageInfo, ok := votes["pageInfo"].(map[string]interface{})
        if !ok {
            return false, fmt.Errorf("unexpected response format: missing pageInfo field")
        }

        hasNextPage, ok := pageInfo["hasNextPage"].(bool)
        if !ok {
            return false, fmt.Errorf("unexpected response format: missing hasNextPage field")
        }

        if !hasNextPage {
            break
        }

        endCursor, ok := pageInfo["endCursor"].(string)
        if !ok {
            return false, fmt.Errorf("unexpected response format: missing endCursor field")
        }
        cursor = &endCursor
    }

    return false, nil
}

users.json

[
  {
      "userID": "7099447",
      "username": "Semanur Guclu"
  },
  {
      "userID": "6562454",
      "username": "Dilara Doğan"
  },
  {
      "userID": "9876542",
      "username": "example_user2"
  },
  {
        "userID": "7109434",
        "username": "Ebrar Kesici"
  }
]
Sddilora commented 4 months ago

Here is an example result:

User Semanur Guclu did not vote for the product.
User Dilara Doğan voted for the product.
User example_user2 did not vote for the product.
User Ebrar Kesici voted for the product.