apollo-backend/internal/api/watcher.go

381 lines
9.3 KiB
Go
Raw Normal View History

2021-09-25 16:56:01 +00:00
package api
import (
2022-11-01 02:33:11 +00:00
"context"
2021-09-25 16:56:01 +00:00
"encoding/json"
2022-05-21 14:00:21 +00:00
"errors"
"fmt"
2021-09-25 16:56:01 +00:00
"net/http"
"strconv"
2021-10-09 14:59:20 +00:00
"strings"
2022-03-28 21:05:01 +00:00
"time"
2021-09-25 16:56:01 +00:00
2022-03-12 17:50:05 +00:00
validation "github.com/go-ozzo/ozzo-validation/v4"
2021-09-25 16:56:01 +00:00
"github.com/gorilla/mux"
2022-03-12 17:50:05 +00:00
"github.com/christianselig/apollo-backend/internal/domain"
"github.com/christianselig/apollo-backend/internal/reddit"
2021-09-25 16:56:01 +00:00
)
type watcherCriteria struct {
2021-10-13 14:44:47 +00:00
Author string
Subreddit string
Upvotes int64
Keyword string
Flair string
Domain string
2021-09-25 16:56:01 +00:00
}
type createWatcherRequest struct {
2021-10-09 14:59:20 +00:00
Type string
User string
2021-09-25 16:56:01 +00:00
Subreddit string
2021-10-10 15:51:42 +00:00
Label string
2021-09-25 16:56:01 +00:00
Criteria watcherCriteria
}
2022-03-12 17:50:05 +00:00
func (cwr *createWatcherRequest) Validate() error {
return validation.ValidateStruct(cwr,
validation.Field(&cwr.Type, validation.Required),
validation.Field(&cwr.User, validation.Required.When(cwr.Type == "user")),
validation.Field(&cwr.Subreddit, validation.Required.When(cwr.Type == "subreddit" || cwr.Type == "trending")),
)
}
2021-10-13 19:11:27 +00:00
type watcherCreatedResponse struct {
ID int64 `json:"id"`
}
2021-09-25 16:56:01 +00:00
func (a *api) createWatcherHandler(w http.ResponseWriter, r *http.Request) {
2022-11-01 02:33:11 +00:00
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
2021-09-25 16:56:01 +00:00
vars := mux.Vars(r)
apns := vars["apns"]
redditID := vars["redditID"]
2021-09-25 18:02:00 +00:00
cwr := &createWatcherRequest{
2021-10-13 14:44:47 +00:00
Criteria: watcherCriteria{},
2021-09-25 18:02:00 +00:00
}
2021-09-25 16:56:01 +00:00
if err := json.NewDecoder(r.Body).Decode(cwr); err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 500, err)
2021-09-25 16:56:01 +00:00
return
}
2022-03-12 17:50:05 +00:00
if err := cwr.Validate(); err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 422, err)
2022-03-12 17:50:05 +00:00
return
}
2021-09-25 16:56:01 +00:00
dev, err := a.deviceRepo.GetByAPNSToken(ctx, apns)
if err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 422, err)
2021-09-25 16:56:01 +00:00
return
}
accs, err := a.accountRepo.GetByAPNSToken(ctx, apns)
2022-05-07 17:38:04 +00:00
if err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 422, err)
2021-09-25 16:56:01 +00:00
return
}
2022-05-07 17:38:04 +00:00
if len(accs) == 0 {
2022-05-21 14:00:21 +00:00
err := errors.New("cannot create watchers without account")
a.errorResponse(w, r, 422, err)
2022-05-07 17:38:23 +00:00
return
2022-05-07 17:38:04 +00:00
}
2021-09-25 16:56:01 +00:00
account := accs[0]
found := false
for _, acc := range accs {
if acc.AccountID == redditID {
found = true
account = acc
}
}
if !found {
2022-05-21 14:00:21 +00:00
err := errors.New("account not associated with device")
a.errorResponse(w, r, 401, err)
2021-09-25 16:56:01 +00:00
return
}
2021-10-09 14:59:20 +00:00
watcher := domain.Watcher{
2021-10-10 15:51:42 +00:00
Label: cwr.Label,
2021-10-09 14:59:20 +00:00
DeviceID: dev.ID,
AccountID: account.ID,
2021-10-10 15:51:42 +00:00
Author: strings.ToLower(cwr.Criteria.Author),
2021-10-13 14:44:47 +00:00
Subreddit: strings.ToLower(cwr.Criteria.Subreddit),
2021-10-09 14:59:20 +00:00
Upvotes: cwr.Criteria.Upvotes,
Keyword: strings.ToLower(cwr.Criteria.Keyword),
Flair: strings.ToLower(cwr.Criteria.Flair),
Domain: strings.ToLower(cwr.Criteria.Domain),
2021-09-25 16:56:01 +00:00
}
2021-10-10 15:51:42 +00:00
if cwr.Type == "subreddit" || cwr.Type == "trending" {
ac := a.reddit.NewAuthenticatedClient(account.AccountID, account.RefreshToken, account.AccessToken)
srr, err := ac.SubredditAbout(ctx, cwr.Subreddit)
if !srr.Public {
a.errorResponse(w, r, 403, reddit.ErrSubredditIsPrivate)
return
} else if err != nil {
switch err {
case reddit.ErrSubredditIsPrivate, reddit.ErrSubredditIsQuarantined:
2022-06-30 19:06:03 +00:00
err = fmt.Errorf("error watching %s: %w", cwr.Subreddit, err)
a.errorResponse(w, r, 403, err)
default:
a.errorResponse(w, r, 422, err)
}
2021-10-09 14:59:20 +00:00
return
}
sr, err := a.subredditRepo.GetByName(ctx, cwr.Subreddit)
if err != nil {
switch err {
case domain.ErrNotFound:
// Might be that we don't know about that subreddit yet
sr = domain.Subreddit{SubredditID: srr.ID, Name: srr.Name}
_ = a.subredditRepo.CreateOrUpdate(ctx, &sr)
default:
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 500, err)
2021-10-09 14:59:20 +00:00
return
}
}
2021-10-10 15:51:42 +00:00
switch cwr.Type {
case "subreddit":
watcher.Type = domain.SubredditWatcher
case "trending":
watcher.Label = "trending"
2021-10-10 15:51:42 +00:00
watcher.Type = domain.TrendingWatcher
}
2021-10-09 14:59:20 +00:00
watcher.WatcheeID = sr.ID
} else if cwr.Type == "user" {
ac := a.reddit.NewAuthenticatedClient(account.AccountID, account.RefreshToken, account.AccessToken)
urr, err := ac.UserAbout(ctx, cwr.User)
2021-10-09 14:59:20 +00:00
if err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 500, err)
2021-09-25 18:02:00 +00:00
return
}
2021-09-25 16:56:01 +00:00
2021-10-09 14:59:20 +00:00
if !urr.AcceptFollowers {
2022-05-21 14:00:21 +00:00
err := errors.New("user has followers disabled")
a.errorResponse(w, r, 403, err)
2021-10-09 14:59:20 +00:00
return
}
u := domain.User{UserID: urr.ID, Name: urr.Name}
err = a.userRepo.CreateOrUpdate(ctx, &u)
if err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 500, err)
2021-10-09 14:59:20 +00:00
return
}
watcher.Type = domain.UserWatcher
watcher.WatcheeID = u.ID
} else {
2022-05-21 14:00:21 +00:00
err := fmt.Errorf("unknown watcher type: %s", cwr.Type)
a.errorResponse(w, r, 422, err)
2021-10-09 14:59:20 +00:00
return
2021-09-25 16:56:01 +00:00
}
if err := a.watcherRepo.Create(ctx, &watcher); err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 422, err)
2021-09-25 16:56:01 +00:00
return
}
w.WriteHeader(http.StatusOK)
2021-10-13 19:11:27 +00:00
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(watcherCreatedResponse{ID: watcher.ID})
2021-09-25 16:56:01 +00:00
}
2021-10-10 15:51:42 +00:00
func (a *api) editWatcherHandler(w http.ResponseWriter, r *http.Request) {
2022-11-01 02:33:11 +00:00
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
2021-09-25 16:56:01 +00:00
vars := mux.Vars(r)
2022-05-21 14:26:23 +00:00
apns := vars["apns"]
wid := vars["watcherID"]
rid := vars["redditID"]
id, err := strconv.ParseInt(wid, 10, 64)
2021-09-25 16:56:01 +00:00
if err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 422, err)
2021-09-25 16:56:01 +00:00
return
}
2021-10-10 15:51:42 +00:00
watcher, err := a.watcherRepo.GetByID(ctx, id)
2022-05-21 14:00:21 +00:00
if err != nil {
a.errorResponse(w, r, 422, err)
return
} else if watcher.Device.APNSToken != vars["apns"] {
err := fmt.Errorf("wrong device for watcher %d", watcher.ID)
a.errorResponse(w, r, 422, err)
2021-10-10 15:51:42 +00:00
return
}
2021-09-25 16:56:01 +00:00
2021-10-10 15:51:42 +00:00
ewr := &createWatcherRequest{
2021-10-13 15:11:28 +00:00
Criteria: watcherCriteria{},
2021-10-10 15:51:42 +00:00
}
if err := json.NewDecoder(r.Body).Decode(ewr); err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 500, err)
2021-10-10 15:51:42 +00:00
return
}
watcher.Label = ewr.Label
2022-05-21 14:26:23 +00:00
watcher.Author = strings.ToLower(ewr.User)
watcher.Subreddit = strings.ToLower(ewr.Subreddit)
2021-10-10 15:51:42 +00:00
watcher.Upvotes = ewr.Criteria.Upvotes
2021-10-13 15:11:28 +00:00
watcher.Keyword = strings.ToLower(ewr.Criteria.Keyword)
watcher.Flair = strings.ToLower(ewr.Criteria.Flair)
watcher.Domain = strings.ToLower(ewr.Criteria.Domain)
2021-10-10 15:51:42 +00:00
2022-05-21 14:26:23 +00:00
if watcher.Type == domain.SubredditWatcher {
lsr := strings.ToLower(watcher.Subreddit)
if watcher.WatcheeLabel != lsr {
var account domain.Account
2022-05-21 14:26:23 +00:00
accs, err := a.accountRepo.GetByAPNSToken(ctx, apns)
if err != nil {
a.errorResponse(w, r, 422, err)
return
}
if len(accs) == 0 {
err := errors.New("cannot create watchers without account")
a.errorResponse(w, r, 422, err)
return
}
found := false
for _, acc := range accs {
if acc.AccountID == rid {
account = acc
2022-05-21 14:26:23 +00:00
found = true
}
}
if !found {
err := errors.New("account not associated with device")
a.errorResponse(w, r, 401, err)
return
}
ac := a.reddit.NewAuthenticatedClient(account.AccountID, account.RefreshToken, account.AccessToken)
srr, err := ac.SubredditAbout(ctx, lsr)
if !srr.Public {
a.errorResponse(w, r, 403, reddit.ErrSubredditIsPrivate)
return
} else if err != nil {
switch err {
case reddit.ErrSubredditIsPrivate, reddit.ErrSubredditIsQuarantined:
2022-06-30 16:11:07 +00:00
err = fmt.Errorf("error watching %s: %w", lsr, err)
a.errorResponse(w, r, 403, err)
default:
a.errorResponse(w, r, 422, err)
}
2022-05-21 14:26:23 +00:00
return
}
sr, err := a.subredditRepo.GetByName(ctx, lsr)
if err != nil {
switch err {
case domain.ErrNotFound:
// Might be that we don't know about that subreddit yet
sr = domain.Subreddit{SubredditID: srr.ID, Name: srr.Name}
_ = a.subredditRepo.CreateOrUpdate(ctx, &sr)
default:
a.errorResponse(w, r, 500, err)
return
}
}
watcher.WatcheeID = sr.ID
}
}
2021-10-10 15:51:42 +00:00
if err := a.watcherRepo.Update(ctx, &watcher); err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 500, err)
2021-10-10 15:51:42 +00:00
return
}
w.WriteHeader(http.StatusOK)
}
func (a *api) deleteWatcherHandler(w http.ResponseWriter, r *http.Request) {
2022-11-01 02:33:11 +00:00
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
2021-10-10 15:51:42 +00:00
vars := mux.Vars(r)
id, err := strconv.ParseInt(vars["watcherID"], 10, 64)
2021-09-25 16:56:01 +00:00
if err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 422, err)
2021-09-25 16:56:01 +00:00
return
}
watcher, err := a.watcherRepo.GetByID(ctx, id)
2022-05-21 14:00:21 +00:00
if err != nil {
a.errorResponse(w, r, 422, err)
return
} else if watcher.Device.APNSToken != vars["apns"] {
err := fmt.Errorf("wrong device for watcher %d", watcher.ID)
a.errorResponse(w, r, 422, err)
2021-09-25 16:56:01 +00:00
return
}
_ = a.watcherRepo.Delete(ctx, id)
w.WriteHeader(http.StatusOK)
}
2021-09-25 18:17:23 +00:00
type watcherItem struct {
2022-03-28 21:05:01 +00:00
ID int64 `json:"id"`
CreatedAt time.Time `json:"created_at"`
Type string `json:"type"`
Label string `json:"label"`
SourceLabel string `json:"source_label"`
2022-05-19 16:43:37 +00:00
Upvotes int64 `json:"upvotes,omitempty"`
2022-03-28 21:05:01 +00:00
Keyword string `json:"keyword,omitempty"`
Flair string `json:"flair,omitempty"`
Domain string `json:"domain,omitempty"`
Hits int64 `json:"hits"`
Author string `json:"author,omitempty"`
2021-09-25 18:17:23 +00:00
}
func (a *api) listWatchersHandler(w http.ResponseWriter, r *http.Request) {
2022-05-07 19:04:35 +00:00
ctx := r.Context()
2021-09-25 18:17:23 +00:00
vars := mux.Vars(r)
apns := vars["apns"]
redditID := vars["redditID"]
watchers, err := a.watcherRepo.GetByDeviceAPNSTokenAndAccountRedditID(ctx, apns, redditID)
if err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 400, err)
2021-09-25 18:17:23 +00:00
return
}
wis := make([]watcherItem, len(watchers))
for i, watcher := range watchers {
wi := watcherItem{
ID: watcher.ID,
CreatedAt: watcher.CreatedAt,
Type: watcher.Type.String(),
Label: watcher.Label,
SourceLabel: watcher.WatcheeLabel,
Keyword: watcher.Keyword,
Flair: watcher.Flair,
Domain: watcher.Domain,
Hits: watcher.Hits,
2022-03-12 17:50:05 +00:00
Author: watcher.Author,
2022-05-19 16:43:37 +00:00
Upvotes: watcher.Upvotes,
2021-09-25 18:17:23 +00:00
}
wis[i] = wi
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(wis)
}