package server import ( "context" "errors" "net/http" "net/url" "sync" "github.com/0x2E/feedfinder" "github.com/0x2e/fusion/model" "github.com/0x2e/fusion/repo" "github.com/0x2e/fusion/service/pull" "github.com/0x2e/fusion/service/pull/client" ) type FeedRepo interface { List(filter *repo.FeedListFilter) ([]*model.Feed, error) Get(id uint) (*model.Feed, error) Create(feed []*model.Feed) error Update(id uint, feed *model.Feed) error Delete(id uint) error } type Feed struct { repo FeedRepo } func NewFeed(repo FeedRepo) *Feed { return &Feed{ repo: repo, } } func (f Feed) List(ctx context.Context, req *ReqFeedList) (*RespFeedList, error) { filter := &repo.FeedListFilter{ HaveUnread: req.HaveUnread, HaveBookmark: req.HaveBookmark, } data, err := f.repo.List(filter) if err != nil { return nil, err } feeds := make([]*FeedForm, 0, len(data)) for _, v := range data { feeds = append(feeds, &FeedForm{ ID: v.ID, Name: v.Name, Link: v.Link, Failure: v.Failure, Suspended: v.Suspended, ReqProxy: v.ReqProxy, UpdatedAt: v.UpdatedAt, UnreadCount: v.UnreadCount, Group: GroupForm{ID: v.GroupID, Name: v.Group.Name}, }) } return &RespFeedList{ Feeds: feeds, }, nil } func (f Feed) Get(ctx context.Context, req *ReqFeedGet) (*RespFeedGet, error) { data, err := f.repo.Get(req.ID) if err != nil { return nil, err } return &RespFeedGet{ ID: data.ID, Name: data.Name, Link: data.Link, Failure: data.Failure, Suspended: data.Suspended, ReqProxy: data.ReqProxy, UpdatedAt: data.UpdatedAt, Group: GroupForm{ID: data.GroupID, Name: data.Group.Name}, }, nil } func (f Feed) Create(ctx context.Context, req *ReqFeedCreate) (*RespFeedCreate, error) { feeds := make([]*model.Feed, 0, len(req.Feeds)) for _, r := range req.Feeds { feeds = append(feeds, &model.Feed{ Name: r.Name, Link: r.Link, FeedRequestOptions: model.FeedRequestOptions{ ReqProxy: r.RequestOptions.Proxy, }, GroupID: req.GroupID, }) } if err := f.repo.Create(feeds); err != nil { return nil, err } // GORM assigns the ID to the model after Create ids := make([]uint, 0, len(feeds)) for _, v := range feeds { ids = append(ids, v.ID) } resp := &RespFeedCreate{ IDs: ids, } puller := pull.NewPuller(repo.NewFeed(repo.DB), repo.NewItem(repo.DB)) if len(feeds) > 1 { go func() { routinePool := make(chan struct{}, 10) defer close(routinePool) wg := sync.WaitGroup{} for _, feed := range feeds { routinePool <- struct{}{} wg.Add(1) go func() { // NOTE: do not use the incoming ctx, as it will be Done() automatically // by api timeout middleware puller.PullOne(context.Background(), feed.ID) <-routinePool wg.Done() }() } wg.Wait() }() return resp, nil } return resp, puller.PullOne(ctx, feeds[0].ID) } func (f Feed) CheckValidity(ctx context.Context, req *ReqFeedCheckValidity) (*RespFeedCheckValidity, error) { if title, err := client.NewFeedClient().FetchTitle(ctx, req.Link, model.FeedRequestOptions{ReqProxy: req.RequestOptions.Proxy}); err == nil { return &RespFeedCheckValidity{ FeedLinks: []ValidityItem{ { Title: &title, Link: &req.Link, }, }, }, nil } validLinks := make([]ValidityItem, 0) target, err := url.Parse(req.Link) if err != nil { return nil, err } sniffed, err := feedfinder.Find(ctx, target.String(), &feedfinder.Options{ RequestProxy: req.RequestOptions.Proxy, }) if err != nil { return nil, err } for _, l := range sniffed { validLinks = append(validLinks, ValidityItem{ Title: &l.Title, Link: &l.Link, }) } return &RespFeedCheckValidity{ FeedLinks: validLinks, }, nil } func (f Feed) Update(ctx context.Context, req *ReqFeedUpdate) error { data := &model.Feed{ Name: req.Name, Link: req.Link, Suspended: req.Suspended, FeedRequestOptions: model.FeedRequestOptions{ ReqProxy: req.ReqProxy, }, } if req.GroupID != nil { data.GroupID = *req.GroupID } err := f.repo.Update(req.ID, data) if errors.Is(err, repo.ErrDuplicatedKey) { err = NewBizError(err, http.StatusBadRequest, "link is not allowed to be the same as other feeds") } return err } func (f Feed) Delete(ctx context.Context, req *ReqFeedDelete) error { return f.repo.Delete(req.ID) } func (f Feed) Refresh(ctx context.Context, req *ReqFeedRefresh) error { pull := pull.NewPuller(repo.NewFeed(repo.DB), repo.NewItem(repo.DB)) if req.ID != nil { return pull.PullOne(ctx, *req.ID) } if req.All != nil && *req.All { // NOTE: do not use the incoming ctx, as it will be Done() automatically // by api timeout middleware go pull.PullAll(context.Background(), true) } return nil }