Compare commits

..

5 commits

7 changed files with 394 additions and 33 deletions

View file

@ -950,7 +950,12 @@ definitions:
with "direct message" visibility. with "direct message" visibility.
properties: properties:
accounts: accounts:
description: Participants in the conversation. description: |-
Participants in the conversation.
If this is a conversation between no accounts (ie., a self-directed DM),
this will include only the requesting account itself. Otherwise, it will
include every other account in the conversation *except* the requester.
items: items:
$ref: '#/definitions/account' $ref: '#/definitions/account'
type: array type: array

View file

@ -27,6 +27,10 @@ type Conversation struct {
// Is the conversation currently marked as unread? // Is the conversation currently marked as unread?
Unread bool `json:"unread"` Unread bool `json:"unread"`
// Participants in the conversation. // Participants in the conversation.
//
// If this is a conversation between no accounts (ie., a self-directed DM),
// this will include only the requesting account itself. Otherwise, it will
// include every other account in the conversation *except* the requester.
Accounts []Account `json:"accounts"` Accounts []Account `json:"accounts"`
// The last status in the conversation. May be `null`. // The last status in the conversation. May be `null`.
LastStatus *Status `json:"last_status"` LastStatus *Status `json:"last_status"`

View file

@ -112,7 +112,7 @@ func (c *sqliteConn) Close() (err error) {
raw := c.connIface.(sqlite3driver.Conn).Raw() raw := c.connIface.(sqlite3driver.Conn).Raw()
// see: https://www.sqlite.org/pragma.html#pragma_optimize // see: https://www.sqlite.org/pragma.html#pragma_optimize
const onClose = "PRAGMA analysis_limit=1000; PRAGMA optimize;" const onClose = "PRAGMA optimize;"
_ = raw.Exec(onClose) _ = raw.Exec(onClose)
// Finally, close. // Finally, close.

View file

@ -247,6 +247,12 @@ func (p *Processor) GetVisibleAPIStatuses(
continue continue
} }
if apiStatus == nil {
// Status was
// filtered out.
continue
}
// Append converted status to return slice. // Append converted status to return slice.
apiStatuses = append(apiStatuses, *apiStatus) apiStatuses = append(apiStatuses, *apiStatus)
} }

View file

@ -1832,46 +1832,23 @@ func (c *Converter) NotificationToAPINotification(
func (c *Converter) ConversationToAPIConversation( func (c *Converter) ConversationToAPIConversation(
ctx context.Context, ctx context.Context,
conversation *gtsmodel.Conversation, conversation *gtsmodel.Conversation,
requestingAccount *gtsmodel.Account, requester *gtsmodel.Account,
filters []*gtsmodel.Filter, filters []*gtsmodel.Filter,
mutes *usermute.CompiledUserMuteList, mutes *usermute.CompiledUserMuteList,
) (*apimodel.Conversation, error) { ) (*apimodel.Conversation, error) {
apiConversation := &apimodel.Conversation{ apiConversation := &apimodel.Conversation{
ID: conversation.ID, ID: conversation.ID,
Unread: !*conversation.Read, Unread: !*conversation.Read,
Accounts: []apimodel.Account{},
}
for _, account := range conversation.OtherAccounts {
var apiAccount *apimodel.Account
blocked, err := c.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, account.ID)
if err != nil {
return nil, gtserror.Newf(
"DB error checking blocks between accounts %s and %s: %w",
requestingAccount.ID,
account.ID,
err,
)
}
if blocked || account.IsSuspended() {
apiAccount, err = c.AccountToAPIAccountBlocked(ctx, account)
} else {
apiAccount, err = c.AccountToAPIAccountPublic(ctx, account)
}
if err != nil {
return nil, gtserror.Newf(
"error converting account %s to API representation: %w",
account.ID,
err,
)
}
apiConversation.Accounts = append(apiConversation.Accounts, *apiAccount)
} }
// Populate most recent status in convo;
// can be nil if this status is filtered.
if conversation.LastStatus != nil { if conversation.LastStatus != nil {
var err error var err error
apiConversation.LastStatus, err = c.StatusToAPIStatus( apiConversation.LastStatus, err = c.StatusToAPIStatus(
ctx, ctx,
conversation.LastStatus, conversation.LastStatus,
requestingAccount, requester,
statusfilter.FilterContextNotifications, statusfilter.FilterContextNotifications,
filters, filters,
mutes, mutes,
@ -1885,6 +1862,60 @@ func (c *Converter) ConversationToAPIConversation(
} }
} }
// If no other accounts are involved in this convo,
// just include the requesting account and return.
//
// See: https://github.com/superseriousbusiness/gotosocial/issues/3385#issuecomment-2394033477
otherAcctsLen := len(conversation.OtherAccounts)
if otherAcctsLen == 0 {
apiAcct, err := c.AccountToAPIAccountPublic(ctx, requester)
if err != nil {
err := gtserror.Newf(
"error converting account %s to API representation: %w",
requester.ID, err,
)
return nil, err
}
apiConversation.Accounts = []apimodel.Account{*apiAcct}
return apiConversation, nil
}
// Other accounts are involved in the
// convo. Convert each to API model.
apiConversation.Accounts = make([]apimodel.Account, otherAcctsLen)
for i, account := range conversation.OtherAccounts {
blocked, err := c.state.DB.IsEitherBlocked(ctx,
requester.ID, account.ID,
)
if err != nil {
err := gtserror.Newf(
"db error checking blocks between accounts %s and %s: %w",
requester.ID, account.ID, err,
)
return nil, err
}
// API account model varies depending
// on status of conversation participant.
var apiAcct *apimodel.Account
if blocked || account.IsSuspended() {
apiAcct, err = c.AccountToAPIAccountBlocked(ctx, account)
} else {
apiAcct, err = c.AccountToAPIAccountPublic(ctx, account)
}
if err != nil {
err := gtserror.Newf(
"error converting account %s to API representation: %w",
account.ID, err,
)
return nil, err
}
apiConversation.Accounts[i] = *apiAcct
}
return apiConversation, nil return apiConversation, nil
} }

View file

@ -3358,6 +3358,321 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() {
}`, string(b)) }`, string(b))
} }
func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() {
var (
ctx = context.Background()
requester = suite.testAccounts["local_account_1"]
lastStatus = suite.testStatuses["local_account_1_status_1"]
filters []*gtsmodel.Filter = nil
mutes *usermute.CompiledUserMuteList = nil
)
convo := &gtsmodel.Conversation{
ID: "01J9C6K86PKZ5GY5WXV94DGH6R",
CreatedAt: testrig.TimeMustParse("2022-06-10T15:22:08Z"),
UpdatedAt: testrig.TimeMustParse("2022-06-10T15:22:08Z"),
AccountID: requester.ID,
Account: requester,
OtherAccounts: nil,
LastStatus: lastStatus,
Read: util.Ptr(true),
}
apiConvo, err := suite.typeconverter.ConversationToAPIConversation(
ctx,
convo,
requester,
filters,
mutes,
)
if err != nil {
suite.FailNow(err.Error())
}
b, err := json.MarshalIndent(apiConvo, "", " ")
if err != nil {
suite.FailNow(err.Error())
}
// No other accounts involved, so we should only
// have our own account in the "accounts" field.
suite.Equal(`{
"id": "01J9C6K86PKZ5GY5WXV94DGH6R",
"unread": false,
"accounts": [
{
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
"username": "the_mighty_zork",
"acct": "the_mighty_zork",
"display_name": "original zork (he/they)",
"locked": false,
"discoverable": true,
"bot": false,
"created_at": "2022-05-20T11:09:18.000Z",
"note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
"url": "http://localhost:8080/@the_mighty_zork",
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
"avatar_description": "a green goblin looking nasty",
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"enable_rss": true
}
],
"last_status": {
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
"created_at": "2021-10-20T10:40:37.000Z",
"in_reply_to_id": null,
"in_reply_to_account_id": null,
"sensitive": true,
"spoiler_text": "introduction post",
"visibility": "public",
"language": "en",
"uri": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
"url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
"replies_count": 2,
"reblogs_count": 1,
"favourites_count": 1,
"favourited": false,
"reblogged": false,
"muted": false,
"bookmarked": false,
"pinned": false,
"content": "hello everyone!",
"reblog": null,
"application": {
"name": "really cool gts application",
"website": "https://reallycool.app"
},
"account": {
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
"username": "the_mighty_zork",
"acct": "the_mighty_zork",
"display_name": "original zork (he/they)",
"locked": false,
"discoverable": true,
"bot": false,
"created_at": "2022-05-20T11:09:18.000Z",
"note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
"url": "http://localhost:8080/@the_mighty_zork",
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
"avatar_description": "a green goblin looking nasty",
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"enable_rss": true
},
"media_attachments": [],
"mentions": [],
"tags": [],
"emojis": [],
"card": null,
"poll": null,
"text": "hello everyone!",
"interaction_policy": {
"can_favourite": {
"always": [
"public",
"me"
],
"with_approval": []
},
"can_reply": {
"always": [
"public",
"me"
],
"with_approval": []
},
"can_reblog": {
"always": [
"public",
"me"
],
"with_approval": []
}
}
}
}`, string(b))
}
func (suite *InternalToFrontendTestSuite) TestConversationToAPI() {
var (
ctx = context.Background()
requester = suite.testAccounts["local_account_1"]
lastStatus = suite.testStatuses["local_account_1_status_1"]
filters []*gtsmodel.Filter = nil
mutes *usermute.CompiledUserMuteList = nil
)
convo := &gtsmodel.Conversation{
ID: "01J9C6K86PKZ5GY5WXV94DGH6R",
CreatedAt: testrig.TimeMustParse("2022-06-10T15:22:08Z"),
UpdatedAt: testrig.TimeMustParse("2022-06-10T15:22:08Z"),
AccountID: requester.ID,
Account: requester,
OtherAccounts: []*gtsmodel.Account{
suite.testAccounts["local_account_2"],
},
LastStatus: lastStatus,
Read: util.Ptr(false),
}
apiConvo, err := suite.typeconverter.ConversationToAPIConversation(
ctx,
convo,
requester,
filters,
mutes,
)
if err != nil {
suite.FailNow(err.Error())
}
b, err := json.MarshalIndent(apiConvo, "", " ")
if err != nil {
suite.FailNow(err.Error())
}
// One other account is involved, so they
// should in the "accounts" field and not us.
suite.Equal(`{
"id": "01J9C6K86PKZ5GY5WXV94DGH6R",
"unread": true,
"accounts": [
{
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
"username": "1happyturtle",
"acct": "1happyturtle",
"display_name": "happy little turtle :3",
"locked": true,
"discoverable": false,
"bot": false,
"created_at": "2022-06-04T13:12:00.000Z",
"note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
"url": "http://localhost:8080/@1happyturtle",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/assets/default_header.webp",
"header_static": "http://localhost:8080/assets/default_header.webp",
"followers_count": 1,
"following_count": 1,
"statuses_count": 8,
"last_status_at": "2021-07-28T08:40:37.000Z",
"emojis": [],
"fields": [
{
"name": "should you follow me?",
"value": "maybe!",
"verified_at": null
},
{
"name": "age",
"value": "120",
"verified_at": null
}
],
"hide_collections": true
}
],
"last_status": {
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
"created_at": "2021-10-20T10:40:37.000Z",
"in_reply_to_id": null,
"in_reply_to_account_id": null,
"sensitive": true,
"spoiler_text": "introduction post",
"visibility": "public",
"language": "en",
"uri": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
"url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
"replies_count": 2,
"reblogs_count": 1,
"favourites_count": 1,
"favourited": false,
"reblogged": false,
"muted": false,
"bookmarked": false,
"pinned": false,
"content": "hello everyone!",
"reblog": null,
"application": {
"name": "really cool gts application",
"website": "https://reallycool.app"
},
"account": {
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
"username": "the_mighty_zork",
"acct": "the_mighty_zork",
"display_name": "original zork (he/they)",
"locked": false,
"discoverable": true,
"bot": false,
"created_at": "2022-05-20T11:09:18.000Z",
"note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
"url": "http://localhost:8080/@the_mighty_zork",
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp",
"avatar_description": "a green goblin looking nasty",
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp",
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"enable_rss": true
},
"media_attachments": [],
"mentions": [],
"tags": [],
"emojis": [],
"card": null,
"poll": null,
"text": "hello everyone!",
"interaction_policy": {
"can_favourite": {
"always": [
"public",
"me"
],
"with_approval": []
},
"can_reply": {
"always": [
"public",
"me"
],
"with_approval": []
},
"can_reblog": {
"always": [
"public",
"me"
],
"with_approval": []
}
}
}
}`, string(b))
}
func TestInternalToFrontendTestSuite(t *testing.T) { func TestInternalToFrontendTestSuite(t *testing.T) {
suite.Run(t, new(InternalToFrontendTestSuite)) suite.Run(t, new(InternalToFrontendTestSuite))
} }

View file

@ -618,7 +618,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
} }
if diff := len(accountsSorted) - len(preserializedKeys); diff > 0 { if diff := len(accountsSorted) - len(preserializedKeys); diff > 0 {
keyStrings := make([]string, diff) keyStrings := make([]string, 0, diff)
for i := 0; i < diff; i++ { for i := 0; i < diff; i++ {
priv, _ := rsa.GenerateKey(rand.Reader, 2048) priv, _ := rsa.GenerateKey(rand.Reader, 2048)
key, _ := x509.MarshalPKCS8PrivateKey(priv) key, _ := x509.MarshalPKCS8PrivateKey(priv)