Skip to content

Commit 0a2e1e3

Browse files
committed
feat: add github stars module
- module that fetches stars from github
1 parent 69403d5 commit 0a2e1e3

File tree

2 files changed

+250
-0
lines changed

2 files changed

+250
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- github module: periodic import of starred repos
1213
- import bookmarks from Pocket csv export with `buku import pocket`
1314
- cli: `--slient` and `-S` to fully disable any log
1415
- Added support for brave browser (linux, snap, flatpak)

mods/github/mod.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// mod-gh-stars
2+
// This gosuki module allows access to the stars and lists from the user's Github
3+
// profile.
4+
5+
// # Github Personal Token:
6+
// This module requires a github personal token to be able to access the user's profile.
7+
// See full documentation at: https://gosuki.net/docs/configuration/config-file/#github-stars
8+
package stars
9+
10+
import (
11+
"context"
12+
"errors"
13+
"fmt"
14+
"os"
15+
"sync"
16+
"time"
17+
18+
"github.com/blob42/gosuki"
19+
"github.com/blob42/gosuki/pkg/config"
20+
"github.com/blob42/gosuki/pkg/events"
21+
"github.com/blob42/gosuki/pkg/logging"
22+
"github.com/blob42/gosuki/pkg/modules"
23+
"github.com/blob42/gosuki/pkg/watch"
24+
25+
"github.com/google/go-github/v66/github"
26+
"golang.org/x/time/rate"
27+
)
28+
29+
var (
30+
GHToken string
31+
Config *StarFetcherConfig
32+
log = logging.GetLogger(ModID)
33+
repoChan chan *github.StarredRepository
34+
errChan = make(chan error, 10)
35+
wg sync.WaitGroup
36+
sfModel *StarFetcherModel
37+
)
38+
39+
const (
40+
ModID = "github-stars"
41+
42+
// default sync interval in seconds
43+
DefaultSyncInterval = 6 * time.Hour
44+
45+
pageSize = 200
46+
)
47+
48+
type StarFetcherConfig struct {
49+
GithubToken string `toml:"github-token" mapstructure:"github-token"`
50+
SyncInterval time.Duration `toml:"sync-interval" mapstructure:"sync-interval"`
51+
}
52+
53+
func NewStarFetcherConfig() *StarFetcherConfig {
54+
return &StarFetcherConfig{
55+
SyncInterval: DefaultSyncInterval,
56+
}
57+
}
58+
59+
type StarFetcherModel struct {
60+
// Github API token
61+
62+
ctx context.Context
63+
64+
token string
65+
gh *github.Client
66+
67+
tui bool
68+
}
69+
70+
// This is the module struct. Used to implement module interface
71+
type StarFetcher struct{}
72+
73+
// NOTE: use Init() to obtain context config params and store them in a model object
74+
func (sf *StarFetcher) Init(ctx *modules.Context) error {
75+
sfModel.ctx = ctx.Context
76+
sfModel.tui = ctx.IsTUI
77+
78+
// Check if the environment variable GH_API_TOKEN is set
79+
if len(Config.GithubToken) == 0 {
80+
if sfModel.token = os.Getenv("GS_GITHUB_TOKEN"); sfModel.token == "" {
81+
return errors.New("no github token in config and GS_GITHUB_TOKEN environment variable not set")
82+
}
83+
} else {
84+
sfModel.token = Config.GithubToken
85+
}
86+
87+
sfModel.gh = github.NewClient(nil).WithAuthToken(sfModel.token)
88+
89+
return nil
90+
}
91+
92+
func (sf StarFetcher) ModInfo() modules.ModInfo {
93+
94+
return modules.ModInfo{
95+
ID: modules.ModID(ModID),
96+
New: func() modules.Module {
97+
return &StarFetcher{}
98+
},
99+
}
100+
}
101+
102+
func (sf *StarFetcher) GetStarredRepos(done *sync.WaitGroup) {
103+
defer done.Done()
104+
ctx := sfModel.ctx
105+
gh := sfModel.gh
106+
107+
opts := &github.ActivityListStarredOptions{
108+
ListOptions: github.ListOptions{PerPage: pageSize},
109+
}
110+
111+
limiter := rate.NewLimiter(rate.Every(time.Second), 10)
112+
113+
total := 0
114+
curPage := 0
115+
for {
116+
log.Debug("fetching starred repos", "page", opts.Page)
117+
118+
err := limiter.Wait(ctx)
119+
if err != nil {
120+
errChan <- err
121+
}
122+
123+
repos, resp, err := gh.Activity.ListStarred(ctx, "", opts)
124+
if err != nil {
125+
errChan <- err
126+
return
127+
}
128+
129+
log.Info("github ratelimit", "limit", resp.Rate.Limit, "remainging", resp.Rate.Remaining)
130+
131+
wg.Add(1)
132+
go func() {
133+
defer wg.Done()
134+
for _, repo := range repos {
135+
repoChan <- repo
136+
}
137+
}()
138+
139+
total += len(repos)
140+
141+
if resp.NextPage == 0 {
142+
break
143+
}
144+
145+
opts.Page = resp.NextPage
146+
147+
curPage++
148+
}
149+
150+
wg.Wait()
151+
close(repoChan)
152+
errChan <- nil
153+
}
154+
155+
func (sf *StarFetcher) Fetch() ([]*gosuki.Bookmark, error) {
156+
gh := sfModel.gh
157+
ctx := sfModel.ctx
158+
159+
waitStars := &sync.WaitGroup{}
160+
log.Info("fetching stars")
161+
bookmarks := make([]*gosuki.Bookmark, 0, 100)
162+
163+
// figuring out the total number of stars the user has
164+
// https://stackoverflow.com/questions/30636798/get-user-total-starred-count-using-github-api-v3
165+
166+
opts := &github.ActivityListStarredOptions{
167+
ListOptions: github.ListOptions{PerPage: 1, Page: 0},
168+
}
169+
_, resp, err := gh.Activity.ListStarred(ctx, "", opts)
170+
if err != nil {
171+
return nil, err
172+
}
173+
174+
nStars := resp.LastPage
175+
176+
// use a channel big enough to hold all stars and avoid blocking repoChan
177+
repoChan = make(chan *github.StarredRepository, nStars+pageSize)
178+
179+
waitStars.Add(1)
180+
go sf.GetStarredRepos(waitStars)
181+
182+
count := 0
183+
184+
for repo := range repoChan {
185+
//DEBUG:
186+
// pretty.Println(repo)
187+
188+
bk := gosuki.Bookmark{Module: string(sf.ModInfo().ID)}
189+
190+
if repo.Repository.CloneURL != nil {
191+
bk.URL = *repo.Repository.CloneURL
192+
}
193+
194+
if repo.Repository.Description != nil {
195+
bk.Desc = *repo.Repository.Description
196+
}
197+
198+
if repo.Repository.Name != nil {
199+
bk.Title = *repo.Repository.Name
200+
}
201+
202+
bk.Tags = repo.Repository.Topics
203+
204+
bookmarks = append(bookmarks, &bk)
205+
count++
206+
207+
//NOTE: this is not part of the final API for plugin/module developers
208+
//and is only used to display a progress bar on the TUI. It will be
209+
//abstracted away later on. You don't have to implement it.
210+
if sfModel.tui {
211+
go func() {
212+
events.TUIBus <- events.ProgressUpdateMsg{
213+
ID: ModID,
214+
Instance: nil,
215+
CurrentCount: uint(count),
216+
Total: uint(len(bookmarks)),
217+
}
218+
}()
219+
}
220+
//ENDNOTE
221+
}
222+
223+
if err = <-errChan; err != nil {
224+
return nil, fmt.Errorf("fetching stars: %w", err)
225+
}
226+
227+
waitStars.Wait()
228+
229+
// bookmarks will be handled by the module runner
230+
return bookmarks, nil
231+
}
232+
233+
// Interval at which the module should be run
234+
func (sf StarFetcher) Interval() time.Duration {
235+
return Config.SyncInterval
236+
}
237+
238+
func init() {
239+
sfModel = &StarFetcherModel{}
240+
241+
// Custom Config
242+
Config = NewStarFetcherConfig()
243+
config.RegisterConfigurator(ModID, config.AsConfigurator(Config))
244+
modules.RegisterModule(&StarFetcher{})
245+
}
246+
247+
// interface guards
248+
var _ watch.Poller = (*StarFetcher)(nil)
249+
var _ modules.Initializer = (*StarFetcher)(nil)

0 commit comments

Comments
 (0)