diff --git a/cmd/pasty/main.go b/cmd/pasty/main.go index 070fa49..d7ef5ec 100644 --- a/cmd/pasty/main.go +++ b/cmd/pasty/main.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/lus/pasty/internal/config" "github.com/lus/pasty/internal/meta" + "github.com/lus/pasty/internal/reports" "github.com/lus/pasty/internal/storage" "github.com/lus/pasty/internal/storage/postgres" "github.com/lus/pasty/internal/storage/sqlite" @@ -74,10 +75,6 @@ func main() { // Start the web server log.Info().Str("address", cfg.WebAddress).Msg("Starting the web server...") - var adminTokens []string - if cfg.ModificationTokenMaster != "" { - adminTokens = []string{cfg.ModificationTokenMaster} - } webServer := &web.Server{ Address: cfg.WebAddress, Storage: driver, @@ -88,7 +85,15 @@ func main() { ModificationTokensEnabled: cfg.ModificationTokens, ModificationTokenLength: cfg.ModificationTokenLength, ModificationTokenCharset: cfg.ModificationTokenCharacters, - AdminTokens: adminTokens, + } + if cfg.Reports.Enabled { + webServer.ReportClient = &reports.Client{ + WebhookURL: cfg.Reports.WebhookURL, + WebhookToken: cfg.Reports.WebhookToken, + } + } + if cfg.ModificationTokenMaster != "" { + webServer.AdminTokens = []string{cfg.ModificationTokenMaster} } go func() { if err := webServer.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) { diff --git a/internal/reports/client.go b/internal/reports/client.go new file mode 100644 index 0000000..10982af --- /dev/null +++ b/internal/reports/client.go @@ -0,0 +1,60 @@ +package reports + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type Report struct { + Paste string `json:"paste"` + Reason string `json:"reason"` +} + +type Response struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +type Client struct { + WebhookURL string + WebhookToken string +} + +func (client *Client) Send(report *Report) (*Response, error) { + data, err := json.Marshal(report) + if err != nil { + return nil, err + } + + request, err := http.NewRequest(http.MethodPost, client.WebhookURL, bytes.NewReader(data)) + if err != nil { + return nil, err + } + + if client.WebhookToken != "" { + request.Header.Set("Authorization", "Bearer "+client.WebhookToken) + } + + response, err := http.DefaultClient.Do(request) + if err != nil { + return nil, err + } + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + if response.StatusCode < 200 || response.StatusCode > 299 { + return nil, fmt.Errorf("the report webhook responded with an unexpected error: %d (%s)", response.StatusCode, string(body)) + } + + reportResponse := new(Response) + if err := json.Unmarshal(body, &reportResponse); err != nil { + return nil, err + } + return reportResponse, nil +} diff --git a/internal/web/server.go b/internal/web/server.go index 040965d..a95eeb2 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -5,6 +5,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/lus/pasty/internal/meta" "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/reports" "github.com/lus/pasty/internal/storage" "net/http" ) @@ -16,6 +17,10 @@ type Server struct { // The storage driver to use. Storage storage.Driver + // The report client to use to send reports. + // If this is set to nil, the report system will be considered deactivated. + ReportClient *reports.Client + // Whether the Hastebin support should be enabled. // If this is set to 'false', the Hastebin specific endpoints will not be registered. HastebinSupport bool @@ -63,12 +68,15 @@ func (server *Server) Start() error { router.Post("/api/v2/pastes", server.v2EndpointCreatePaste) router.With(server.v2MiddlewareInjectPaste, server.v2MiddlewareAuthorize).Patch("/api/v2/pastes/{paste_id}", server.v2EndpointModifyPaste) router.With(server.v2MiddlewareInjectPaste, server.v2MiddlewareAuthorize).Delete("/api/v2/pastes/{paste_id}", server.v2EndpointDeletePaste) + if server.ReportClient != nil { + router.With(server.v2MiddlewareInjectPaste).Post("/api/v2/pastes/{paste_id}/report", server.v2EndpointReportPaste) + } router.Get("/api/v2/info", func(writer http.ResponseWriter, request *http.Request) { writeJSONOrErr(writer, http.StatusOK, map[string]any{ "version": meta.Version, "modificationTokens": server.ModificationTokensEnabled, - "reports": false, // TODO: Return report state - "pasteLifetime": -1, // TODO: Return paste lifetime + "reports": server.ReportClient != nil, + "pasteLifetime": -1, // TODO: Return paste lifetime }) }) diff --git a/internal/web/v2_end_report_paste.go b/internal/web/v2_end_report_paste.go new file mode 100644 index 0000000..186c88e --- /dev/null +++ b/internal/web/v2_end_report_paste.go @@ -0,0 +1,48 @@ +package web + +import ( + "encoding/json" + "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/reports" + "io" + "net/http" +) + +type v2EndpointReportPastePayload struct { + Reason string `json:"reason"` +} + +func (server *Server) v2EndpointReportPaste(writer http.ResponseWriter, request *http.Request) { + paste, ok := request.Context().Value("paste").(*pastes.Paste) + if !ok { + writeString(writer, http.StatusInternalServerError, "missing paste object") + return + } + + // Read, parse and validate the request payload + body, err := io.ReadAll(request.Body) + if err != nil { + writeErr(writer, err) + return + } + payload := new(v2EndpointReportPastePayload) + if err := json.Unmarshal(body, payload); err != nil { + writeErr(writer, err) + return + } + if payload.Reason == "" { + writeString(writer, http.StatusBadRequest, "missing report reason") + return + } + + report := &reports.Report{ + Paste: paste.ID, + Reason: payload.Reason, + } + response, err := server.ReportClient.Send(report) + if err != nil { + writeErr(writer, err) + return + } + writeJSONOrErr(writer, http.StatusOK, response) +}