apollo-backend/internal/api/api.go

207 lines
6.2 KiB
Go
Raw Permalink Normal View History

package api
import (
"context"
"fmt"
2022-03-12 17:50:05 +00:00
"net"
"net/http"
"os"
2021-08-08 18:19:47 +00:00
"time"
"github.com/DataDog/datadog-go/statsd"
2022-03-28 17:20:01 +00:00
"github.com/bugsnag/bugsnag-go/v2"
2022-03-12 17:50:05 +00:00
"github.com/go-redis/redis/v8"
2021-08-08 18:19:47 +00:00
"github.com/gorilla/mux"
"github.com/jackc/pgx/v4/pgxpool"
2021-08-08 18:19:47 +00:00
"github.com/sideshow/apns2/token"
2022-05-23 18:17:25 +00:00
"go.uber.org/zap"
2021-07-26 16:34:26 +00:00
"github.com/christianselig/apollo-backend/internal/domain"
"github.com/christianselig/apollo-backend/internal/reddit"
2021-07-26 16:34:26 +00:00
"github.com/christianselig/apollo-backend/internal/repository"
)
type api struct {
2022-05-23 18:17:25 +00:00
logger *zap.Logger
statsd *statsd.Client
reddit *reddit.Client
apns *token.Token
httpClient *http.Client
2021-07-26 16:34:26 +00:00
2021-09-25 16:56:01 +00:00
accountRepo domain.AccountRepository
deviceRepo domain.DeviceRepository
subredditRepo domain.SubredditRepository
watcherRepo domain.WatcherRepository
2021-10-09 14:59:20 +00:00
userRepo domain.UserRepository
}
2022-05-23 18:17:25 +00:00
func NewAPI(ctx context.Context, logger *zap.Logger, statsd *statsd.Client, redis *redis.Client, pool *pgxpool.Pool) *api {
reddit := reddit.NewClient(
os.Getenv("REDDIT_CLIENT_ID"),
os.Getenv("REDDIT_CLIENT_SECRET"),
statsd,
2022-03-12 17:50:05 +00:00
redis,
2021-07-14 00:09:44 +00:00
16,
)
2021-08-08 18:19:47 +00:00
var apns *token.Token
{
authKey, err := token.AuthKeyFromFile(os.Getenv("APPLE_KEY_PATH"))
if err != nil {
panic(err)
}
apns = &token.Token{
AuthKey: authKey,
KeyID: os.Getenv("APPLE_KEY_ID"),
TeamID: os.Getenv("APPLE_TEAM_ID"),
}
}
2021-07-26 16:34:26 +00:00
accountRepo := repository.NewPostgresAccount(pool)
deviceRepo := repository.NewPostgresDevice(pool)
2021-09-25 16:56:01 +00:00
subredditRepo := repository.NewPostgresSubreddit(pool)
watcherRepo := repository.NewPostgresWatcher(pool)
2021-10-09 14:59:20 +00:00
userRepo := repository.NewPostgresUser(pool)
client := &http.Client{}
2021-07-26 16:34:26 +00:00
return &api{
logger: logger,
statsd: statsd,
reddit: reddit,
apns: apns,
httpClient: client,
2021-09-25 16:56:01 +00:00
accountRepo: accountRepo,
deviceRepo: deviceRepo,
subredditRepo: subredditRepo,
watcherRepo: watcherRepo,
2021-10-09 14:59:20 +00:00
userRepo: userRepo,
2021-07-26 16:34:26 +00:00
}
}
func (a *api) Server(port int) *http.Server {
return &http.Server{
Addr: fmt.Sprintf(":%d", port),
2022-03-28 17:20:01 +00:00
Handler: bugsnag.Handler(a.Routes()),
}
}
2021-08-08 18:19:47 +00:00
func (a *api) Routes() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/v1/health", a.healthCheckHandler).Methods("GET")
r.HandleFunc("/v1/device", a.upsertDeviceHandler).Methods("POST")
r.HandleFunc("/v1/device/{apns}", a.deleteDeviceHandler).Methods("DELETE")
r.HandleFunc("/v1/device/{apns}/test", a.testDeviceHandler).Methods("POST")
2022-05-07 16:53:42 +00:00
r.HandleFunc("/v1/device/{apns}/test/comment_reply", generateNotificationTester(a, commentReply)).Methods("POST")
r.HandleFunc("/v1/device/{apns}/test/post_reply", generateNotificationTester(a, postReply)).Methods("POST")
r.HandleFunc("/v1/device/{apns}/test/private_message", generateNotificationTester(a, privateMessage)).Methods("POST")
r.HandleFunc("/v1/device/{apns}/test/subreddit_watcher", generateNotificationTester(a, subredditWatcher)).Methods("POST")
r.HandleFunc("/v1/device/{apns}/test/trending_post", generateNotificationTester(a, trendingPost)).Methods("POST")
r.HandleFunc("/v1/device/{apns}/test/username_mention", generateNotificationTester(a, usernameMention)).Methods("POST")
2021-08-08 18:19:47 +00:00
r.HandleFunc("/v1/device/{apns}/account", a.upsertAccountHandler).Methods("POST")
r.HandleFunc("/v1/device/{apns}/accounts", a.upsertAccountsHandler).Methods("POST")
r.HandleFunc("/v1/device/{apns}/account/{redditID}", a.disassociateAccountHandler).Methods("DELETE")
r.HandleFunc("/v1/device/{apns}/account/{redditID}/notifications", a.notificationsAccountHandler).Methods("PATCH")
2022-03-12 17:50:05 +00:00
r.HandleFunc("/v1/device/{apns}/account/{redditID}/notifications", a.getNotificationsAccountHandler).Methods("GET")
2021-08-08 18:19:47 +00:00
2021-09-25 18:17:23 +00:00
r.HandleFunc("/v1/device/{apns}/account/{redditID}/watcher", a.createWatcherHandler).Methods("POST")
r.HandleFunc("/v1/device/{apns}/account/{redditID}/watcher/{watcherID}", a.deleteWatcherHandler).Methods("DELETE")
2021-10-10 15:51:42 +00:00
r.HandleFunc("/v1/device/{apns}/account/{redditID}/watcher/{watcherID}", a.editWatcherHandler).Methods("PATCH")
r.HandleFunc("/v1/device/{apns}/account/{redditID}/watchers", a.listWatchersHandler).Methods("GET")
2021-09-25 16:56:01 +00:00
2021-08-14 15:51:27 +00:00
r.HandleFunc("/v1/receipt", a.checkReceiptHandler).Methods("POST")
r.HandleFunc("/v1/receipt/{apns}", a.checkReceiptHandler).Methods("POST")
2021-08-08 18:19:47 +00:00
2022-05-01 17:57:30 +00:00
r.HandleFunc("/v1/contact", a.contactHandler).Methods("POST")
2022-03-28 17:39:19 +00:00
r.HandleFunc("/v1/test/bugsnag", a.testBugsnagHandler).Methods("POST")
2021-08-08 18:19:47 +00:00
r.Use(a.loggingMiddleware)
return r
}
2022-03-28 17:39:19 +00:00
func (a *api) testBugsnagHandler(w http.ResponseWriter, r *http.Request) {
if err := bugsnag.Notify(fmt.Errorf("Test error")); err != nil {
2022-05-21 14:00:21 +00:00
a.errorResponse(w, r, 500, err)
2022-03-28 17:39:19 +00:00
return
}
w.WriteHeader(http.StatusOK)
}
2021-08-08 18:19:47 +00:00
type LoggingResponseWriter struct {
w http.ResponseWriter
statusCode int
bytes int
}
func (lrw *LoggingResponseWriter) Header() http.Header {
return lrw.w.Header()
}
func (lrw *LoggingResponseWriter) Write(bb []byte) (int, error) {
wb, err := lrw.w.Write(bb)
lrw.bytes += wb
return wb, err
}
func (lrw *LoggingResponseWriter) WriteHeader(statusCode int) {
lrw.w.WriteHeader(statusCode)
lrw.statusCode = statusCode
}
2021-08-08 18:19:47 +00:00
func (a *api) loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2021-08-14 15:34:32 +00:00
// Skip logging health checks
if r.RequestURI == "/v1/health" {
next.ServeHTTP(w, r)
return
}
2021-08-08 18:19:47 +00:00
start := time.Now()
lrw := &LoggingResponseWriter{w: w}
2022-06-25 18:12:56 +00:00
2021-08-08 18:19:47 +00:00
// Call the next handler, which can be another middleware in the chain, or the final handler.
next.ServeHTTP(lrw, r)
2022-06-25 18:12:56 +00:00
duration := time.Since(start).Milliseconds()
2022-03-12 17:50:05 +00:00
remoteAddr := r.Header.Get("X-Forwarded-For")
if remoteAddr == "" {
if ip, _, err := net.SplitHostPort(r.RemoteAddr); err != nil {
remoteAddr = "unknown"
} else {
remoteAddr = ip
}
}
2022-05-23 18:17:25 +00:00
fields := []zap.Field{
2022-06-25 18:12:56 +00:00
zap.Int64("duration", duration),
2022-05-23 18:17:25 +00:00
zap.String("method", r.Method),
zap.String("remote#addr", remoteAddr),
zap.Int("response#bytes", lrw.bytes),
zap.Int("status", lrw.statusCode),
zap.String("uri", r.RequestURI),
}
2021-08-08 18:19:47 +00:00
if lrw.statusCode == 200 {
2022-05-23 18:17:25 +00:00
a.logger.Info("", fields...)
2021-08-08 18:19:47 +00:00
} else {
err := lrw.Header().Get("X-Apollo-Error")
2022-05-23 18:17:25 +00:00
a.logger.Error(err, fields...)
2021-08-08 18:19:47 +00:00
}
2022-06-25 18:12:56 +00:00
tags := []string{fmt.Sprintf("status:%d", lrw.statusCode)}
2022-06-28 17:13:42 +00:00
_ = a.statsd.Histogram("api.latency", float64(duration), nil, 1.0)
_ = a.statsd.Incr("api.calls", tags, 1.0)
2022-06-25 18:12:56 +00:00
if lrw.statusCode >= 500 {
2022-06-28 17:13:42 +00:00
_ = a.statsd.Incr("api.errors", nil, 1.0)
2022-06-25 18:12:56 +00:00
}
2021-08-08 18:19:47 +00:00
})
}