diff options
| author | 2025-08-12 14:05:15 +0200 | |
|---|---|---|
| committer | 2025-08-12 14:05:15 +0200 | |
| commit | 660cf2c94ce6a87ac33d704ab1f68b2d4a258d92 (patch) | |
| tree | 03eeab55b2f3bc2f3fc7667d245ed3b561a4f1a4 /internal/api/client | |
| parent | [docs] Revamp trusted proxies warning a bit (#4365) (diff) | |
| download | gotosocial-660cf2c94ce6a87ac33d704ab1f68b2d4a258d92.tar.xz | |
[feature] scheduled statuses (#4274)
An implementation of [`scheduled_statuses`](https://docs.joinmastodon.org/methods/scheduled_statuses/). Will fix #1006.
this is heavily WIP and I need to reorganize some of the code, working on this made me somehow familiar with the codebase and led to my other recent contributions
i told some fops on fedi i'd work on this so i have no choice but to complete it 🤷‍♀️
btw iirc my avatar presents me working on this branch
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4274
Co-authored-by: nicole mikołajczyk <git@mkljczk.pl>
Co-committed-by: nicole mikołajczyk <git@mkljczk.pl>
Diffstat (limited to 'internal/api/client')
7 files changed, 533 insertions, 5 deletions
diff --git a/internal/api/client/scheduledstatuses/scheduledstatus.go b/internal/api/client/scheduledstatuses/scheduledstatus.go new file mode 100644 index 000000000..4710640ff --- /dev/null +++ b/internal/api/client/scheduledstatuses/scheduledstatus.go @@ -0,0 +1,52 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package scheduledstatuses + +import ( + "net/http" + + "code.superseriousbusiness.org/gotosocial/internal/processing" + "github.com/gin-gonic/gin" +) + +const ( + // IDKey is for status UUIDs + IDKey = "id" + // BasePath is the base path for serving the scheduled statuses API, minus the 'api' prefix + BasePath = "/v1/scheduled_statuses" + // BasePathWithID is just the base path with the ID key in it. + // Use this anywhere you need to know the ID of the scheduled status being queried. + BasePathWithID = BasePath + "/:" + IDKey +) + +type Module struct { + processor *processing.Processor +} + +func New(processor *processing.Processor) *Module { + return &Module{ + processor: processor, + } +} + +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.ScheduledStatusesGETHandler) + attachHandler(http.MethodGet, BasePathWithID, m.ScheduledStatusGETHandler) + attachHandler(http.MethodPut, BasePathWithID, m.ScheduledStatusPUTHandler) + attachHandler(http.MethodDelete, BasePathWithID, m.ScheduledStatusDELETEHandler) +} diff --git a/internal/api/client/scheduledstatuses/scheduledstatusdelete.go b/internal/api/client/scheduledstatuses/scheduledstatusdelete.go new file mode 100644 index 000000000..efd645e2a --- /dev/null +++ b/internal/api/client/scheduledstatuses/scheduledstatusdelete.go @@ -0,0 +1,97 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package scheduledstatuses + +import ( + "net/http" + + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + + "github.com/gin-gonic/gin" +) + +// ScheduledStatusDELETEHandler swagger:operation DELETE /api/v1/scheduled_statuses/{id} deleteScheduledStatus +// +// Cancel a scheduled status with the given id. +// +// --- +// tags: +// - scheduled_statuses +// +// produces: +// - application/json +// +// parameters: +// - +// name: id +// type: string +// description: ID of the status +// in: path +// required: true +// +// security: +// - OAuth2 Bearer: +// - write:statuses +// +// responses: +// '200': +// description: status canceled +// '400': +// description: bad request +// '401': +// description: unauthorized +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ScheduledStatusDELETEHandler(c *gin.Context) { + authed, errWithCode := apiutil.TokenAuth(c, + true, true, true, true, + apiutil.ScopeWriteStatuses, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + targetScheduledStatusID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + errWithCode = m.processor.Status().ScheduledStatusesDelete( + c.Request.Context(), + authed.Account, + targetScheduledStatusID, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + c.JSON(http.StatusOK, apiutil.EmptyJSONObject) +} diff --git a/internal/api/client/scheduledstatuses/scheduledstatusesget.go b/internal/api/client/scheduledstatuses/scheduledstatusesget.go new file mode 100644 index 000000000..6d08ff18b --- /dev/null +++ b/internal/api/client/scheduledstatuses/scheduledstatusesget.go @@ -0,0 +1,136 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package scheduledstatuses + +import ( + "net/http" + + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + "code.superseriousbusiness.org/gotosocial/internal/paging" + "github.com/gin-gonic/gin" +) + +// ScheduledStatusesGETHandler swagger:operation GET /api/v1/scheduled_statuses getScheduledStatuses +// +// Get an array of statuses scheduled by authorized user. +// +// --- +// tags: +// - scheduled_statuses +// +// produces: +// - application/json +// +// parameters: +// - +// name: max_id +// type: string +// description: >- +// Return only statuses *OLDER* than the given max status ID. +// The status with the specified ID will not be included in the response. +// in: query +// required: false +// - +// name: since_id +// type: string +// description: >- +// Return only statuses *newer* than the given since status ID. +// The status with the specified ID will not be included in the response. +// in: query +// - +// name: min_id +// type: string +// description: >- +// Return only statuses *immediately newer* than the given min ID. +// The status with the specified ID will not be included in the response. +// in: query +// required: false +// - +// name: limit +// type: integer +// description: Number of scheduled statuses to return. +// default: 20 +// in: query +// required: false +// +// security: +// - OAuth2 Bearer: +// - read:statuses +// +// responses: +// '200': +// headers: +// Link: +// type: string +// description: Links to the next and previous queries. +// schema: +// type: array +// items: +// "$ref": "#/definitions/scheduledStatus" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ScheduledStatusesGETHandler(c *gin.Context) { + authed, errWithCode := apiutil.TokenAuth(c, + true, true, true, true, + apiutil.ScopeReadStatuses, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + page, errWithCode := paging.ParseIDPage(c, + 1, // min limit + 80, // max limit + 20, // default limit + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + resp, errWithCode := m.processor.Status().ScheduledStatusesGetPage( + c.Request.Context(), + authed.Account, + page, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + if resp.LinkHeader != "" { + c.Header("Link", resp.LinkHeader) + } + + apiutil.JSON(c, http.StatusOK, resp.Items) +} diff --git a/internal/api/client/scheduledstatuses/scheduledstatusget.go b/internal/api/client/scheduledstatuses/scheduledstatusget.go new file mode 100644 index 000000000..893c44938 --- /dev/null +++ b/internal/api/client/scheduledstatuses/scheduledstatusget.go @@ -0,0 +1,98 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package scheduledstatuses + +import ( + "net/http" + + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + + "github.com/gin-gonic/gin" +) + +// ScheduledStatusGETHandler swagger:operation GET /api/v1/scheduled_statuses/{id} getScheduledStatus +// +// Get a scheduled status with the given id. +// +// --- +// tags: +// - scheduled_statuses +// +// produces: +// - application/json +// +// parameters: +// - +// name: id +// type: string +// description: ID of the status +// in: path +// required: true +// +// security: +// - OAuth2 Bearer: +// - read:statuses +// +// responses: +// '200': +// schema: +// "$ref": "#/definitions/scheduledStatus" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ScheduledStatusGETHandler(c *gin.Context) { + authed, errWithCode := apiutil.TokenAuth(c, + true, true, true, true, + apiutil.ScopeReadStatuses, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + targetScheduledStatusID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + scheduledStatus, errWithCode := m.processor.Status().ScheduledStatusesGetOne( + c.Request.Context(), + authed.Account, + targetScheduledStatusID, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.JSON(c, http.StatusOK, scheduledStatus) +} diff --git a/internal/api/client/scheduledstatuses/scheduledstatusput.go b/internal/api/client/scheduledstatuses/scheduledstatusput.go new file mode 100644 index 000000000..f037716e7 --- /dev/null +++ b/internal/api/client/scheduledstatuses/scheduledstatusput.go @@ -0,0 +1,131 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package scheduledstatuses + +import ( + "net/http" + "time" + + apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + + "github.com/gin-gonic/gin" +) + +// ScheduledStatusPUTHandler swagger:operation PUT /api/v1/scheduled_statuses/{id} updateScheduledStatus +// +// Update a scheduled status's publishing date +// +// --- +// tags: +// - scheduled_statuses +// +// produces: +// - application/json +// +// parameters: +// - +// name: id +// type: string +// description: ID of the status +// in: path +// required: true +// - +// name: scheduled_at +// x-go-name: ScheduledAt +// description: |- +// ISO 8601 Datetime at which to schedule a status. +// +// Must be at least 5 minutes in the future. +// type: string +// format: date-time +// in: formData +// +// security: +// - OAuth2 Bearer: +// - write:statuses +// +// responses: +// '200': +// schema: +// "$ref": "#/definitions/scheduledStatus" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '404': +// description: not found +// '406': +// description: not acceptable +// '422': +// description: unprocessable content +// '500': +// description: internal server error +func (m *Module) ScheduledStatusPUTHandler(c *gin.Context) { + authed, errWithCode := apiutil.TokenAuth(c, + true, true, true, true, + apiutil.ScopeWriteStatuses, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + if authed.Account.IsMoving() { + apiutil.ForbiddenAfterMove(c) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + targetScheduledStatusID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + form := &apimodel.ScheduledStatusUpdateRequest{} + if err := c.ShouldBind(form); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + return + } + + now := time.Now() + if !now.Add(5 * time.Minute).Before(*form.ScheduledAt) { + const errText = "scheduled_at must be at least 5 minutes in the future" + apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(gtserror.New(errText), errText), m.processor.InstanceGetV1) + return + } + + scheduledStatus, errWithCode := m.processor.Status().ScheduledStatusesUpdate( + c.Request.Context(), + authed.Account, + targetScheduledStatusID, + form.ScheduledAt, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.JSON(c, http.StatusOK, scheduledStatus) +} diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go index 048acd421..167b59b23 100644 --- a/internal/api/client/statuses/statuscreate.go +++ b/internal/api/client/statuses/statuscreate.go @@ -181,7 +181,6 @@ import ( // // Providing this parameter with a *future* time will cause ScheduledStatus to be returned instead of Status. // Must be at least 5 minutes in the future. -// This feature isn't implemented yet. // // Providing this parameter with a *past* time will cause the status to be backdated, // and will not push it to the user's followers. This is intended for importing old statuses. @@ -256,6 +255,8 @@ import ( // description: not found // '406': // description: not acceptable +// '422': +// description: unprocessable content // '500': // description: internal server error // '501': @@ -300,7 +301,9 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { authed.Account, authed.Application, form, + nil, ) + if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go index 090dae593..84a6622a2 100644 --- a/internal/api/client/statuses/statuscreate_test.go +++ b/internal/api/client/statuses/statuscreate_test.go @@ -476,13 +476,24 @@ func (suite *StatusCreateTestSuite) TestPostNewScheduledStatus() { "scheduled_at": {"2080-10-04T15:32:02.018Z"}, }, "") - // We should have 501 from + // We should have OK from // our call to the function. - suite.Equal(http.StatusNotImplemented, recorder.Code) + suite.Equal(http.StatusOK, recorder.Code) - // We should have a helpful error message. + // A scheduled status with scheduled_at and status params should be returned. suite.Equal(`{ - "error": "Not Implemented: scheduled statuses are not yet supported" + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "media_attachments": [], + "params": { + "application_id": "01F8MGY43H3N2C8EWPR2FPYEXG", + "language": "", + "scheduled_at": null, + "sensitive": true, + "spoiler_text": "hello hello", + "text": "this is a brand new status! #helloworld", + "visibility": "private" + }, + "scheduled_at": "2080-10-04T15:32:02.018Z" }`, out) } |
