GitHub で片思いフォロー、片思われフォローをリストアップする

GitHub で片思いフォロー、片思われフォローをリストアップする

掲題を行うスクリプトを作成したのでお使いください。

使い方 #

go run main.go -u my-user-name -t ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Go 言語で作成したため、go run で実行します。
  • -u で自分の GitHub ユーザ名を、-t で自分の GitHub アクセストークンを指定してください。
  • 使用するアクセストークンには user:follow の権限が付与されている必要があります。

GitHub アクセストークン

コード(Go 言語) #

main.goのファイルを作成し、そこにコピペしてください。なおファイル名は任意です。

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
)

const githubAPI = "https://api.github.com"

func main() {
	// ---------------------------------------------------------------------------
	// コマンドライン引数を受け取る
	// ---------------------------------------------------------------------------
	username := flag.String("u", "", "GitHub のユーザー名")
	token := flag.String("t", "", "GitHub のアクセストークン")
	flag.Parse()

	if *username == "" || *token == "" {
		fmt.Println("使い方: go run main.go -u <username> -t <token>")
		os.Exit(1)
	}

	// ---------------------------------------------------------------------------
	// フォロー・フォロワーを取得する
	// ---------------------------------------------------------------------------
	githubFollowingUrl := fmt.Sprintf("%s/users/%s/following", githubAPI, *username)
	following, err := fetchUsers(githubFollowingUrl, *token)
	if err != nil {
		log.Fatalf("following の取得に失敗: %v", err)
	}

	githubFollowersUrl := fmt.Sprintf("%s/users/%s/followers", githubAPI, *username)
	followers, err := fetchUsers(githubFollowersUrl, *token)
	if err != nil {
		log.Fatalf("followers の取得に失敗: %v", err)
	}

	// ---------------------------------------------------------------------------
	// 片思いリストを作成する
	// ---------------------------------------------------------------------------
	onlyFollowing := []string{} // 自分はフォローしているが相手はしていない
	onlyFollowers := []string{} // 相手はフォローしているが自分はしていない

	followingSet := make(map[string]struct{})
	for _, user := range following {
		followingSet[user] = struct{}{}
	}

	followersSet := make(map[string]struct{})
	for _, user := range followers {
		followersSet[user] = struct{}{}
	}

	for _, user := range following {
		if _, ok := followersSet[user]; !ok {
			onlyFollowing = append(onlyFollowing, user)
		}
	}

	for _, user := range followers {
		if _, ok := followingSet[user]; !ok {
			onlyFollowers = append(onlyFollowers, user)
		}
	}

	// ---------------------------------------------------------------------------
	// 出力する
	// ---------------------------------------------------------------------------
	fmt.Println("---------------------------------------------------------------")
	fmt.Println("✅ 自分は相手をフォローしているが、その逆はないユーザー")
	fmt.Println("---------------------------------------------------------------")
	for _, user := range onlyFollowing {
		fmt.Println(user)
	}

	fmt.Println("---------------------------------------------------------------")
	fmt.Println("✅ 相手は自分をフォローしているが、その逆はないユーザー")
	fmt.Println("---------------------------------------------------------------")
	for _, user := range onlyFollowers {
		fmt.Println(user)
	}
}

func fetchUsers(url, token string) ([]string, error) {
	var users []string
	client := &http.Client{}

	for {
		// 新しいHTTPリクエストを作成
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			return nil, err
		}

		// 認証ヘッダーを設定
		req.Header.Set("Authorization", "token "+token)
		req.Header.Set("Accept", "application/vnd.github+json")

		// リクエストを送信
		resp, err := client.Do(req)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()

		// ステータスコードが 200 でなければエラー扱い
		if resp.StatusCode != http.StatusOK {
			body, _ := io.ReadAll(resp.Body)
			return nil, fmt.Errorf("GitHub API エラー: %s", string(body))
		}

		// レスポンスボディをデコードしてユーザー名(Login)だけ取り出す
		var data []struct {
			Login string `json:"login"`
		}
		if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
			return nil, err
		}

		// 取得したユーザー名をリストに追加
		for _, user := range data {
			users = append(users, user.Login)
		}

		// ページネーションがあるか確認(Link ヘッダーを見る)
		linkHeader := resp.Header.Get("Link")
		nextURL := parseNextURL(linkHeader)
		if nextURL == "" {
			break // 次ページがなければループを終了する
		}
		url = nextURL // 次のページの URL をセットしてループ継続する
	}

	return users, nil
}

func parseNextURL(linkHeader string) string {
	if linkHeader == "" {
		return ""
	}

	// Link ヘッダーをカンマ区切りで分割する
	parts := strings.Split(linkHeader, ",")
	for _, part := range parts {
		// 各パートをセミコロンで分割して URL 部分と rel 部分に分ける
		section := strings.Split(part, ";")
		if len(section) < 2 {
			continue
		}

		urlPart := strings.Trim(section[0], " <>") // URL 部分の <> を除去する
		relPart := strings.Trim(section[1], " ") // rel 情報を取得する

		// rel="next"(次ページリンク)ならその URL を返す
		if relPart == `rel="next"` {
			return urlPart
		}
	}

	// 次ページが見つからない場合は空文字を返す
	return ""
}

補足 #

最近流行りのバイブコーディングで作成しました。