This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / xrpc / xrpc.go
6.3 kB 184 lines
1package xrpc 2 3import ( 4 "encoding/json" 5 "log/slog" 6 "net/http" 7 "runtime/debug" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/go-chi/chi/v5" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/appview/cloudflare" 13 "tangled.org/core/appview/codesearch" 14 "tangled.org/core/appview/config" 15 "tangled.org/core/appview/db" 16 whnotify "tangled.org/core/appview/notify/webhook" 17 "tangled.org/core/idresolver" 18 xrpcerr "tangled.org/core/xrpc/errors" 19 "tangled.org/core/xrpc/serviceauth" 20) 21 22const ActorDid = serviceauth.ActorDid 23 24type Xrpc struct { 25 DB *db.DB 26 Config *config.Config 27 Logger *slog.Logger 28 ServiceAuth *serviceauth.ServiceAuth 29 IdResolver *idresolver.Resolver 30 Cloudflare *cloudflare.Client 31 CodeSearch *codesearch.CodeSearch 32 Webhooks *whnotify.Notifier 33 34 // reserved usernames rejected at signup completion 35 DisallowedNicknames map[string]bool 36} 37 38func (x *Xrpc) Router() http.Handler { 39 r := chi.NewRouter() 40 41 r.Use(x.cors) 42 43 // health check, atproto _health convention 44 r.Get("/_health", x.health) 45 46 // open endpoints: signup happens pre-identity, so no service auth 47 r.Post("/"+tangled.TempAccountBeginSignupNSID, x.AccountBeginSignup) 48 r.Post("/"+tangled.TempAccountCompleteSignupNSID, x.AccountCompleteSignup) 49 50 // authenticated endpoints 51 r.Group(func(r chi.Router) { 52 r.Use(x.ServiceAuth.VerifyServiceAuth) 53 54 // code search is gated on login, matching the appview ui 55 r.Get("/"+tangled.TempSearchSearchCodeNSID, x.SearchSearchCode) 56 57 // notifications 58 r.Get("/"+tangled.TempNotificationListNotificationsNSID, x.NotificationList) 59 r.Get("/"+tangled.TempNotificationGetUnreadCountNSID, x.NotificationGetUnreadCount) 60 r.Post("/"+tangled.TempNotificationUpdateSeenNSID, x.NotificationUpdateSeen) 61 r.Post("/"+tangled.TempNotificationMarkAllReadNSID, x.NotificationMarkAllRead) 62 r.Post("/"+tangled.TempNotificationDeleteNotificationNSID, x.NotificationDelete) 63 r.Get("/"+tangled.TempNotificationGetPreferencesNSID, x.NotificationGetPreferences) 64 r.Post("/"+tangled.TempNotificationUpdatePreferencesNSID, x.NotificationUpdatePreferences) 65 66 // focus mode 67 r.Post("/"+tangled.TempFocusBeginSessionNSID, x.FocusBegin) 68 r.Post("/"+tangled.TempFocusNextItemNSID, x.FocusNext) 69 r.Post("/"+tangled.TempFocusEndSessionNSID, x.FocusEnd) 70 71 // account management 72 r.Get("/"+tangled.TempAccountListEmailsNSID, x.AccountListEmails) 73 r.Post("/"+tangled.TempAccountDeleteEmailNSID, x.AccountDeleteEmail) 74 r.Post("/"+tangled.TempAccountSetPrimaryEmailNSID, x.AccountSetPrimaryEmail) 75 r.Post("/"+tangled.TempAccountSubscribeNewsletterNSID, x.AccountSubscribeNewsletter) 76 r.Post("/"+tangled.TempAccountDismissNewsletterNSID, x.AccountDismissNewsletter) 77 78 // webhooks 79 r.Get("/"+tangled.TempRepoListWebhooksNSID, x.WebhookList) 80 r.Post("/"+tangled.TempRepoCreateWebhookNSID, x.WebhookCreate) 81 r.Post("/"+tangled.TempRepoUpdateWebhookNSID, x.WebhookUpdate) 82 r.Post("/"+tangled.TempRepoDeleteWebhookNSID, x.WebhookDelete) 83 r.Post("/"+tangled.TempRepoToggleWebhookNSID, x.WebhookToggle) 84 r.Get("/"+tangled.TempRepoListWebhookDeliveriesNSID, x.WebhookListDeliveries) 85 r.Post("/"+tangled.TempRepoRetryWebhookDeliveryNSID, x.WebhookRetryDelivery) 86 87 // sites 88 r.Get("/"+tangled.TempSiteGetDomainClaimNSID, x.SiteGetDomainClaim) 89 r.Post("/"+tangled.TempSiteClaimDomainNSID, x.SiteClaimDomain) 90 r.Post("/"+tangled.TempSiteReleaseDomainNSID, x.SiteReleaseDomain) 91 r.Get("/"+tangled.TempRepoGetSiteConfigNSID, x.SiteGetRepoSiteConfig) 92 r.Post("/"+tangled.TempRepoUpdateSiteConfigNSID, x.SiteUpdateRepoSiteConfig) 93 r.Post("/"+tangled.TempRepoDisableSiteNSID, x.SiteDisableRepoSite) 94 }) 95 96 return r 97} 98 99// timeFormat is the datetime format used across lexicon output fields 100const timeFormat = "2006-01-02T15:04:05.000Z" 101 102// health responds to /xrpc/_health with the running version 103func (x *Xrpc) health(w http.ResponseWriter, r *http.Request) { 104 x.writeJSON(w, map[string]string{"version": serviceVersion()}) 105} 106 107// serviceVersion returns the build's vcs revision, or "dev" 108func serviceVersion() string { 109 info, ok := debug.ReadBuildInfo() 110 if !ok { 111 return "dev" 112 } 113 for _, s := range info.Settings { 114 if s.Key == "vcs.revision" && s.Value != "" { 115 return s.Value 116 } 117 } 118 return "dev" 119} 120 121// cors allows the browser origin to call the xrpc endpoints. auth is via 122// bearer tokens, not cookies, so a wildcard origin is safe. 123func (x *Xrpc) cors(next http.Handler) http.Handler { 124 origin := x.Config.Core.XrpcCorsOrigin 125 if origin == "" { 126 origin = "*" 127 } 128 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 129 w.Header().Set("Access-Control-Allow-Origin", origin) 130 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 131 w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 132 w.Header().Set("Access-Control-Max-Age", "86400") 133 if origin != "*" { 134 w.Header().Add("Vary", "Origin") 135 } 136 if r.Method == http.MethodOptions { 137 w.WriteHeader(http.StatusNoContent) 138 return 139 } 140 next.ServeHTTP(w, r) 141 }) 142} 143 144func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) { 145 w.Header().Set("Content-Type", "application/json") 146 w.WriteHeader(status) 147 json.NewEncoder(w).Encode(e) 148} 149 150func (x *Xrpc) writeJSON(w http.ResponseWriter, v any) { 151 w.Header().Set("Content-Type", "application/json") 152 json.NewEncoder(w).Encode(v) 153} 154 155func actorDid(r *http.Request) (string, bool) { 156 did, ok := r.Context().Value(ActorDid).(syntax.DID) 157 if !ok { 158 return "", false 159 } 160 return did.String(), true 161} 162 163// stable client-facing errors; handlers log the real cause and return these 164var ( 165 errInternal = xrpcErrorTag("InternalError", "internal server error") 166 errBadRequestBody = xrpcErrorTag("InvalidRequest", "invalid request body") 167 errUpstream = xrpcErrorTag("UpstreamError", "an upstream service failed") 168) 169 170func xrpcErrorTag(tag, message string) xrpcerr.XrpcError { 171 return xrpcerr.NewXrpcError(xrpcerr.WithTag(tag), xrpcerr.WithMessage(message)) 172} 173 174func badRequestError(message string) xrpcerr.XrpcError { 175 return xrpcErrorTag("InvalidRequest", message) 176} 177 178func notFoundError(message string) xrpcerr.XrpcError { 179 return xrpcErrorTag("NotFound", message) 180} 181 182func notImplementedError(message string) xrpcerr.XrpcError { 183 return xrpcErrorTag("MethodNotImplemented", message) 184}