Add more score when two users are in the same orgs #7

Signed-off-by: Hadi <[email protected]>
This commit is contained in:
Hadi
2025-08-23 15:08:50 +02:00
parent b0c7ce633e
commit 9b914e14ba
+83 -51
View File
@@ -1,11 +1,13 @@
package recon package recon
import ( import (
"errors"
"fmt" "fmt"
"sort" "sort"
github_recon_settings "github.com/anotherhadi/github-recon/settings" github_recon_settings "github.com/anotherhadi/github-recon/settings"
"github.com/anotherhadi/github-recon/utils" "github.com/anotherhadi/github-recon/utils"
"github.com/google/go-github/v72/github"
) )
type CloseFriendsResult []CloseFriendResult type CloseFriendsResult []CloseFriendResult
@@ -16,51 +18,56 @@ type CloseFriendResult struct {
} }
const ( const (
maxFollowingForTarget = 50 maxTargetFollowing = 50
maxFollowersForFollowing = 20 maxFollowersForCandidate = 20
pointPerCriterion = 1 pointsPerCondition = 1
) )
// CloseFriends returns a list of close friends of the user // CloseFriends returns a list of "close friends" for the target user.
// To derive this, we check the following: // A candidate is considered closer if:
// 1. The target has less than 50 Following // 1. The target follows fewer than 50 people.
// 2. The target's following has less than 20 followers (+1 point) // 2. The candidate has fewer than 20 followers (+1 point).
// 3. The target's following follows the target (+1 point) // 3. The candidate follows the target back (+1 point).
// 4. The candidate shares at least one organization with the target (+1 point).
func CloseFriends(s github_recon_settings.Settings) (response CloseFriendsResult) { func CloseFriends(s github_recon_settings.Settings) (results CloseFriendsResult) {
following, resp, err := s.Client.Users.ListFollowing(s.Ctx, s.Target, nil) targetFollowing, resp, err := s.Client.Users.ListFollowing(s.Ctx, s.Target, nil)
if err != nil { if err != nil {
s.Logger.Error("Failed to fetch user's following list", "err", err) s.Logger.Error("Failed to fetch target's following list", "err", err)
return return
} }
utils.WaitForRateLimit(s, resp) utils.WaitForRateLimit(s, resp)
if len(following) >= maxFollowingForTarget { targetOrgs, err := getOrgs(s, s.Target)
s.Logger.Info("Skipping close friends check", "reason", fmt.Sprintf("Target follows %d or more users (%d)", maxFollowingForTarget, len(following))) if err != nil {
s.Logger.Error("Failed to fetch target's organizations", "err", err)
targetOrgs = []*github.Organization{}
}
if len(targetFollowing) >= maxTargetFollowing {
s.Logger.Info("Skipping close friends check",
"reason",
fmt.Sprintf("Target follows %d or more users (limit: %d)", len(targetFollowing), maxTargetFollowing),
)
return return
} }
if len(following) == 0 { if len(targetFollowing) == 0 {
return return
} }
for _, userBeingFollowedByTarget := range following { for _, candidate := range targetFollowing {
loginName := userBeingFollowedByTarget.GetLogin() candidateLogin := candidate.GetLogin()
if loginName == "" { if candidateLogin == "" {
continue continue
} }
currentScore := 0 score := 0
userDetails, userResp, userErr := s.Client.Users.Get(s.Ctx, loginName) candidateDetails, userResp, err := s.Client.Users.Get(s.Ctx, candidateLogin)
if userErr != nil { if err != nil {
s.Logger.Warn( s.Logger.Warn("Failed to fetch details for candidate",
"Failed to fetch details for followed user", "candidate", candidateLogin,
"followed_user", "err", err,
loginName,
"err",
userErr,
) )
if userResp != nil { if userResp != nil {
utils.WaitForRateLimit(s, userResp) utils.WaitForRateLimit(s, userResp)
@@ -69,52 +76,77 @@ func CloseFriends(s github_recon_settings.Settings) (response CloseFriendsResult
} }
utils.WaitForRateLimit(s, userResp) utils.WaitForRateLimit(s, userResp)
if userDetails.GetFollowers() < maxFollowersForFollowing { // Condition: candidate has few followers
currentScore += pointPerCriterion if candidateDetails.GetFollowers() < maxFollowersForCandidate {
score += pointsPerCondition
} }
followsTargetBack, checkErr := checkIfUserFollows(s, loginName, s.Target) // Condition: candidate follows target back
if checkErr != nil { followsBack, err := checkIfUserFollows(s, candidateLogin, s.Target)
continue if err == nil && followsBack {
} else if followsTargetBack { score += pointsPerCondition
currentScore += pointPerCriterion
} }
if currentScore > 0 { // Condition: same organization
response = append(response, CloseFriendResult{ candidateOrgs, _ := getOrgs(s, candidateLogin)
Username: loginName, if isInSameOrg(targetOrgs, candidateOrgs) {
Score: currentScore, score += pointsPerCondition
}
// Add candidate if they matched at least one condition
if score > 0 {
results = append(results, CloseFriendResult{
Username: candidateLogin,
Score: score,
}) })
} }
} }
if len(response) == 0 { if len(results) > 0 {
return sort.Slice(results, func(i, j int) bool {
} else { return results[i].Score > results[j].Score
sort.Slice(response, func(i, j int) bool {
return response[i].Score > response[j].Score
}) })
} }
return return
} }
// checkIfUserFollows checks if sourceUserLogin follows targetUserLogin. // checkIfUserFollows checks if sourceUser follows targetUser.
func checkIfUserFollows(s github_recon_settings.Settings, sourceUserLogin, targetUserLogin string) (bool, error) { func checkIfUserFollows(s github_recon_settings.Settings, sourceUser, targetUser string) (bool, error) {
isFollowing, resp, err := s.Client.Users.IsFollowing(s.Ctx, sourceUserLogin, targetUserLogin) isFollowing, resp, err := s.Client.Users.IsFollowing(s.Ctx, sourceUser, targetUser)
if err != nil { if err != nil {
s.Logger.Warn("Error checking if user follows target", s.Logger.Warn("Error checking if user follows target",
"source_user_checking", sourceUserLogin, "source", sourceUser,
"target_user_to_check", targetUserLogin, "target", targetUser,
"err", err) "err", err,
)
if resp != nil { if resp != nil {
utils.WaitForRateLimit(s, resp) utils.WaitForRateLimit(s, resp)
} }
return false, err return false, err
} }
if resp != nil { if resp != nil {
utils.WaitForRateLimit(s, resp) utils.WaitForRateLimit(s, resp)
} }
return isFollowing, nil return isFollowing, nil
} }
func getOrgs(s github_recon_settings.Settings, user string) ([]*github.Organization, error) {
orgs, resp, err := s.Client.Organizations.List(s.Ctx, user, nil)
if err != nil {
return nil, errors.New("failed to fetch organizations for user")
}
utils.WaitForRateLimit(s, resp)
return orgs, nil
}
func isInSameOrg(orgsA, orgsB []*github.Organization) bool {
for _, orgA := range orgsA {
for _, orgB := range orgsB {
if orgA.GetLogin() == orgB.GetLogin() {
return true
}
}
}
return false
}