Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Conversations API #3013

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dd99868
Implement conversations API
VyrCossont Jun 11, 2024
31b260d
Sort and page conversations by last status ID
VyrCossont Jul 2, 2024
86262e6
Appease linter
VyrCossont Jul 2, 2024
6a3e4d3
Fix deleting conversations and statuses
VyrCossont Jul 2, 2024
a388920
Refactor to make migrations automatic
VyrCossont Jul 12, 2024
480744b
Lint
VyrCossont Jul 12, 2024
3767a1a
Merge remote-tracking branch 'upstream/main' into conversations-api
VyrCossont Jul 12, 2024
80a2d62
Update tests post-merge
VyrCossont Jul 12, 2024
3b6c594
Fixes from live-fire testing
VyrCossont Jul 12, 2024
aafd0b9
Linter caught a format problem
VyrCossont Jul 12, 2024
43856e6
Refactor tests, fix cache
VyrCossont Jul 12, 2024
9b72520
Negative test for non-DMs
VyrCossont Jul 12, 2024
6db1026
Merge remote-tracking branch 'upstream/main' into conversations-api
VyrCossont Jul 12, 2024
2a84531
Run conversations advanced migration on testrig startup as well as re…
VyrCossont Jul 14, 2024
7e4f4bb
Document (lack of) side effects of API method for deleting a conversa…
VyrCossont Jul 14, 2024
a8c9b60
Make not-found check less nested for readability
VyrCossont Jul 14, 2024
b7db4a5
Rename PutConversation to UpsertConversation
VyrCossont Jul 14, 2024
3308051
Use util.Ptr instead of IIFE
VyrCossont Jul 14, 2024
cf37096
Reduce cache used by conversations
VyrCossont Jul 14, 2024
94fe752
Remove unnecessary TableExpr/ColumnExpr
VyrCossont Jul 14, 2024
f6ced07
Use struct tags for both unique constraints on Conversation
VyrCossont Jul 15, 2024
75a9ed8
Make it clear how paging with GetDirectStatusIDsBatch should be used
VyrCossont Jul 15, 2024
67b7cca
Let conversation paging skip conversations it can't render
VyrCossont Jul 15, 2024
10abcf9
Use Bun NewDropTable
VyrCossont Jul 15, 2024
573a81d
Convert delete raw query to Bun
VyrCossont Jul 15, 2024
66520da
Convert update raw query to Bun
VyrCossont Jul 15, 2024
45114ff
Convert latestConversationStatusesTempTable raw query partially to Bun
VyrCossont Jul 15, 2024
041584b
Convert conversationStatusesTempTable raw query partially to Bun
VyrCossont Jul 15, 2024
b26d104
Rename field used to store result of MaxDirectStatusID
VyrCossont Jul 15, 2024
284b730
Move advanced migrations to their own tiny processor
VyrCossont Jul 20, 2024
aff687c
Merge remote-tracking branch 'upstream/main' into conversations-api
VyrCossont Jul 20, 2024
adbd7ed
Catch up util function name with main
VyrCossont Jul 20, 2024
1b853a6
Remove json.… wrappers
VyrCossont Jul 23, 2024
caefe9b
Remove redundant check
VyrCossont Jul 23, 2024
ff4feb9
Combine error checks
VyrCossont Jul 23, 2024
cf7a626
Replace map with slice of structs
VyrCossont Jul 23, 2024
0367bb3
Address processor/type converter comments
VyrCossont Jul 23, 2024
6b3dfde
Add error context when dropping temp tables
VyrCossont Jul 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/gotosocial/action/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ var Start action.GTSAction = func(ctx context.Context) error {
return fmt.Errorf("error initializing metrics: %w", err)
}

// Run advanced migrations.
if err := processor.Conversations().MigrateDMsToConversations(ctx); err != nil {
return fmt.Errorf("error running conversations advanced migration: %w", err)
}

/*
HTTP router initialization
*/
Expand Down
72 changes: 67 additions & 5 deletions docs/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6121,11 +6121,43 @@ paths:
- read:bookmarks
tags:
- bookmarks
/api/v1/conversation/{id}/read:
post:
operationId: conversationRead
parameters:
- description: ID of the conversation.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Updated conversation.
schema:
$ref: '#/definitions/conversation'
"400":
description: bad request
"401":
description: unauthorized
"404":
description: not found
"406":
description: not acceptable
"422":
description: unprocessable content
"500":
description: internal server error
security:
- OAuth2 Bearer:
- write:conversations
summary: Mark a conversation with the given ID as read.
tags:
- conversations
/api/v1/conversations:
get:
description: |-
NOT IMPLEMENTED YET: Will currently always return an array of length 0.

The next and previous queries can be parsed from the returned Link header.
Example:

Expand All @@ -6134,15 +6166,15 @@ paths:
````
operationId: conversationsGet
parameters:
- description: 'Return only conversations *OLDER* than the given max ID. The conversation with the specified ID will not be included in the response. NOTE: the ID is of the internal conversation, use the Link header for pagination.'
- description: 'Return only conversations with last statuses *OLDER* than the given max ID. The conversation with the specified ID will not be included in the response. NOTE: The ID is a status ID. Use the Link header for pagination.'
in: query
name: max_id
type: string
- description: 'Return only conversations *NEWER* than the given since ID. The conversation with the specified ID will not be included in the response. NOTE: the ID is of the internal conversation, use the Link header for pagination.'
- description: 'Return only conversations with last statuses *NEWER* than the given since ID. The conversation with the specified ID will not be included in the response. NOTE: The ID is a status ID. Use the Link header for pagination.'
in: query
name: since_id
type: string
- description: 'Return only conversations *IMMEDIATELY NEWER* than the given min ID. The conversation with the specified ID will not be included in the response. NOTE: the ID is of the internal conversation, use the Link header for pagination.'
- description: 'Return only conversations with last statuses *IMMEDIATELY NEWER* than the given min ID. The conversation with the specified ID will not be included in the response. NOTE: The ID is a status ID. Use the Link header for pagination.'
in: query
name: min_id
type: string
Expand Down Expand Up @@ -6182,6 +6214,36 @@ paths:
summary: Get an array of (direct message) conversations that requesting account is involved in.
tags:
- conversations
/api/v1/conversations/{id}:
delete:
operationId: conversationDelete
parameters:
- description: ID of the conversation
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: conversation deleted
"400":
description: bad request
"401":
description: unauthorized
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security:
- OAuth2 Bearer:
- write:conversations
summary: Delete a single conversation with the given ID.
tags:
- conversations
/api/v1/custom_emojis:
get:
operationId: customEmojisGet
Expand Down
90 changes: 90 additions & 0 deletions internal/api/client/conversations/conversationdelete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// GoToSocial
// Copyright (C) GoToSocial Authors [email protected]
// 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 conversations

import (
"net/http"

"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)

// ConversationDELETEHandler swagger:operation DELETE /api/v1/conversations/{id} conversationDelete
//
// Delete a single conversation with the given ID.
//
// ---
// tags:
// - conversations
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// type: string
// description: ID of the conversation
// in: path
// required: true
//
// security:
// - OAuth2 Bearer:
// - write:conversations
//
// responses:
// '200':
// description: conversation deleted
// '400':
// description: bad request
// '401':
// description: unauthorized
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) ConversationDELETEHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), 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
}

id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}

errWithCode = m.processor.Conversations().Delete(c.Request.Context(), authed.Account, id)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}

c.JSON(http.StatusOK, apiutil.EmptyJSONObject)
}
95 changes: 95 additions & 0 deletions internal/api/client/conversations/conversationread.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// GoToSocial
// Copyright (C) GoToSocial Authors [email protected]
// 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 conversations

import (
"net/http"

"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)

// ConversationReadPOSTHandler swagger:operation POST /api/v1/conversation/{id}/read conversationRead
//
// Mark a conversation with the given ID as read.
//
// ---
// tags:
// - conversations
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// in: path
// type: string
// required: true
// description: ID of the conversation.
//
// security:
// - OAuth2 Bearer:
// - write:conversations
//
// responses:
// '200':
// name: conversation
// description: Updated conversation.
// schema:
// "$ref": "#/definitions/conversation"
// '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) ConversationReadPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), 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
}

id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}

apiConversation, errWithCode := m.processor.Conversations().Read(c.Request.Context(), authed.Account, id)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}

apiutil.JSON(c, http.StatusOK, apiConversation)
}
10 changes: 8 additions & 2 deletions internal/api/client/conversations/conversations.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ import (
"net/http"

"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/processing"
)

const (
// BasePath is the base URI path for serving
// conversations, minus the api prefix.
// BasePath is the base path for serving the conversations API, minus the 'api' prefix.
BasePath = "/v1/conversations"
// BasePathWithID is the base path with the ID key in it, for operations on an existing conversation.
BasePathWithID = BasePath + "/:" + apiutil.IDKey
// ReadPathWithID is the path for marking an existing conversation as read.
ReadPathWithID = BasePathWithID + "/read"
)

type Module struct {
Expand All @@ -42,4 +46,6 @@ func New(processor *processing.Processor) *Module {

func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
attachHandler(http.MethodGet, BasePath, m.ConversationsGETHandler)
attachHandler(http.MethodDelete, BasePathWithID, m.ConversationDELETEHandler)
attachHandler(http.MethodPost, ReadPathWithID, m.ConversationReadPOSTHandler)
}
Loading