Skip to content

Commit

Permalink
feat: allow export of followers and following lists (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
cecobask authored Oct 16, 2023
1 parent b35c6ff commit 3ba2fad
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 36 deletions.
20 changes: 20 additions & 0 deletions cmd/followdata/followers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package followdata

import (
"github.com/cecobask/instagram-insights/pkg/instagram"
"github.com/cecobask/instagram-insights/pkg/instagram/followdata"
"github.com/spf13/cobra"
)

const CommandNameFollowers = "followers"

func NewFollowersCommand() *cobra.Command {
return &cobra.Command{
Use: CommandNameFollowers,
Short: "Retrieve a list of users who follow you",
RunE: func(cmd *cobra.Command, args []string) error {
opts := instagram.NewOptions(instagram.OutputTable)
return followdata.NewHandler().Followers(opts)
},
}
}
20 changes: 20 additions & 0 deletions cmd/followdata/following.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package followdata

import (
"github.com/cecobask/instagram-insights/pkg/instagram"
"github.com/cecobask/instagram-insights/pkg/instagram/followdata"
"github.com/spf13/cobra"
)

const CommandNameFollowing = "following"

func NewFollowingCommand() *cobra.Command {
return &cobra.Command{
Use: CommandNameFollowing,
Short: "Retrieve a list of users who you follow",
RunE: func(cmd *cobra.Command, args []string) error {
opts := instagram.NewOptions(instagram.OutputTable)
return followdata.NewHandler().Following(opts)
},
}
}
2 changes: 2 additions & 0 deletions cmd/followdata/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ func NewRootCommand() *cobra.Command {
return cmd.Help()
},
}
cmd.AddCommand(NewFollowersCommand())
cmd.AddCommand(NewFollowingCommand())
cmd.AddCommand(NewUnfollowersCommand())
return cmd
}
2 changes: 1 addition & 1 deletion cmd/followdata/unfollowers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const CommandNameUnfollowers = "unfollowers"
func NewUnfollowersCommand() *cobra.Command {
return &cobra.Command{
Use: CommandNameUnfollowers,
Short: "Find out which instagram users are not following back",
Short: "Retrieve a list of users who are not following you back",
RunE: func(cmd *cobra.Command, args []string) error {
opts := instagram.NewOptions(instagram.OutputTable)
return followdata.NewHandler().Unfollowers(opts)
Expand Down
1 change: 1 addition & 0 deletions pkg/instagram/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ const (
PathFollowing = PathData + "/followers_and_following/following.json"
TableHeaderProfileUrl = "PROFILE URL"
TableHeaderUsername = "USERNAME"
TableHeaderTimestamp = "TIMESTAMP"
)
94 changes: 66 additions & 28 deletions pkg/instagram/followdata/followdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"time"

"github.com/cecobask/instagram-insights/pkg/filesystem"
"github.com/cecobask/instagram-insights/pkg/instagram"
Expand Down Expand Up @@ -69,9 +70,9 @@ func (h *handler) Unfollowers(opts instagram.Options) error {
}

type followData struct {
Following users
Followers users
Unfollowers users
Following *userList
Followers *userList
Unfollowers *userList
}

type userData struct {
Expand All @@ -80,9 +81,18 @@ type userData struct {

func newFollowData() *followData {
return &followData{
Following: make(users),
Followers: make(users),
Unfollowers: make(users),
Following: &userList{
users: make(map[string]user),
showTimestamp: true,
},
Followers: &userList{
users: make(map[string]user),
showTimestamp: true,
},
Unfollowers: &userList{
users: make(map[string]user),
showTimestamp: false,
},
}
}

Expand All @@ -91,9 +101,9 @@ func (fd *followData) hydrateFollowers(data []byte) error {
if err := json.Unmarshal(data, &jsonData); err != nil {
return err
}
for _, follower := range jsonData {
userData := follower.UserData[0]
fd.Followers[userData.Username] = userData
for i := range jsonData {
ud := jsonData[i].UserData[0]
fd.Followers.users[ud.Username] = ud
}
return nil
}
Expand All @@ -103,30 +113,50 @@ func (fd *followData) hydrateFollowing(data []byte) error {
if err := json.Unmarshal(data, &jsonData); err != nil {
return err
}
for _, following := range jsonData["relationships_following"] {
userData := following.UserData[0]
fd.Following[userData.Username] = userData
for i := range jsonData["relationships_following"] {
ud := jsonData["relationships_following"][i].UserData[0]
fd.Following.users[ud.Username] = ud
}
return nil
}

func (fd *followData) hydrateUnfollowers() {
for username, user := range fd.Following {
if _, found := fd.Followers[username]; !found {
fd.Unfollowers[username] = user
for username, ud := range fd.Following.users {
if _, found := fd.Followers.users[username]; !found {
fd.Unfollowers.users[username] = ud
}
}
}

type timestamp struct {
time.Time
}

func (t *timestamp) UnmarshalJSON(b []byte) error {
var unixTimestamp int64
if err := json.Unmarshal(b, &unixTimestamp); err != nil {
return err
}
t.Time = time.Unix(unixTimestamp, 0)
return nil
}

func (t *timestamp) String() string {
return t.Format(time.DateOnly)
}

type user struct {
ProfileUrl string `json:"href"`
Username string `json:"value"`
Timestamp int `json:"timestamp"`
ProfileUrl string `json:"href"`
Username string `json:"value"`
Timestamp *timestamp `json:"timestamp"`
}

type users map[string]user
type userList struct {
users map[string]user
showTimestamp bool
}

func (u users) output(format string) error {
func (u *userList) output(format string) error {
switch format {
case instagram.OutputTable:
return u.outputTable()
Expand All @@ -137,22 +167,30 @@ func (u users) output(format string) error {
}
}

func (u users) outputTable() error {
func (u *userList) outputTable() error {
var rows []table.Row
for _, user := range u {
rows = append(rows, table.Row{
for _, user := range u.users {
row := table.Row{
user.Username,
user.ProfileUrl,
})
}
if u.showTimestamp {
row = append(row, user.Timestamp)
}
rows = append(rows, row)
}
headers := table.Row{
instagram.TableHeaderUsername,
instagram.TableHeaderProfileUrl,
}
if u.showTimestamp {
headers = append(headers, instagram.TableHeaderTimestamp)
}
usersTable := table.NewWriter()
usersTable.SetAutoIndex(true)
usersTable.SetOutputMirror(os.Stdout)
usersTable.SetStyle(table.StyleBold)
usersTable.AppendHeader(table.Row{
instagram.TableHeaderUsername,
instagram.TableHeaderProfileUrl,
})
usersTable.AppendHeader(headers)
usersTable.AppendRows(rows)
usersTable.Render()
return nil
Expand Down
51 changes: 44 additions & 7 deletions pkg/instagram/followdata/followdata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,20 +228,23 @@ func Test_handler_Unfollowers(t *testing.T) {
}
}

func Test_users_output(t *testing.T) {
func Test_userList_output(t *testing.T) {
type args struct {
format string
}
u := users{
"username": {
ProfileUrl: "https://www.instagram.com/username",
Username: "username",
Timestamp: 0,
u := userList{
users: map[string]user{
"username": {
ProfileUrl: "https://www.instagram.com/username",
Username: "username",
Timestamp: &timestamp{},
},
},
showTimestamp: true,
}
tests := []struct {
name string
u users
u userList
args args
wantErr bool
}{
Expand Down Expand Up @@ -278,3 +281,37 @@ func Test_users_output(t *testing.T) {
})
}
}

func Test_timestamp_UnmarshalJSON(t *testing.T) {
type args struct {
b []byte
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "succeeds to unmarshal json",
args: args{
b: []byte("1697474963"),
},
wantErr: false,
},
{
name: "fails to unmarshal json",
args: args{
b: []byte("invalid"),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := &timestamp{}
if err := ts.UnmarshalJSON(tt.args.b); (err != nil) != tt.wantErr {
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

0 comments on commit 3ba2fad

Please sign in to comment.