mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 15:42:25 -05:00 
			
		
		
		
	[feature] Conversations API (#3013)
* Implement conversations API * Sort and page conversations by last status ID * Appease linter * Fix deleting conversations and statuses * Refactor to make migrations automatic * Lint * Update tests post-merge * Fixes from live-fire testing * Linter caught a format problem * Refactor tests, fix cache * Negative test for non-DMs * Run conversations advanced migration on testrig startup as well as regular server startup * Document (lack of) side effects of API method for deleting a conversation * Make not-found check less nested for readability * Rename PutConversation to UpsertConversation * Use util.Ptr instead of IIFE * Reduce cache used by conversations * Remove unnecessary TableExpr/ColumnExpr * Use struct tags for both unique constraints on Conversation * Make it clear how paging with GetDirectStatusIDsBatch should be used * Let conversation paging skip conversations it can't render * Use Bun NewDropTable * Convert delete raw query to Bun * Convert update raw query to Bun * Convert latestConversationStatusesTempTable raw query partially to Bun * Convert conversationStatusesTempTable raw query partially to Bun * Rename field used to store result of MaxDirectStatusID * Move advanced migrations to their own tiny processor * Catch up util function name with main * Remove json.… wrappers * Remove redundant check * Combine error checks * Replace map with slice of structs * Address processor/type converter comments - Add context info for errors - Extract some common processor code into shared methods - Move conversation eligibility check ahead of populating conversation * Add error context when dropping temp tables
This commit is contained in:
		
					parent
					
						
							
								31294f7c78
							
						
					
				
			
			
				commit
				
					
						8fdd358f4b
					
				
			
		
					 55 changed files with 3317 additions and 143 deletions
				
			
		|  | @ -50,6 +50,8 @@ func (suite *FromClientAPITestSuite) newStatus( | |||
| 	visibility gtsmodel.Visibility, | ||||
| 	replyToStatus *gtsmodel.Status, | ||||
| 	boostOfStatus *gtsmodel.Status, | ||||
| 	mentionedAccounts []*gtsmodel.Account, | ||||
| 	createThread bool, | ||||
| ) *gtsmodel.Status { | ||||
| 	var ( | ||||
| 		protocol = config.GetProtocol() | ||||
|  | @ -102,6 +104,39 @@ func (suite *FromClientAPITestSuite) newStatus( | |||
| 		newStatus.Visibility = boostOfStatus.Visibility | ||||
| 	} | ||||
| 
 | ||||
| 	for _, mentionedAccount := range mentionedAccounts { | ||||
| 		newMention := >smodel.Mention{ | ||||
| 			ID:               id.NewULID(), | ||||
| 			StatusID:         newStatus.ID, | ||||
| 			Status:           newStatus, | ||||
| 			OriginAccountID:  account.ID, | ||||
| 			OriginAccountURI: account.URI, | ||||
| 			OriginAccount:    account, | ||||
| 			TargetAccountID:  mentionedAccount.ID, | ||||
| 			TargetAccount:    mentionedAccount, | ||||
| 			Silent:           util.Ptr(false), | ||||
| 		} | ||||
| 
 | ||||
| 		newStatus.Mentions = append(newStatus.Mentions, newMention) | ||||
| 		newStatus.MentionIDs = append(newStatus.MentionIDs, newMention.ID) | ||||
| 
 | ||||
| 		if err := state.DB.PutMention(ctx, newMention); err != nil { | ||||
| 			suite.FailNow(err.Error()) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if createThread { | ||||
| 		newThread := >smodel.Thread{ | ||||
| 			ID: id.NewULID(), | ||||
| 		} | ||||
| 
 | ||||
| 		newStatus.ThreadID = newThread.ID | ||||
| 
 | ||||
| 		if err := state.DB.PutThread(ctx, newThread); err != nil { | ||||
| 			suite.FailNow(err.Error()) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Put the status in the db, to mimic what would | ||||
| 	// have already happened earlier up the flow. | ||||
| 	if err := state.DB.PutStatus(ctx, newStatus); err != nil { | ||||
|  | @ -168,6 +203,31 @@ func (suite *FromClientAPITestSuite) statusJSON( | |||
| 	return string(statusJSON) | ||||
| } | ||||
| 
 | ||||
| func (suite *FromClientAPITestSuite) conversationJSON( | ||||
| 	ctx context.Context, | ||||
| 	typeConverter *typeutils.Converter, | ||||
| 	conversation *gtsmodel.Conversation, | ||||
| 	requestingAccount *gtsmodel.Account, | ||||
| ) string { | ||||
| 	apiConversation, err := typeConverter.ConversationToAPIConversation( | ||||
| 		ctx, | ||||
| 		conversation, | ||||
| 		requestingAccount, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	conversationJSON, err := json.Marshal(apiConversation) | ||||
| 	if err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	return string(conversationJSON) | ||||
| } | ||||
| 
 | ||||
| func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() { | ||||
| 	testStructs := suite.SetupTestStructs() | ||||
| 	defer suite.TearDownTestStructs(testStructs) | ||||
|  | @ -194,6 +254,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() { | |||
| 			gtsmodel.VisibilityPublic, | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			false, | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
|  | @ -303,6 +365,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() { | |||
| 			gtsmodel.VisibilityPublic, | ||||
| 			suite.testStatuses["local_account_2_status_1"], | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			false, | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
|  | @ -362,6 +426,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() { | |||
| 			gtsmodel.VisibilityPublic, | ||||
| 			suite.testStatuses["local_account_1_status_1"], | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			false, | ||||
| 		) | ||||
| 		threadMute = >smodel.ThreadMute{ | ||||
| 			ID:        "01HD3KRMBB1M85QRWHD912QWRE", | ||||
|  | @ -420,6 +486,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() { | |||
| 			gtsmodel.VisibilityPublic, | ||||
| 			nil, | ||||
| 			suite.testStatuses["local_account_1_status_1"], | ||||
| 			nil, | ||||
| 			false, | ||||
| 		) | ||||
| 		threadMute = >smodel.ThreadMute{ | ||||
| 			ID:        "01HD3KRMBB1M85QRWHD912QWRE", | ||||
|  | @ -483,6 +551,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis | |||
| 			gtsmodel.VisibilityPublic, | ||||
| 			suite.testStatuses["local_account_2_status_1"], | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			false, | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
|  | @ -556,6 +626,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis | |||
| 			gtsmodel.VisibilityPublic, | ||||
| 			suite.testStatuses["local_account_2_status_1"], | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			false, | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
|  | @ -634,6 +706,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli | |||
| 			gtsmodel.VisibilityPublic, | ||||
| 			suite.testStatuses["local_account_2_status_1"], | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			false, | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
|  | @ -704,6 +778,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() { | |||
| 			gtsmodel.VisibilityPublic, | ||||
| 			nil, | ||||
| 			suite.testStatuses["local_account_2_status_1"], | ||||
| 			nil, | ||||
| 			false, | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
|  | @ -765,6 +841,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() { | |||
| 			gtsmodel.VisibilityPublic, | ||||
| 			nil, | ||||
| 			suite.testStatuses["local_account_2_status_1"], | ||||
| 			nil, | ||||
| 			false, | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
|  | @ -807,6 +885,159 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() { | |||
| 	) | ||||
| } | ||||
| 
 | ||||
| // A DM to a local user should create a conversation and accompanying notification. | ||||
| func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichBeginsConversation() { | ||||
| 	testStructs := suite.SetupTestStructs() | ||||
| 	defer suite.TearDownTestStructs(testStructs) | ||||
| 
 | ||||
| 	var ( | ||||
| 		ctx              = context.Background() | ||||
| 		postingAccount   = suite.testAccounts["local_account_2"] | ||||
| 		receivingAccount = suite.testAccounts["local_account_1"] | ||||
| 		streams          = suite.openStreams(ctx, | ||||
| 			testStructs.Processor, | ||||
| 			receivingAccount, | ||||
| 			nil, | ||||
| 		) | ||||
| 		homeStream   = streams[stream.TimelineHome] | ||||
| 		directStream = streams[stream.TimelineDirect] | ||||
| 
 | ||||
| 		// turtle posts a new top-level DM mentioning zork. | ||||
| 		status = suite.newStatus( | ||||
| 			ctx, | ||||
| 			testStructs.State, | ||||
| 			postingAccount, | ||||
| 			gtsmodel.VisibilityDirect, | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			[]*gtsmodel.Account{receivingAccount}, | ||||
| 			true, | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
| 	// Process the new status. | ||||
| 	if err := testStructs.Processor.Workers().ProcessFromClientAPI( | ||||
| 		ctx, | ||||
| 		&messages.FromClientAPI{ | ||||
| 			APObjectType:   ap.ObjectNote, | ||||
| 			APActivityType: ap.ActivityCreate, | ||||
| 			GTSModel:       status, | ||||
| 			Origin:         postingAccount, | ||||
| 		}, | ||||
| 	); err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	// Locate the conversation which should now exist for zork. | ||||
| 	conversation, err := testStructs.State.DB.GetConversationByThreadAndAccountIDs( | ||||
| 		ctx, | ||||
| 		status.ThreadID, | ||||
| 		receivingAccount.ID, | ||||
| 		[]string{postingAccount.ID}, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	// Check status in home stream. | ||||
| 	suite.checkStreamed( | ||||
| 		homeStream, | ||||
| 		true, | ||||
| 		"", | ||||
| 		stream.EventTypeUpdate, | ||||
| 	) | ||||
| 
 | ||||
| 	// Check mention notification in home stream. | ||||
| 	suite.checkStreamed( | ||||
| 		homeStream, | ||||
| 		true, | ||||
| 		"", | ||||
| 		stream.EventTypeNotification, | ||||
| 	) | ||||
| 
 | ||||
| 	// Check conversation in direct stream. | ||||
| 	conversationJSON := suite.conversationJSON( | ||||
| 		ctx, | ||||
| 		testStructs.TypeConverter, | ||||
| 		conversation, | ||||
| 		receivingAccount, | ||||
| 	) | ||||
| 	suite.checkStreamed( | ||||
| 		directStream, | ||||
| 		true, | ||||
| 		conversationJSON, | ||||
| 		stream.EventTypeConversation, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // A public message to a local user should not result in a conversation notification. | ||||
| func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichShouldNotCreateConversation() { | ||||
| 	testStructs := suite.SetupTestStructs() | ||||
| 	defer suite.TearDownTestStructs(testStructs) | ||||
| 
 | ||||
| 	var ( | ||||
| 		ctx              = context.Background() | ||||
| 		postingAccount   = suite.testAccounts["local_account_2"] | ||||
| 		receivingAccount = suite.testAccounts["local_account_1"] | ||||
| 		streams          = suite.openStreams(ctx, | ||||
| 			testStructs.Processor, | ||||
| 			receivingAccount, | ||||
| 			nil, | ||||
| 		) | ||||
| 		homeStream   = streams[stream.TimelineHome] | ||||
| 		directStream = streams[stream.TimelineDirect] | ||||
| 
 | ||||
| 		// turtle posts a new top-level public message mentioning zork. | ||||
| 		status = suite.newStatus( | ||||
| 			ctx, | ||||
| 			testStructs.State, | ||||
| 			postingAccount, | ||||
| 			gtsmodel.VisibilityPublic, | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			[]*gtsmodel.Account{receivingAccount}, | ||||
| 			true, | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
| 	// Process the new status. | ||||
| 	if err := testStructs.Processor.Workers().ProcessFromClientAPI( | ||||
| 		ctx, | ||||
| 		&messages.FromClientAPI{ | ||||
| 			APObjectType:   ap.ObjectNote, | ||||
| 			APActivityType: ap.ActivityCreate, | ||||
| 			GTSModel:       status, | ||||
| 			Origin:         postingAccount, | ||||
| 		}, | ||||
| 	); err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	// Check status in home stream. | ||||
| 	suite.checkStreamed( | ||||
| 		homeStream, | ||||
| 		true, | ||||
| 		"", | ||||
| 		stream.EventTypeUpdate, | ||||
| 	) | ||||
| 
 | ||||
| 	// Check mention notification in home stream. | ||||
| 	suite.checkStreamed( | ||||
| 		homeStream, | ||||
| 		true, | ||||
| 		"", | ||||
| 		stream.EventTypeNotification, | ||||
| 	) | ||||
| 
 | ||||
| 	// Check for absence of conversation notification in direct stream. | ||||
| 	suite.checkStreamed( | ||||
| 		directStream, | ||||
| 		false, | ||||
| 		"", | ||||
| 		"", | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| func (suite *FromClientAPITestSuite) TestProcessStatusDelete() { | ||||
| 	testStructs := suite.SetupTestStructs() | ||||
| 	defer suite.TearDownTestStructs(testStructs) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue