mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 14:42:26 -05:00 
			
		
		
		
	Merge branch 'main' into frogend-restructure
This commit is contained in:
		
				commit
				
					
						eec7e5573c
					
				
			
		
					 144 changed files with 2556 additions and 1784 deletions
				
			
		|  | @ -38,6 +38,7 @@ steps: | ||||||
|       - apk update --no-cache && apk add git |       - apk update --no-cache && apk add git | ||||||
|       - CGO_ENABLED=0 GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test ./... |       - CGO_ENABLED=0 GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test ./... | ||||||
|       - CGO_ENABLED=0 ./test/cliparsing.sh |       - CGO_ENABLED=0 ./test/cliparsing.sh | ||||||
|  |       - CGO_ENABLED=0 ./test/envparsing.sh | ||||||
|     when: |     when: | ||||||
|       event: |       event: | ||||||
|         include: |         include: | ||||||
|  | @ -145,6 +146,6 @@ steps: | ||||||
| 
 | 
 | ||||||
| --- | --- | ||||||
| kind: signature | kind: signature | ||||||
| hmac: f3cf4e422d9ce7dc0a881da429db628232e2f9e91383ee5a33cf4f13542c0a23 | hmac: adfcc11559717e4e371e714f3ac19ab528208f678961436f316f491bf82de8ad | ||||||
| 
 | 
 | ||||||
| ... | ... | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -21,3 +21,6 @@ web/assets/swagger.yaml | ||||||
| 
 | 
 | ||||||
| # exludes docker-volume from exemple/docker-compose | # exludes docker-volume from exemple/docker-compose | ||||||
| example/docker-compose/docker-volume | example/docker-compose/docker-volume | ||||||
|  | 
 | ||||||
|  | # excludes debug build | ||||||
|  | cmd/gotosocial/__debug_bin | ||||||
							
								
								
									
										16
									
								
								.vscode/launch.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.vscode/launch.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | { | ||||||
|  | 	"version": "0.2.0", | ||||||
|  | 	"configurations": [ | ||||||
|  | 		{ | ||||||
|  | 			"name": "Launch Package", | ||||||
|  | 			"type": "go", | ||||||
|  | 			"request": "launch", | ||||||
|  | 			"mode": "auto", | ||||||
|  | 			"program": "${workspaceFolder}/cmd/gotosocial", | ||||||
|  | 			"args": [ | ||||||
|  | 				"testrig", "start" | ||||||
|  | 			], | ||||||
|  | 			"cwd": "${workspaceFolder}" | ||||||
|  | 		} | ||||||
|  | 	] | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | { | ||||||
|  | 	"go.lintTool":"golangci-lint", | ||||||
|  | 	"go.lintFlags": [ | ||||||
|  | 	  "--fast" | ||||||
|  | 	] | ||||||
|  | } | ||||||
|  | @ -165,7 +165,7 @@ var Start action.GTSAction = func(ctx context.Context) error { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// build client api modules | 	// build client api modules | ||||||
| 	authModule := auth.New(dbService, oauthServer, idp) | 	authModule := auth.New(dbService, oauthServer, idp, processor) | ||||||
| 	accountModule := account.New(processor) | 	accountModule := account.New(processor) | ||||||
| 	instanceModule := instance.New(processor) | 	instanceModule := instance.New(processor) | ||||||
| 	appsModule := app.New(processor) | 	appsModule := app.New(processor) | ||||||
|  |  | ||||||
|  | @ -108,7 +108,7 @@ var Start action.GTSAction = func(ctx context.Context) error { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// build client api modules | 	// build client api modules | ||||||
| 	authModule := auth.New(dbService, oauthServer, idp) | 	authModule := auth.New(dbService, oauthServer, idp, processor) | ||||||
| 	accountModule := account.New(processor) | 	accountModule := account.New(processor) | ||||||
| 	instanceModule := instance.New(processor) | 	instanceModule := instance.New(processor) | ||||||
| 	appsModule := app.New(processor) | 	appsModule := app.New(processor) | ||||||
|  |  | ||||||
|  | @ -692,7 +692,7 @@ definitions: | ||||||
|       text_url: |       text_url: | ||||||
|         description: |- |         description: |- | ||||||
|           A shorter URL for the attachment. |           A shorter URL for the attachment. | ||||||
|           Not currently used. |           In our case, we just give the URL again since we don't create smaller URLs. | ||||||
|         type: string |         type: string | ||||||
|         x-go-name: TextURL |         x-go-name: TextURL | ||||||
|       type: |       type: | ||||||
|  | @ -1894,8 +1894,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|         "500": |         "500": | ||||||
|           description: internal error |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Application: |       - OAuth2 Application: | ||||||
|         - write:accounts |         - write:accounts | ||||||
|  | @ -1924,6 +1926,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:accounts |         - read:accounts | ||||||
|  | @ -1952,6 +1958,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:blocks |         - write:blocks | ||||||
|  | @ -1999,6 +2009,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:follows |         - write:follows | ||||||
|  | @ -2029,6 +2043,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:accounts |         - read:accounts | ||||||
|  | @ -2059,6 +2077,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:accounts |         - read:accounts | ||||||
|  | @ -2134,6 +2156,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:accounts |         - read:accounts | ||||||
|  | @ -2162,6 +2188,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:blocks |         - write:blocks | ||||||
|  | @ -2190,6 +2220,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:follows |         - write:follows | ||||||
|  | @ -2215,6 +2249,12 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:accounts |         - write:accounts | ||||||
|  | @ -2247,6 +2287,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:accounts |         - read:accounts | ||||||
|  | @ -2313,6 +2357,12 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:accounts |         - write:accounts | ||||||
|  | @ -2335,6 +2385,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:accounts |         - read:accounts | ||||||
|  | @ -2372,6 +2426,12 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "403": |         "403": | ||||||
|           description: forbidden |           description: forbidden | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - admin |         - admin | ||||||
|  | @ -2406,10 +2466,18 @@ paths: | ||||||
|             $ref: '#/definitions/emoji' |             $ref: '#/definitions/emoji' | ||||||
|         "400": |         "400": | ||||||
|           description: bad request |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|         "403": |         "403": | ||||||
|           description: forbidden |           description: forbidden | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|         "409": |         "409": | ||||||
|           description: conflict -- domain/shortcode combo for emoji already exists |           description: conflict -- domain/shortcode combo for emoji already exists | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - admin |         - admin | ||||||
|  | @ -2439,10 +2507,16 @@ paths: | ||||||
|             type: array |             type: array | ||||||
|         "400": |         "400": | ||||||
|           description: bad request |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|         "403": |         "403": | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - admin |         - admin | ||||||
|  | @ -2511,8 +2585,16 @@ paths: | ||||||
|             $ref: '#/definitions/domainBlock' |             $ref: '#/definitions/domainBlock' | ||||||
|         "400": |         "400": | ||||||
|           description: bad request |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|         "403": |         "403": | ||||||
|           description: forbidden |           description: forbidden | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - admin |         - admin | ||||||
|  | @ -2537,10 +2619,16 @@ paths: | ||||||
|             $ref: '#/definitions/domainBlock' |             $ref: '#/definitions/domainBlock' | ||||||
|         "400": |         "400": | ||||||
|           description: bad request |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|         "403": |         "403": | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - admin |         - admin | ||||||
|  | @ -2564,10 +2652,16 @@ paths: | ||||||
|             $ref: '#/definitions/domainBlock' |             $ref: '#/definitions/domainBlock' | ||||||
|         "400": |         "400": | ||||||
|           description: bad request |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|         "403": |         "403": | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - admin |         - admin | ||||||
|  | @ -2599,8 +2693,16 @@ paths: | ||||||
|             asynchronously after the request completes. |             asynchronously after the request completes. | ||||||
|         "400": |         "400": | ||||||
|           description: bad request |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|         "403": |         "403": | ||||||
|           description: forbidden |           description: forbidden | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - admin |         - admin | ||||||
|  | @ -2660,10 +2762,14 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "422": |         "403": | ||||||
|           description: unprocessable |           description: forbidden | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|         "500": |         "500": | ||||||
|           description: internal error |           description: internal server error | ||||||
|       summary: Register a new application on this instance. |       summary: Register a new application on this instance. | ||||||
|       tags: |       tags: | ||||||
|       - apps |       - apps | ||||||
|  | @ -2714,6 +2820,10 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:blocks |         - read:blocks | ||||||
|  | @ -2753,10 +2863,12 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "403": |  | ||||||
|           description: forbidden |  | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:follows |         - read:follows | ||||||
|  | @ -2785,10 +2897,10 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "403": |  | ||||||
|           description: forbidden |  | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|         "500": |         "500": | ||||||
|           description: internal server error |           description: internal server error | ||||||
|       security: |       security: | ||||||
|  | @ -2817,10 +2929,10 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "403": |  | ||||||
|           description: forbidden |  | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|         "500": |         "500": | ||||||
|           description: internal server error |           description: internal server error | ||||||
|       security: |       security: | ||||||
|  | @ -2843,6 +2955,8 @@ paths: | ||||||
|           description: Instance information. |           description: Instance information. | ||||||
|           schema: |           schema: | ||||||
|             $ref: '#/definitions/instance' |             $ref: '#/definitions/instance' | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|         "500": |         "500": | ||||||
|           description: internal error |           description: internal error | ||||||
|       summary: View instance information. |       summary: View instance information. | ||||||
|  | @ -2909,6 +3023,14 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|  |         "403": | ||||||
|  |           description: forbidden | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - admin |         - admin | ||||||
|  | @ -2952,10 +3074,10 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "403": |  | ||||||
|           description: forbidden |  | ||||||
|         "422": |         "422": | ||||||
|           description: unprocessable |           description: unprocessable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:media |         - write:media | ||||||
|  | @ -2982,10 +3104,12 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "403": |         "404": | ||||||
|           description: forbidden |           description: not found | ||||||
|         "422": |         "406": | ||||||
|           description: unprocessable |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:media |         - read:media | ||||||
|  | @ -3036,10 +3160,12 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "403": |         "404": | ||||||
|           description: forbidden |           description: not found | ||||||
|         "422": |         "406": | ||||||
|           description: unprocessable |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:media |         - write:media | ||||||
|  | @ -3141,6 +3267,12 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:search |         - read:search | ||||||
|  | @ -3226,10 +3358,14 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|  |         "403": | ||||||
|  |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|         "500": |         "500": | ||||||
|           description: internal error |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:statuses |         - write:statuses | ||||||
|  | @ -3263,6 +3399,10 @@ paths: | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:statuses |         - write:statuses | ||||||
|  | @ -3288,10 +3428,14 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "401": |         "401": | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|  |         "403": | ||||||
|  |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|         "500": |         "500": | ||||||
|           description: internal error |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:statuses |         - read:statuses | ||||||
|  | @ -3324,6 +3468,10 @@ paths: | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:statuses |         - read:statuses | ||||||
|  | @ -3354,6 +3502,10 @@ paths: | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:statuses |         - write:statuses | ||||||
|  | @ -3386,6 +3538,10 @@ paths: | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - read:accounts |         - read:accounts | ||||||
|  | @ -3419,6 +3575,10 @@ paths: | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:statuses |         - write:statuses | ||||||
|  | @ -3481,6 +3641,10 @@ paths: | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:statuses |         - write:statuses | ||||||
|  | @ -3511,6 +3675,10 @@ paths: | ||||||
|           description: forbidden |           description: forbidden | ||||||
|         "404": |         "404": | ||||||
|           description: not found |           description: not found | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - write:statuses |         - write:statuses | ||||||
|  | @ -3778,6 +3946,8 @@ paths: | ||||||
|           description: unauthorized |           description: unauthorized | ||||||
|         "403": |         "403": | ||||||
|           description: forbidden |           description: forbidden | ||||||
|  |         "406": | ||||||
|  |           description: not acceptable | ||||||
|         "500": |         "500": | ||||||
|           description: internal error |           description: internal error | ||||||
|       security: |       security: | ||||||
|  |  | ||||||
|  | @ -23,12 +23,11 @@ import ( | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/validate" | 	"github.com/superseriousbusiness/gotosocial/internal/validate" | ||||||
| ) | ) | ||||||
|  | @ -61,58 +60,51 @@ import ( | ||||||
| //     description: "An OAuth2 access token for the newly-created account." | //     description: "An OAuth2 access token for the newly-created account." | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/oauthToken" | //       "$ref": "#/definitions/oauthToken" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
| //   '500': | //   '500': | ||||||
| //      description: internal error | //      description: internal server error | ||||||
| func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { | func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "accountCreatePOSTHandler") |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, false, false) | 	authed, err := oauth.Authed(c, true, true, false, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Trace("parsing request form") |  | ||||||
| 	form := &model.AccountCreateRequest{} | 	form := &model.AccountCreateRequest{} | ||||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		l.Debugf("could not parse form from request: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Tracef("validating form %+v", form) |  | ||||||
| 	if err := validateCreateAccount(form); err != nil { | 	if err := validateCreateAccount(form); err != nil { | ||||||
| 		l.Debugf("error validating form: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	clientIP := c.ClientIP() | 	clientIP := c.ClientIP() | ||||||
| 	l.Tracef("attempting to parse client ip address %s", clientIP) |  | ||||||
| 	signUpIP := net.ParseIP(clientIP) | 	signUpIP := net.ParseIP(clientIP) | ||||||
| 	if signUpIP == nil { | 	if signUpIP == nil { | ||||||
| 		l.Debugf("error validating sign up ip address %s", clientIP) | 		err := errors.New("ip address could not be parsed from request") | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	form.IP = signUpIP | 	form.IP = signUpIP | ||||||
| 
 | 
 | ||||||
| 	ti, err := m.processor.AccountCreate(c.Request.Context(), authed, form) | 	ti, errWithCode := m.processor.AccountCreate(c.Request.Context(), authed, form) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Errorf("internal server error while creating new account: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -122,6 +114,10 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { | ||||||
| // validateCreateAccount checks through all the necessary prerequisites for creating a new account, | // validateCreateAccount checks through all the necessary prerequisites for creating a new account, | ||||||
| // according to the provided account create request. If the account isn't eligible, an error will be returned. | // according to the provided account create request. If the account isn't eligible, an error will be returned. | ||||||
| func validateCreateAccount(form *model.AccountCreateRequest) error { | func validateCreateAccount(form *model.AccountCreateRequest) error { | ||||||
|  | 	if form == nil { | ||||||
|  | 		return errors.New("form was nil") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if !config.GetAccountsRegistrationOpen() { | 	if !config.GetAccountsRegistrationOpen() { | ||||||
| 		return errors.New("registration is not open for this server") | 		return errors.New("registration is not open for this server") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -19,12 +19,13 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -57,32 +58,35 @@ import ( | ||||||
| //      description: bad request | //      description: bad request | ||||||
| //   '401': | //   '401': | ||||||
| //      description: unauthorized | //      description: unauthorized | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountDeletePOSTHandler(c *gin.Context) { | func (m *Module) AccountDeletePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "AccountDeletePOSTHandler") |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Tracef("retrieved account %+v", authed.Account.ID) |  | ||||||
| 
 | 
 | ||||||
| 	form := &model.AccountDeleteRequest{} | 	form := &model.AccountDeleteRequest{} | ||||||
| 	if err := c.ShouldBind(&form); err != nil { | 	if err := c.ShouldBind(&form); err != nil { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if form.Password == "" { | 	if form.Password == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no password provided in account delete request"}) | 		err = errors.New("no password provided in account delete request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	form.DeleteOriginID = authed.Account.ID | 	form.DeleteOriginID = authed.Account.ID | ||||||
| 
 | 
 | ||||||
| 	if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil { | 	if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil { | ||||||
| 		l.Debugf("could not delete account: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -53,34 +54,38 @@ import ( | ||||||
| //   '200': | //   '200': | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/account" | //       "$ref": "#/definitions/account" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountGETHandler(c *gin.Context) { | func (m *Module) AccountGETHandler(c *gin.Context) { | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetAcctID := c.Param(IDKey) | 	targetAcctID := c.Param(IDKey) | ||||||
| 	if targetAcctID == "" { | 	if targetAcctID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 		err := errors.New("no account id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID) | 	acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		logrus.Debug(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,15 +19,15 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -98,68 +98,67 @@ import ( | ||||||
| //     description: "The newly updated account." | //     description: "The newly updated account." | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/account" | //       "$ref": "#/definitions/account" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { | func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "accountUpdateCredentialsPATCHHandler") |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Tracef("retrieved account %+v", authed.Account.ID) |  | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	form, err := parseUpdateAccountForm(c) | 	form, err := parseUpdateAccountForm(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// if everything on the form is nil, then nothing has been set and we shouldn't continue | 	acctSensitive, errWithCode := m.processor.AccountUpdate(c.Request.Context(), authed, form) | ||||||
| 	if form.Discoverable == nil && | 	if errWithCode != nil { | ||||||
| 		form.Bot == nil && | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		form.DisplayName == nil && |  | ||||||
| 		form.Note == nil && |  | ||||||
| 		form.Avatar == nil && |  | ||||||
| 		form.Header == nil && |  | ||||||
| 		form.Locked == nil && |  | ||||||
| 		form.Source.Privacy == nil && |  | ||||||
| 		form.Source.Sensitive == nil && |  | ||||||
| 		form.Source.Language == nil && |  | ||||||
| 		form.FieldsAttributes == nil { |  | ||||||
| 		l.Debugf("could not parse form from request") |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	acctSensitive, err := m.processor.AccountUpdate(c.Request.Context(), authed, form) |  | ||||||
| 	if err != nil { |  | ||||||
| 		l.Debugf("could not update account: %s", err) |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	l.Tracef("conversion successful, returning OK and apisensitive account %+v", acctSensitive) |  | ||||||
| 	c.JSON(http.StatusOK, acctSensitive) | 	c.JSON(http.StatusOK, acctSensitive) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) { | func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) { | ||||||
| 	// parse main fields from request |  | ||||||
| 	form := &model.UpdateCredentialsRequest{ | 	form := &model.UpdateCredentialsRequest{ | ||||||
| 		Source: &model.UpdateSource{}, | 		Source: &model.UpdateSource{}, | ||||||
| 	} | 	} | ||||||
| 	if err := c.ShouldBind(&form); err != nil || form == nil { | 
 | ||||||
|  | 	if err := c.ShouldBind(&form); err != nil { | ||||||
| 		return nil, fmt.Errorf("could not parse form from request: %s", err) | 		return nil, fmt.Errorf("could not parse form from request: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if form == nil || | ||||||
|  | 		(form.Discoverable == nil && | ||||||
|  | 			form.Bot == nil && | ||||||
|  | 			form.DisplayName == nil && | ||||||
|  | 			form.Note == nil && | ||||||
|  | 			form.Avatar == nil && | ||||||
|  | 			form.Header == nil && | ||||||
|  | 			form.Locked == nil && | ||||||
|  | 			form.Source.Privacy == nil && | ||||||
|  | 			form.Source.Sensitive == nil && | ||||||
|  | 			form.Source.Language == nil && | ||||||
|  | 			form.FieldsAttributes == nil) { | ||||||
|  | 		return nil, errors.New("empty form submitted") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// parse source field-by-field | 	// parse source field-by-field | ||||||
| 	sourceMap := c.PostFormMap("source") | 	sourceMap := c.PostFormMap("source") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -26,7 +26,6 @@ import ( | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/account" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/account" | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | @ -65,7 +64,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() | ||||||
| 
 | 
 | ||||||
| 	// check the response | 	// check the response | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	// unmarshal the returned account | 	// unmarshal the returned account | ||||||
| 	apimodelAccount := &apimodel.Account{} | 	apimodelAccount := &apimodel.Account{} | ||||||
|  | @ -104,7 +103,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUnl | ||||||
| 
 | 
 | ||||||
| 	// check the response | 	// check the response | ||||||
| 	b1, err := ioutil.ReadAll(result1.Body) | 	b1, err := ioutil.ReadAll(result1.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	// unmarshal the returned account | 	// unmarshal the returned account | ||||||
| 	apimodelAccount1 := &apimodel.Account{} | 	apimodelAccount1 := &apimodel.Account{} | ||||||
|  | @ -185,7 +184,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerGet | ||||||
| 
 | 
 | ||||||
| 	// check the response | 	// check the response | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	// unmarshal the returned account | 	// unmarshal the returned account | ||||||
| 	apimodelAccount := &apimodel.Account{} | 	apimodelAccount := &apimodel.Account{} | ||||||
|  | @ -227,7 +226,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwo | ||||||
| 
 | 
 | ||||||
| 	// check the response | 	// check the response | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	// unmarshal the returned account | 	// unmarshal the returned account | ||||||
| 	apimodelAccount := &apimodel.Account{} | 	apimodelAccount := &apimodel.Account{} | ||||||
|  | @ -271,7 +270,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerWit | ||||||
| 
 | 
 | ||||||
| 	// check the response | 	// check the response | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	// unmarshal the returned account | 	// unmarshal the returned account | ||||||
| 	apimodelAccount := &apimodel.Account{} | 	apimodelAccount := &apimodel.Account{} | ||||||
|  | @ -313,8 +312,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerEmp | ||||||
| 
 | 
 | ||||||
| 	// check the response | 	// check the response | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 	suite.Equal(`{"error":"empty form submitted"}`, string(b)) | 	suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateSource() { | func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateSource() { | ||||||
|  | @ -348,7 +347,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd | ||||||
| 
 | 
 | ||||||
| 	// check the response | 	// check the response | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	// unmarshal the returned account | 	// unmarshal the returned account | ||||||
| 	apimodelAccount := &apimodel.Account{} | 	apimodelAccount := &apimodel.Account{} | ||||||
|  |  | ||||||
|  | @ -21,10 +21,9 @@ package account | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -47,30 +46,31 @@ import ( | ||||||
| //   '200': | //   '200': | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/account" | //       "$ref": "#/definitions/account" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountVerifyGETHandler(c *gin.Context) { | func (m *Module) AccountVerifyGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "accountVerifyGETHandler") | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	authed, err := oauth.Authed(c, true, false, false, true) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	acctSensitive, err := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID) | 	acctSensitive, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error getting account from processor: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,10 +19,12 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -54,33 +56,38 @@ import ( | ||||||
| //     description: Your relationship to this account. | //     description: Your relationship to this account. | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/accountRelationship" | //       "$ref": "#/definitions/accountRelationship" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountBlockPOSTHandler(c *gin.Context) { | func (m *Module) AccountBlockPOSTHandler(c *gin.Context) { | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetAcctID := c.Param(IDKey) | 	targetAcctID := c.Param(IDKey) | ||||||
| 	if targetAcctID == "" { | 	if targetAcctID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 		err := errors.New("no account id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID) | 	relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,13 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -75,39 +77,45 @@ import ( | ||||||
| //     description: Your relationship to this account. | //     description: Your relationship to this account. | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/accountRelationship" | //       "$ref": "#/definitions/accountRelationship" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountFollowPOSTHandler(c *gin.Context) { | func (m *Module) AccountFollowPOSTHandler(c *gin.Context) { | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetAcctID := c.Param(IDKey) | 	targetAcctID := c.Param(IDKey) | ||||||
| 	if targetAcctID == "" { | 	if targetAcctID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 		err := errors.New("no account id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	form := &model.AccountFollowRequest{} | 	form := &model.AccountFollowRequest{} | ||||||
| 	if err := c.ShouldBind(form); err != nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	form.ID = targetAcctID | 	form.ID = targetAcctID | ||||||
| 
 | 
 | ||||||
| 	relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form) | 	relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,10 +19,12 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -56,33 +58,38 @@ import ( | ||||||
| //       type: array | //       type: array | ||||||
| //       items: | //       items: | ||||||
| //         "$ref": "#/definitions/account" | //         "$ref": "#/definitions/account" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountFollowersGETHandler(c *gin.Context) { | func (m *Module) AccountFollowersGETHandler(c *gin.Context) { | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetAcctID := c.Param(IDKey) | 	targetAcctID := c.Param(IDKey) | ||||||
| 	if targetAcctID == "" { | 	if targetAcctID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 		err := errors.New("no account id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID) | 	followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,10 +19,12 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -56,33 +58,38 @@ import ( | ||||||
| //       type: array | //       type: array | ||||||
| //       items: | //       items: | ||||||
| //         "$ref": "#/definitions/account" | //         "$ref": "#/definitions/account" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountFollowingGETHandler(c *gin.Context) { | func (m *Module) AccountFollowingGETHandler(c *gin.Context) { | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetAcctID := c.Param(IDKey) | 	targetAcctID := c.Param(IDKey) | ||||||
| 	if targetAcctID == "" { | 	if targetAcctID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 		err := errors.New("no account id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID) | 	following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -43,24 +43,25 @@ import ( | ||||||
| //       type: array | //       type: array | ||||||
| //       items: | //       items: | ||||||
| //         "$ref": "#/definitions/accountRelationship" | //         "$ref": "#/definitions/accountRelationship" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { | func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "AccountRelationshipsGETHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error authing: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -69,8 +70,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { | ||||||
| 		// check fallback -- let's be generous and see if maybe it's just set as 'id'? | 		// check fallback -- let's be generous and see if maybe it's just set as 'id'? | ||||||
| 		id := c.Query("id") | 		id := c.Query("id") | ||||||
| 		if id == "" { | 		if id == "" { | ||||||
| 			l.Debug("no account id specified in query") | 			err = errors.New("no account id(s) specified in query") | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		targetAccountIDs = append(targetAccountIDs, id) | 		targetAccountIDs = append(targetAccountIDs, id) | ||||||
|  | @ -80,8 +81,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { | ||||||
| 
 | 
 | ||||||
| 	for _, targetAccountID := range targetAccountIDs { | 	for _, targetAccountID := range targetAccountIDs { | ||||||
| 		r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID) | 		r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID) | ||||||
| 		if err != nil { | 		if errWithCode != nil { | ||||||
| 			c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 			api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		relationships = append(relationships, *r) | 		relationships = append(relationships, *r) | ||||||
|  |  | ||||||
|  | @ -19,13 +19,14 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -110,31 +111,32 @@ import ( | ||||||
| //       type: array | //       type: array | ||||||
| //       items: | //       items: | ||||||
| //         "$ref": "#/definitions/status" | //         "$ref": "#/definitions/status" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "AccountStatusesGETHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, false, false, false, false) | 	authed, err := oauth.Authed(c, false, false, false, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error authing: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetAcctID := c.Param(IDKey) | 	targetAcctID := c.Param(IDKey) | ||||||
| 	if targetAcctID == "" { | 	if targetAcctID == "" { | ||||||
| 		l.Debug("no account id specified in query") | 		err := errors.New("no account id specified") | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -143,8 +145,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | ||||||
| 	if limitString != "" { | 	if limitString != "" { | ||||||
| 		i, err := strconv.ParseInt(limitString, 10, 64) | 		i, err := strconv.ParseInt(limitString, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing limit string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", LimitKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		limit = int(i) | 		limit = int(i) | ||||||
|  | @ -155,8 +157,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | ||||||
| 	if excludeRepliesString != "" { | 	if excludeRepliesString != "" { | ||||||
| 		i, err := strconv.ParseBool(excludeRepliesString) | 		i, err := strconv.ParseBool(excludeRepliesString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing exclude replies string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", ExcludeRepliesKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude replies query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		excludeReplies = i | 		excludeReplies = i | ||||||
|  | @ -167,8 +169,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | ||||||
| 	if excludeReblogsString != "" { | 	if excludeReblogsString != "" { | ||||||
| 		i, err := strconv.ParseBool(excludeReblogsString) | 		i, err := strconv.ParseBool(excludeReblogsString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing exclude reblogs string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", ExcludeReblogsKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude reblogs query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		excludeReblogs = i | 		excludeReblogs = i | ||||||
|  | @ -191,8 +193,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | ||||||
| 	if pinnedString != "" { | 	if pinnedString != "" { | ||||||
| 		i, err := strconv.ParseBool(pinnedString) | 		i, err := strconv.ParseBool(pinnedString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing pinned string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", PinnedKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		pinnedOnly = i | 		pinnedOnly = i | ||||||
|  | @ -203,8 +205,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | ||||||
| 	if mediaOnlyString != "" { | 	if mediaOnlyString != "" { | ||||||
| 		i, err := strconv.ParseBool(mediaOnlyString) | 		i, err := strconv.ParseBool(mediaOnlyString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing media only string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", OnlyMediaKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse media only query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		mediaOnly = i | 		mediaOnly = i | ||||||
|  | @ -215,19 +217,21 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { | ||||||
| 	if publicOnlyString != "" { | 	if publicOnlyString != "" { | ||||||
| 		i, err := strconv.ParseBool(publicOnlyString) | 		i, err := strconv.ParseBool(publicOnlyString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing public only string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", OnlyPublicKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse public only query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		publicOnly = i | 		publicOnly = i | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) | 	resp, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error from processor account statuses get: %s", errWithCode) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	c.JSON(http.StatusOK, statuses) | 	if resp.LinkHeader != "" { | ||||||
|  | 		c.Header("Link", resp.LinkHeader) | ||||||
|  | 	} | ||||||
|  | 	c.JSON(http.StatusOK, resp.Items) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -37,7 +37,47 @@ type AccountStatusesTestSuite struct { | ||||||
| 	AccountStandardTestSuite | 	AccountStandardTestSuite | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() { | func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() { | ||||||
|  | 	// set up the request | ||||||
|  | 	// we're getting statuses of admin | ||||||
|  | 	targetAccount := suite.testAccounts["admin_account"] | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodGet, nil, fmt.Sprintf("/api/v1/accounts/%s/statuses?limit=20&only_media=false&only_public=true", targetAccount.ID), "") | ||||||
|  | 	ctx.Params = gin.Params{ | ||||||
|  | 		gin.Param{ | ||||||
|  | 			Key:   account.IDKey, | ||||||
|  | 			Value: targetAccount.ID, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.accountModule.AccountStatusesGETHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	// 1. we should have OK because our request was valid | ||||||
|  | 	suite.Equal(http.StatusOK, recorder.Code) | ||||||
|  | 
 | ||||||
|  | 	// 2. we should have no error message in the result body | ||||||
|  | 	result := recorder.Result() | ||||||
|  | 	defer result.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	// check the response | ||||||
|  | 	b, err := ioutil.ReadAll(result.Body) | ||||||
|  | 	assert.NoError(suite.T(), err) | ||||||
|  | 
 | ||||||
|  | 	// unmarshal the returned statuses | ||||||
|  | 	apimodelStatuses := []*apimodel.Status{} | ||||||
|  | 	err = json.Unmarshal(b, &apimodelStatuses) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 	suite.NotEmpty(apimodelStatuses) | ||||||
|  | 
 | ||||||
|  | 	for _, s := range apimodelStatuses { | ||||||
|  | 		suite.Equal(apimodel.VisibilityPublic, s.Visibility) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01G36SF3V6Y6V5BF9P4R7PQG7G&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link")) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() { | ||||||
| 	// set up the request | 	// set up the request | ||||||
| 	// we're getting statuses of admin | 	// we're getting statuses of admin | ||||||
| 	targetAccount := suite.testAccounts["admin_account"] | 	targetAccount := suite.testAccounts["admin_account"] | ||||||
|  | @ -74,6 +114,8 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() { | ||||||
| 		suite.NotEmpty(s.MediaAttachments) | 		suite.NotEmpty(s.MediaAttachments) | ||||||
| 		suite.Equal(apimodel.VisibilityPublic, s.Visibility) | 		suite.Equal(apimodel.VisibilityPublic, s.Visibility) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="prev"`, result.Header.Get("link")) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestAccountStatusesTestSuite(t *testing.T) { | func TestAccountStatusesTestSuite(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -19,10 +19,12 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -54,33 +56,38 @@ import ( | ||||||
| //     description: Your relationship to this account. | //     description: Your relationship to this account. | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/accountRelationship" | //       "$ref": "#/definitions/accountRelationship" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) { | func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) { | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetAcctID := c.Param(IDKey) | 	targetAcctID := c.Param(IDKey) | ||||||
| 	if targetAcctID == "" { | 	if targetAcctID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 		err := errors.New("no account id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID) | 	relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,12 +19,12 @@ | ||||||
| package account | package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -56,37 +56,38 @@ import ( | ||||||
| //     description: Your relationship to this account. | //     description: Your relationship to this account. | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/accountRelationship" | //       "$ref": "#/definitions/accountRelationship" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) { | func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "AccountUnfollowPOSTHandler") |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debug(err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetAcctID := c.Param(IDKey) | 	targetAcctID := c.Param(IDKey) | ||||||
| 	if targetAcctID == "" { | 	if targetAcctID == "" { | ||||||
| 		l.Debug(err) | 		err := errors.New("no account id specified") | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID) | 	relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debug(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,12 +19,14 @@ | ||||||
| package admin | package admin | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -72,53 +74,47 @@ import ( | ||||||
| //      description: unauthorized | //      description: unauthorized | ||||||
| //   '403': | //   '403': | ||||||
| //      description: forbidden | //      description: forbidden | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) AccountActionPOSTHandler(c *gin.Context) { | func (m *Module) AccountActionPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "AccountActionPOSTHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// make sure we're authed... |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// with an admin account |  | ||||||
| 	if !authed.User.Admin { | 	if !authed.User.Admin { | ||||||
| 		l.Debugf("user %s not an admin", authed.User.ID) | 		err := fmt.Errorf("user %s not an admin", authed.User.ID) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) | 		api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// extract the form from the request context |  | ||||||
| 	l.Tracef("parsing request form: %+v", c.Request.Form) |  | ||||||
| 	form := &model.AdminAccountActionRequest{} | 	form := &model.AdminAccountActionRequest{} | ||||||
| 	if err := c.ShouldBind(form); err != nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		l.Debugf("error parsing form %+v: %s", c.Request.Form, err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if form.Type == "" { | 	if form.Type == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no type specified"}) | 		err := errors.New("no type specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetAcctID := c.Param(IDKey) | 	targetAcctID := c.Param(IDKey) | ||||||
| 	if targetAcctID == "" { | 	if targetAcctID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) | 		err := errors.New("no account id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	form.TargetAccountID = targetAcctID | 	form.TargetAccountID = targetAcctID | ||||||
| 
 | 
 | ||||||
| 	if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil { | 	if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil { | ||||||
| 		l.Debugf("error performing account action: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,9 +7,9 @@ import ( | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -86,33 +86,33 @@ import ( | ||||||
| //       Note that if a list has been imported, then an `array` of newly created domain blocks will be returned instead. | //       Note that if a list has been imported, then an `array` of newly created domain blocks will be returned instead. | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/domainBlock" | //       "$ref": "#/definitions/domainBlock" | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { | func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "DomainBlocksPOSTHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// make sure we're authed with an admin account |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if !authed.User.Admin { | 	if !authed.User.Admin { | ||||||
| 		l.Debugf("user %s not an admin", authed.User.ID) | 		err := fmt.Errorf("user %s not an admin", authed.User.ID) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) | 		api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -121,49 +121,43 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { | ||||||
| 	if importString != "" { | 	if importString != "" { | ||||||
| 		i, err := strconv.ParseBool(importString) | 		i, err := strconv.ParseBool(importString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing import string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", ImportQueryKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse import query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		imp = i | 		imp = i | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// extract the media create form from the request context |  | ||||||
| 	l.Tracef("parsing request form: %+v", c.Request.Form) |  | ||||||
| 	form := &model.DomainBlockCreateRequest{} | 	form := &model.DomainBlockCreateRequest{} | ||||||
| 	if err := c.ShouldBind(form); err != nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		l.Debugf("error parsing form %+v: %s", c.Request.Form, err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Give the fields on the request form a first pass to make sure the request is superficially valid. |  | ||||||
| 	l.Tracef("validating form %+v", form) |  | ||||||
| 	if err := validateCreateDomainBlock(form, imp); err != nil { | 	if err := validateCreateDomainBlock(form, imp); err != nil { | ||||||
| 		l.Debugf("error validating form: %s", err) | 		err := fmt.Errorf("error validating form: %s", err) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if imp { | 	if imp { | ||||||
| 		// we're importing multiple blocks | 		// we're importing multiple blocks | ||||||
| 		domainBlocks, err := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form) | 		domainBlocks, errWithCode := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form) | ||||||
| 		if err != nil { | 		if errWithCode != nil { | ||||||
| 			l.Debugf("error importing domain blocks: %s", err) | 			api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		c.JSON(http.StatusOK, domainBlocks) | 		c.JSON(http.StatusOK, domainBlocks) | ||||||
| 	} else { | 		return | ||||||
| 		// we're just creating one block |  | ||||||
| 		domainBlock, err := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form) |  | ||||||
| 		if err != nil { |  | ||||||
| 			l.Debugf("error creating domain block: %s", err) |  | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		c.JSON(http.StatusOK, domainBlock) |  | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// we're just creating one block | ||||||
|  | 	domainBlock, errWithCode := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	c.JSON(http.StatusOK, domainBlock) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error { | func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error { | ||||||
|  |  | ||||||
|  | @ -1,11 +1,13 @@ | ||||||
| package admin | package admin | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -36,48 +38,46 @@ import ( | ||||||
| //     description: The domain block that was just deleted. | //     description: The domain block that was just deleted. | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/domainBlock" | //       "$ref": "#/definitions/domainBlock" | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) DomainBlockDELETEHandler(c *gin.Context) { | func (m *Module) DomainBlockDELETEHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "DomainBlockDELETEHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// make sure we're authed with an admin account |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if !authed.User.Admin { | 	if !authed.User.Admin { | ||||||
| 		l.Debugf("user %s not an admin", authed.User.ID) | 		err := fmt.Errorf("user %s not an admin", authed.User.ID) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) | 		api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	domainBlockID := c.Param(IDKey) | 	domainBlockID := c.Param(IDKey) | ||||||
| 	if domainBlockID == "" { | 	if domainBlockID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) | 		err := errors.New("no domain block id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID) | 	domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error deleting domain block: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,14 @@ | ||||||
| package admin | package admin | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -37,41 +39,40 @@ import ( | ||||||
| //     description: The requested domain block. | //     description: The requested domain block. | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/domainBlock" | //       "$ref": "#/definitions/domainBlock" | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) DomainBlockGETHandler(c *gin.Context) { | func (m *Module) DomainBlockGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "DomainBlockGETHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// make sure we're authed with an admin account |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if !authed.User.Admin { | 	if !authed.User.Admin { | ||||||
| 		l.Debugf("user %s not an admin", authed.User.ID) | 		err := fmt.Errorf("user %s not an admin", authed.User.ID) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) | 		api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	domainBlockID := c.Param(IDKey) | 	domainBlockID := c.Param(IDKey) | ||||||
| 	if domainBlockID == "" { | 	if domainBlockID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) | 		err := errors.New("no domain block id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -80,17 +81,16 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) { | ||||||
| 	if exportString != "" { | 	if exportString != "" { | ||||||
| 		i, err := strconv.ParseBool(exportString) | 		i, err := strconv.ParseBool(exportString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing export string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		export = i | 		export = i | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	domainBlock, err := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export) | 	domainBlock, errWithCode := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error getting domain block: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,14 @@ | ||||||
| package admin | package admin | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -43,35 +45,40 @@ import ( | ||||||
| //       type: array | //       type: array | ||||||
| //       items: | //       items: | ||||||
| //         "$ref": "#/definitions/domainBlock" | //         "$ref": "#/definitions/domainBlock" | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) DomainBlocksGETHandler(c *gin.Context) { | func (m *Module) DomainBlocksGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "DomainBlocksGETHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// make sure we're authed with an admin account |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if !authed.User.Admin { | 	if !authed.User.Admin { | ||||||
| 		l.Debugf("user %s not an admin", authed.User.ID) | 		err := fmt.Errorf("user %s not an admin", authed.User.ID) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) | 		api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	domainBlockID := c.Param(IDKey) | ||||||
|  | 	if domainBlockID == "" { | ||||||
|  | 		err := errors.New("no domain block id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -80,17 +87,16 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) { | ||||||
| 	if exportString != "" { | 	if exportString != "" { | ||||||
| 		i, err := strconv.ParseBool(exportString) | 		i, err := strconv.ParseBool(exportString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing export string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		export = i | 		export = i | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	domainBlocks, err := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export) | 	domainBlocks, errWithCode := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error getting domain blocks: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,9 +24,9 @@ import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/validate" | 	"github.com/superseriousbusiness/gotosocial/internal/validate" | ||||||
| ) | ) | ||||||
|  | @ -69,59 +69,52 @@ import ( | ||||||
| //     description: The newly-created emoji. | //     description: The newly-created emoji. | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/emoji" | //       "$ref": "#/definitions/emoji" | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
| //   '409': | //   '409': | ||||||
| //      description: conflict -- domain/shortcode combo for emoji already exists | //      description: conflict -- domain/shortcode combo for emoji already exists | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) { | func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 		"func":        "emojiCreatePOSTHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// make sure we're authed with an admin account |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) // posting a status is serious business so we want *everything* |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if !authed.User.Admin { | 	if !authed.User.Admin { | ||||||
| 		l.Debugf("user %s not an admin", authed.User.ID) | 		err := fmt.Errorf("user %s not an admin", authed.User.ID) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) | 		api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// extract the media create form from the request context |  | ||||||
| 	l.Tracef("parsing request form: %+v", c.Request.Form) |  | ||||||
| 	form := &model.EmojiCreateRequest{} | 	form := &model.EmojiCreateRequest{} | ||||||
| 	if err := c.ShouldBind(form); err != nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		l.Debugf("error parsing form %+v: %s", c.Request.Form, err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Give the fields on the request form a first pass to make sure the request is superficially valid. |  | ||||||
| 	l.Tracef("validating form %+v", form) |  | ||||||
| 	if err := validateCreateEmoji(form); err != nil { | 	if err := validateCreateEmoji(form); err != nil { | ||||||
| 		l.Debugf("error validating form: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form) | 	apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error creating emoji: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -129,7 +122,6 @@ func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func validateCreateEmoji(form *model.EmojiCreateRequest) error { | func validateCreateEmoji(form *model.EmojiCreateRequest) error { | ||||||
| 	// check there actually is an image attached and it's not size 0 |  | ||||||
| 	if form.Image == nil || form.Image.Size == 0 { | 	if form.Image == nil || form.Image.Size == 0 { | ||||||
| 		return errors.New("no emoji given") | 		return errors.New("no emoji given") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -120,7 +120,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() { | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 	suite.NotEmpty(b) | 	suite.NotEmpty(b) | ||||||
| 
 | 
 | ||||||
| 	suite.Equal(`{"error":"conflict: emoji with shortcode rainbow already exists"}`, string(b)) | 	suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists"}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestEmojiCreateTestSuite(t *testing.T) { | func TestEmojiCreateTestSuite(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -23,9 +23,10 @@ import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -54,39 +55,34 @@ import ( | ||||||
| //   '200': | //   '200': | ||||||
| //     description: |- | //     description: |- | ||||||
| //      Echos the number of days requested. The cleanup is performed asynchronously after the request completes. | //      Echos the number of days requested. The cleanup is performed asynchronously after the request completes. | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) { | func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "MediaCleanupPOSTHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// make sure we're authed... |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// with an admin account |  | ||||||
| 	if !authed.User.Admin { | 	if !authed.User.Admin { | ||||||
| 		l.Debugf("user %s not an admin", authed.User.ID) | 		err := fmt.Errorf("user %s not an admin", authed.User.ID) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) | 		api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// extract the form from the request context |  | ||||||
| 	l.Tracef("parsing request form: %+v", c.Request.Form) |  | ||||||
| 	form := &model.MediaCleanupRequest{} | 	form := &model.MediaCleanupRequest{} | ||||||
| 	if err := c.ShouldBind(form); err != nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		l.Debugf("error parsing form %+v: %s", c.Request.Form, err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -101,8 +97,7 @@ func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil { | 	if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil { | ||||||
| 		l.Debugf("error starting prune of remote media: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -22,18 +22,16 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // these consts are used to ensure users can't spam huge entries into our database | ||||||
| const ( | const ( | ||||||
| 	// permitted length for most fields | 	formFieldLen    = 64 | ||||||
| 	formFieldLen = 64 |  | ||||||
| 	// redirect can be a bit bigger because we probably need to encode data in the redirect uri |  | ||||||
| 	formRedirectLen = 512 | 	formRedirectLen = 512 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -64,56 +62,63 @@ const ( | ||||||
| //     description: "The newly-created application." | //     description: "The newly-created application." | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/application" | //       "$ref": "#/definitions/application" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
| //   '422': | //   '401': | ||||||
| //      description: unprocessable | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
| //   '500': | //   '500': | ||||||
| //      description: internal error | //      description: internal server error | ||||||
| func (m *Module) AppsPOSTHandler(c *gin.Context) { | func (m *Module) AppsPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "AppsPOSTHandler") |  | ||||||
| 	l.Trace("entering AppsPOSTHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, false, false, false, false) | 	authed, err := oauth.Authed(c, false, false, false, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	form := &model.ApplicationCreateRequest{} | 	form := &model.ApplicationCreateRequest{} | ||||||
| 	if err := c.ShouldBind(form); err != nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// check lengths of fields before proceeding so the user can't spam huge entries into the database |  | ||||||
| 	if len(form.ClientName) > formFieldLen { | 	if len(form.ClientName) > formFieldLen { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", formFieldLen)}) | 		err := fmt.Errorf("client_name must be less than %d bytes", formFieldLen) | ||||||
| 		return | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 	} |  | ||||||
| 	if len(form.Website) > formFieldLen { |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", formFieldLen)}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if len(form.RedirectURIs) > formRedirectLen { |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", formRedirectLen)}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if len(form.Scopes) > formFieldLen { |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", formFieldLen)}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiApp, err := m.processor.AppCreate(c.Request.Context(), authed, form) | 	if len(form.RedirectURIs) > formRedirectLen { | ||||||
| 	if err != nil { | 		err := fmt.Errorf("redirect_uris must be less than %d bytes", formRedirectLen) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(form.Scopes) > formFieldLen { | ||||||
|  | 		err := fmt.Errorf("scopes must be less than %d bytes", formFieldLen) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(form.Website) > formFieldLen { | ||||||
|  | 		err := fmt.Errorf("website must be less than %d bytes", formFieldLen) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	apiApp, errWithCode := m.processor.AppCreate(c.Request.Context(), authed, form) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oidc" | 	"github.com/superseriousbusiness/gotosocial/internal/oidc" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/processing" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -66,17 +67,19 @@ const ( | ||||||
| 
 | 
 | ||||||
| // Module implements the ClientAPIModule interface for | // Module implements the ClientAPIModule interface for | ||||||
| type Module struct { | type Module struct { | ||||||
| 	db     db.DB | 	db        db.DB | ||||||
| 	server oauth.Server | 	server    oauth.Server | ||||||
| 	idp    oidc.IDP | 	idp       oidc.IDP | ||||||
|  | 	processor processing.Processor | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // New returns a new auth module | // New returns a new auth module | ||||||
| func New(db db.DB, server oauth.Server, idp oidc.IDP) api.ClientModule { | func New(db db.DB, server oauth.Server, idp oidc.IDP, processor processing.Processor) api.ClientModule { | ||||||
| 	return &Module{ | 	return &Module{ | ||||||
| 		db:     db, | 		db:        db, | ||||||
| 		server: server, | 		server:    server, | ||||||
| 		idp:    idp, | 		idp:       idp, | ||||||
|  | 		processor: processor, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,25 +23,37 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 
 | 
 | ||||||
|  | 	"codeberg.org/gruf/go-store/kv" | ||||||
| 	"github.com/gin-contrib/sessions" | 	"github.com/gin-contrib/sessions" | ||||||
| 	"github.com/gin-contrib/sessions/memstore" | 	"github.com/gin-contrib/sessions/memstore" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/auth" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/auth" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/concurrency" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/email" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/federation" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/messages" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oidc" | 	"github.com/superseriousbusiness/gotosocial/internal/oidc" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/processing" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/router" | 	"github.com/superseriousbusiness/gotosocial/internal/router" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type AuthStandardTestSuite struct { | type AuthStandardTestSuite struct { | ||||||
| 	suite.Suite | 	suite.Suite | ||||||
| 	db          db.DB | 	db           db.DB | ||||||
| 	idp         oidc.IDP | 	storage      *kv.KVStore | ||||||
| 	oauthServer oauth.Server | 	mediaManager media.Manager | ||||||
|  | 	federator    federation.Federator | ||||||
|  | 	processor    processing.Processor | ||||||
|  | 	emailSender  email.Sender | ||||||
|  | 	idp          oidc.IDP | ||||||
|  | 	oauthServer  oauth.Server | ||||||
| 
 | 
 | ||||||
| 	// standard suite models | 	// standard suite models | ||||||
| 	testTokens       map[string]*gtsmodel.Token | 	testTokens       map[string]*gtsmodel.Token | ||||||
|  | @ -69,17 +81,26 @@ func (suite *AuthStandardTestSuite) SetupSuite() { | ||||||
| 
 | 
 | ||||||
| func (suite *AuthStandardTestSuite) SetupTest() { | func (suite *AuthStandardTestSuite) SetupTest() { | ||||||
| 	testrig.InitTestConfig() | 	testrig.InitTestConfig() | ||||||
| 	suite.db = testrig.NewTestDB() |  | ||||||
| 	testrig.InitTestLog() | 	testrig.InitTestLog() | ||||||
| 
 | 
 | ||||||
|  | 	fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) | ||||||
|  | 	clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) | ||||||
|  | 
 | ||||||
|  | 	suite.db = testrig.NewTestDB() | ||||||
|  | 	suite.storage = testrig.NewTestStorage() | ||||||
|  | 	suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) | ||||||
|  | 	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) | ||||||
|  | 	suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) | ||||||
|  | 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) | ||||||
|  | 
 | ||||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||||
| 	var err error | 	var err error | ||||||
| 	suite.idp, err = oidc.NewIDP(context.Background()) | 	suite.idp, err = oidc.NewIDP(context.Background()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		panic(err) | 		panic(err) | ||||||
| 	} | 	} | ||||||
| 	suite.authModule = auth.New(suite.db, suite.oauthServer, suite.idp).(*auth.Module) | 	suite.authModule = auth.New(suite.db, suite.oauthServer, suite.idp, suite.processor).(*auth.Module) | ||||||
| 	testrig.StandardDBSetup(suite.db, nil) | 	testrig.StandardDBSetup(suite.db, suite.testAccounts) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *AuthStandardTestSuite) TearDownTest() { | func (suite *AuthStandardTestSuite) TearDownTest() { | ||||||
|  | @ -92,7 +113,7 @@ func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath | ||||||
| 	ctx, engine := gin.CreateTestContext(recorder) | 	ctx, engine := gin.CreateTestContext(recorder) | ||||||
| 
 | 
 | ||||||
| 	// load templates into the engine | 	// load templates into the engine | ||||||
| 	testrig.ConfigureTemplatesWithGin(engine) | 	testrig.ConfigureTemplatesWithGin(engine, "../../../../web/template") | ||||||
| 
 | 
 | ||||||
| 	// create the request | 	// create the request | ||||||
| 	protocol := config.GetProtocol() | 	protocol := config.GetProtocol() | ||||||
|  |  | ||||||
|  | @ -23,9 +23,6 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-contrib/sessions" | 	"github.com/gin-contrib/sessions" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | @ -33,18 +30,22 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // helpfulAdvice is a handy hint to users; | ||||||
|  | // particularly important during the login flow | ||||||
|  | var helpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials" | ||||||
|  | 
 | ||||||
| // AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize | // AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize | ||||||
| // The idea here is to present an oauth authorize page to the user, with a button | // The idea here is to present an oauth authorize page to the user, with a button | ||||||
| // that they have to click to accept. | // that they have to click to accept. | ||||||
| func (m *Module) AuthorizeGETHandler(c *gin.Context) { | func (m *Module) AuthorizeGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "AuthorizeGETHandler") |  | ||||||
| 	s := sessions.Default(c) | 	s := sessions.Default(c) | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { | ||||||
| 		c.HTML(http.StatusNotAcceptable, "error.tmpl", gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -52,56 +53,75 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { | ||||||
| 	// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page. | 	// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page. | ||||||
| 	userID, ok := s.Get(sessionUserID).(string) | 	userID, ok := s.Get(sessionUserID).(string) | ||||||
| 	if !ok || userID == "" { | 	if !ok || userID == "" { | ||||||
| 		l.Trace("userid was empty, parsing form then redirecting to sign in page") |  | ||||||
| 		form := &model.OAuthAuthorize{} | 		form := &model.OAuthAuthorize{} | ||||||
| 		if err := c.Bind(form); err != nil { | 		if err := c.ShouldBind(form); err != nil { | ||||||
| 			l.Debugf("invalid auth form: %s", err) |  | ||||||
| 			m.clearSession(s) | 			m.clearSession(s) | ||||||
| 			c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		l.Debugf("parsed auth form: %+v", form) |  | ||||||
| 
 | 
 | ||||||
| 		if err := extractAuthForm(s, form); err != nil { | 		if errWithCode := saveAuthFormToSession(s, form); errWithCode != nil { | ||||||
| 			l.Debugf(fmt.Sprintf("error parsing form at /oauth/authorize: %s", err)) |  | ||||||
| 			m.clearSession(s) | 			m.clearSession(s) | ||||||
| 			c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()}) | 			api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
| 		c.Redirect(http.StatusSeeOther, AuthSignInPath) | 		c.Redirect(http.StatusSeeOther, AuthSignInPath) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// We can use the client_id on the session to retrieve info about the app associated with the client_id | 	// use session information to validate app, user, and account for this request | ||||||
| 	clientID, ok := s.Get(sessionClientID).(string) | 	clientID, ok := s.Get(sessionClientID).(string) | ||||||
| 	if !ok || clientID == "" { | 	if !ok || clientID == "" { | ||||||
| 		c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no client_id found in session"}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	app := >smodel.Application{} |  | ||||||
| 	if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { |  | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ | 		err := fmt.Errorf("key %s was not found in session", sessionClientID) | ||||||
| 			"error": fmt.Sprintf("no application found for client id %s", clientID), | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) | ||||||
| 		}) | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	app := >smodel.Application{} | ||||||
|  | 	if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { | ||||||
|  | 		m.clearSession(s) | ||||||
|  | 		safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID) | ||||||
|  | 		var errWithCode gtserror.WithCode | ||||||
|  | 		if err == db.ErrNoEntries { | ||||||
|  | 			errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) | ||||||
|  | 		} else { | ||||||
|  | 			errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) | ||||||
|  | 		} | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// redirect the user if they have not confirmed their email yet, thier account has not been approved yet, |  | ||||||
| 	// or thier account has been disabled. |  | ||||||
| 	user := >smodel.User{} | 	user := >smodel.User{} | ||||||
| 	if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil { | 	if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) | 		safe := fmt.Sprintf("user with id %s could not be retrieved", userID) | ||||||
|  | 		var errWithCode gtserror.WithCode | ||||||
|  | 		if err == db.ErrNoEntries { | ||||||
|  | 			errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) | ||||||
|  | 		} else { | ||||||
|  | 			errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) | ||||||
|  | 		} | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) | 	acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) | 		safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID) | ||||||
|  | 		var errWithCode gtserror.WithCode | ||||||
|  | 		if err == db.ErrNoEntries { | ||||||
|  | 			errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) | ||||||
|  | 		} else { | ||||||
|  | 			errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) | ||||||
|  | 		} | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if !ensureUserIsAuthorizedOrRedirect(c, user, acct) { | 
 | ||||||
|  | 	if ensureUserIsAuthorizedOrRedirect(c, user, acct) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -109,25 +129,27 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { | ||||||
| 	redirect, ok := s.Get(sessionRedirectURI).(string) | 	redirect, ok := s.Get(sessionRedirectURI).(string) | ||||||
| 	if !ok || redirect == "" { | 	if !ok || redirect == "" { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no redirect_uri found in session"}) | 		err := fmt.Errorf("key %s was not found in session", sessionRedirectURI) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	scope, ok := s.Get(sessionScope).(string) | 	scope, ok := s.Get(sessionScope).(string) | ||||||
| 	if !ok || scope == "" { | 	if !ok || scope == "" { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no scope found in session"}) | 		err := fmt.Errorf("key %s was not found in session", sessionScope) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// the authorize template will display a form to the user where they can get some information | 	// the authorize template will display a form to the user where they can get some information | ||||||
| 	// about the app that's trying to authorize, and the scope of the request. | 	// about the app that's trying to authorize, and the scope of the request. | ||||||
| 	// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler | 	// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler | ||||||
| 	l.Trace("serving authorize html") |  | ||||||
| 	c.HTML(http.StatusOK, "authorize.tmpl", gin.H{ | 	c.HTML(http.StatusOK, "authorize.tmpl", gin.H{ | ||||||
| 		"appname":    app.Name, | 		"appname":    app.Name, | ||||||
| 		"appwebsite": app.Website, | 		"appwebsite": app.Website, | ||||||
| 		"redirect":   redirect, | 		"redirect":   redirect, | ||||||
| 		sessionScope: scope, | 		"scope":      scope, | ||||||
| 		"user":       acct.Username, | 		"user":       acct.Username, | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  | @ -136,13 +158,10 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { | ||||||
| // At this point we assume that the user has A) logged in and B) accepted that the app should act for them, | // At this point we assume that the user has A) logged in and B) accepted that the app should act for them, | ||||||
| // so we should proceed with the authentication flow and generate an oauth token for them if we can. | // so we should proceed with the authentication flow and generate an oauth token for them if we can. | ||||||
| func (m *Module) AuthorizePOSTHandler(c *gin.Context) { | func (m *Module) AuthorizePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "AuthorizePOSTHandler") |  | ||||||
| 	s := sessions.Default(c) | 	s := sessions.Default(c) | ||||||
| 
 | 
 | ||||||
| 	// We need to retrieve the original form submitted to the authorizeGEThandler, and | 	// We need to retrieve the original form submitted to the authorizeGEThandler, and | ||||||
| 	// recreate it on the request so that it can be used further by the oauth2 library. | 	// recreate it on the request so that it can be used further by the oauth2 library. | ||||||
| 	// So first fetch all the values from the session. |  | ||||||
| 
 |  | ||||||
| 	errs := []string{} | 	errs := []string{} | ||||||
| 
 | 
 | ||||||
| 	forceLogin, ok := s.Get(sessionForceLogin).(string) | 	forceLogin, ok := s.Get(sessionForceLogin).(string) | ||||||
|  | @ -152,77 +171,107 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) { | ||||||
| 
 | 
 | ||||||
| 	responseType, ok := s.Get(sessionResponseType).(string) | 	responseType, ok := s.Get(sessionResponseType).(string) | ||||||
| 	if !ok || responseType == "" { | 	if !ok || responseType == "" { | ||||||
| 		errs = append(errs, "session missing response_type") | 		errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionResponseType)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	clientID, ok := s.Get(sessionClientID).(string) | 	clientID, ok := s.Get(sessionClientID).(string) | ||||||
| 	if !ok || clientID == "" { | 	if !ok || clientID == "" { | ||||||
| 		errs = append(errs, "session missing client_id") | 		errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionClientID)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	redirectURI, ok := s.Get(sessionRedirectURI).(string) | 	redirectURI, ok := s.Get(sessionRedirectURI).(string) | ||||||
| 	if !ok || redirectURI == "" { | 	if !ok || redirectURI == "" { | ||||||
| 		errs = append(errs, "session missing redirect_uri") | 		errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionRedirectURI)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	scope, ok := s.Get(sessionScope).(string) | 	scope, ok := s.Get(sessionScope).(string) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		errs = append(errs, "session missing scope") | 		errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionScope)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	userID, ok := s.Get(sessionUserID).(string) | 	userID, ok := s.Get(sessionUserID).(string) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		errs = append(errs, "session missing userid") | 		errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionUserID)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(errs) != 0 { | ||||||
|  | 		errs = append(errs, helpfulAdvice) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during AuthorizePOSTHandler"), errs...), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// redirect the user if they have not confirmed their email yet, thier account has not been approved yet, |  | ||||||
| 	// or thier account has been disabled. |  | ||||||
| 	user := >smodel.User{} | 	user := >smodel.User{} | ||||||
| 	if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil { | 	if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) | 		safe := fmt.Sprintf("user with id %s could not be retrieved", userID) | ||||||
|  | 		var errWithCode gtserror.WithCode | ||||||
|  | 		if err == db.ErrNoEntries { | ||||||
|  | 			errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) | ||||||
|  | 		} else { | ||||||
|  | 			errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) | ||||||
|  | 		} | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) | 	acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) | 		safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID) | ||||||
| 		return | 		var errWithCode gtserror.WithCode | ||||||
| 	} | 		if err == db.ErrNoEntries { | ||||||
| 	if !ensureUserIsAuthorizedOrRedirect(c, user, acct) { | 			errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) | ||||||
|  | 		} else { | ||||||
|  | 			errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) | ||||||
|  | 		} | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if ensureUserIsAuthorizedOrRedirect(c, user, acct) { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// we're done with the session now, so just clear it out | ||||||
| 	m.clearSession(s) | 	m.clearSession(s) | ||||||
| 
 | 
 | ||||||
| 	if len(errs) != 0 { | 	// we have to set the values on the request form | ||||||
| 		c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": strings.Join(errs, ": ")}) | 	// so that they're picked up by the oauth server | ||||||
| 		return | 	c.Request.Form = url.Values{ | ||||||
|  | 		sessionForceLogin:   {forceLogin}, | ||||||
|  | 		sessionResponseType: {responseType}, | ||||||
|  | 		sessionClientID:     {clientID}, | ||||||
|  | 		sessionRedirectURI:  {redirectURI}, | ||||||
|  | 		sessionScope:        {scope}, | ||||||
|  | 		sessionUserID:       {userID}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// now set the values on the request |  | ||||||
| 	values := url.Values{} |  | ||||||
| 	values.Set(sessionForceLogin, forceLogin) |  | ||||||
| 	values.Set(sessionResponseType, responseType) |  | ||||||
| 	values.Set(sessionClientID, clientID) |  | ||||||
| 	values.Set(sessionRedirectURI, redirectURI) |  | ||||||
| 	values.Set(sessionScope, scope) |  | ||||||
| 	values.Set(sessionUserID, userID) |  | ||||||
| 	c.Request.Form = values |  | ||||||
| 	l.Tracef("values on request set to %+v", c.Request.Form) |  | ||||||
| 
 |  | ||||||
| 	// and proceed with authorization using the oauth2 library |  | ||||||
| 	if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil { | 	if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil { | ||||||
| 		c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice), m.processor.InstanceGet) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // extractAuthForm checks the given OAuthAuthorize form, and stores | // saveAuthFormToSession checks the given OAuthAuthorize form, | ||||||
| // the values in the form into the session. | // and stores the values in the form into the session. | ||||||
| func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error { | func saveAuthFormToSession(s sessions.Session, form *model.OAuthAuthorize) gtserror.WithCode { | ||||||
| 	// these fields are *required* so check 'em | 	if form == nil { | ||||||
| 	if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { | 		err := errors.New("OAuthAuthorize form was nil") | ||||||
| 		return errors.New("missing one of: response_type, client_id or redirect_uri") | 		return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if form.ResponseType == "" { | ||||||
|  | 		err := errors.New("field response_type was not set on OAuthAuthorize form") | ||||||
|  | 		return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if form.ClientID == "" { | ||||||
|  | 		err := errors.New("field client_id was not set on OAuthAuthorize form") | ||||||
|  | 		return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if form.RedirectURI == "" { | ||||||
|  | 		err := errors.New("field redirect_uri was not set on OAuthAuthorize form") | ||||||
|  | 		return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// set default scope to read | 	// set default scope to read | ||||||
|  | @ -237,29 +286,33 @@ func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error { | ||||||
| 	s.Set(sessionRedirectURI, form.RedirectURI) | 	s.Set(sessionRedirectURI, form.RedirectURI) | ||||||
| 	s.Set(sessionScope, form.Scope) | 	s.Set(sessionScope, form.Scope) | ||||||
| 	s.Set(sessionState, uuid.NewString()) | 	s.Set(sessionState, uuid.NewString()) | ||||||
| 	return s.Save() | 
 | ||||||
|  | 	if err := s.Save(); err != nil { | ||||||
|  | 		err := fmt.Errorf("error saving form values onto session: %s", err) | ||||||
|  | 		return gtserror.NewErrorInternalError(err, helpfulAdvice) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) bool { | func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) (redirected bool) { | ||||||
| 	if user.ConfirmedAt.IsZero() { | 	if user.ConfirmedAt.IsZero() { | ||||||
| 		ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath) | 		ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath) | ||||||
| 		return false | 		redirected = true | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !user.Approved { | 	if !user.Approved { | ||||||
| 		ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath) | 		ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath) | ||||||
| 		return false | 		redirected = true | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if user.Disabled { | 	if user.Disabled || !account.SuspendedAt.IsZero() { | ||||||
| 		ctx.Redirect(http.StatusSeeOther, AccountDisabledPath) | 		ctx.Redirect(http.StatusSeeOther, AccountDisabledPath) | ||||||
| 		return false | 		redirected = true | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !account.SuspendedAt.IsZero() { | 	return | ||||||
| 		ctx.Redirect(http.StatusSeeOther, AccountDisabledPath) |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return true |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -30,7 +30,9 @@ import ( | ||||||
| 	"github.com/gin-contrib/sessions" | 	"github.com/gin-contrib/sessions" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/google/uuid" | 	"github.com/google/uuid" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oidc" | 	"github.com/superseriousbusiness/gotosocial/internal/oidc" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/validate" | 	"github.com/superseriousbusiness/gotosocial/internal/validate" | ||||||
|  | @ -40,11 +42,14 @@ import ( | ||||||
| func (m *Module) CallbackGETHandler(c *gin.Context) { | func (m *Module) CallbackGETHandler(c *gin.Context) { | ||||||
| 	s := sessions.Default(c) | 	s := sessions.Default(c) | ||||||
| 
 | 
 | ||||||
| 	// first make sure the state set in the cookie is the same as the state returned from the external provider | 	// check the query vs session state parameter to mitigate csrf | ||||||
|  | 	// https://auth0.com/docs/secure/attack-protection/state-parameters | ||||||
|  | 
 | ||||||
| 	state := c.Query(callbackStateParam) | 	state := c.Query(callbackStateParam) | ||||||
| 	if state == "" { | 	if state == "" { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "state query not found on callback"}) | 		err := fmt.Errorf("%s parameter not found on callback query", callbackStateParam) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -52,84 +57,104 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { | ||||||
| 	savedState, ok := savedStateI.(string) | 	savedState, ok := savedStateI.(string) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"}) | 		err := fmt.Errorf("key %s was not found in session", sessionState) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if state != savedState { | 	if state != savedState { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "state mismatch"}) | 		err := errors.New("mismatch between query state and session state") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// retrieve stored claims using code | ||||||
| 	code := c.Query(callbackCodeParam) | 	code := c.Query(callbackCodeParam) | ||||||
| 
 | 	if code == "" { | ||||||
| 	claims, err := m.idp.HandleCallback(c.Request.Context(), code) |  | ||||||
| 	if err != nil { |  | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | 		err := fmt.Errorf("%s parameter not found on callback query", callbackCodeParam) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// We can use the client_id on the session to retrieve info about the app associated with the client_id | 	claims, errWithCode := m.idp.HandleCallback(c.Request.Context(), code) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		m.clearSession(s) | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// We can use the client_id on the session to retrieve | ||||||
|  | 	// info about the app associated with the client_id | ||||||
| 	clientID, ok := s.Get(sessionClientID).(string) | 	clientID, ok := s.Get(sessionClientID).(string) | ||||||
| 	if !ok || clientID == "" { | 	if !ok || clientID == "" { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": "no client_id found in session during callback"}) | 		err := fmt.Errorf("key %s was not found in session", sessionClientID) | ||||||
| 		return | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) | ||||||
| 	} |  | ||||||
| 	app := >smodel.Application{} |  | ||||||
| 	if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { |  | ||||||
| 		m.clearSession(s) |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	user, err := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID) | 	app := >smodel.Application{} | ||||||
| 	if err != nil { | 	if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | 		safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID) | ||||||
|  | 		var errWithCode gtserror.WithCode | ||||||
|  | 		if err == db.ErrNoEntries { | ||||||
|  | 			errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) | ||||||
|  | 		} else { | ||||||
|  | 			errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) | ||||||
|  | 		} | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	user, errWithCode := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		m.clearSession(s) | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	s.Set(sessionUserID, user.ID) | 	s.Set(sessionUserID, user.ID) | ||||||
| 	if err := s.Save(); err != nil { | 	if err := s.Save(); err != nil { | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	c.Redirect(http.StatusFound, OauthAuthorizePath) | 	c.Redirect(http.StatusFound, OauthAuthorizePath) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, error) { | func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) { | ||||||
| 	if claims.Email == "" { | 	if claims.Email == "" { | ||||||
| 		return nil, errors.New("no email returned in claims") | 		err := errors.New("no email returned in claims") | ||||||
|  | 		return nil, gtserror.NewErrorBadRequest(err, err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// see if we already have a user for this email address | 	// see if we already have a user for this email address | ||||||
|  | 	// if so, we don't need to continue + create one | ||||||
| 	user := >smodel.User{} | 	user := >smodel.User{} | ||||||
| 	err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: claims.Email}}, user) | 	err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: claims.Email}}, user) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		// we do! so we can just return it |  | ||||||
| 		return user, nil | 		return user, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err != db.ErrNoEntries { | 	if err != db.ErrNoEntries { | ||||||
| 		// we have an actual error in the database | 		err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err) | ||||||
| 		return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err) | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// maybe we have an unconfirmed user | 	// maybe we have an unconfirmed user | ||||||
| 	err = m.db.GetWhere(ctx, []db.Where{{Key: "unconfirmed_email", Value: claims.Email}}, user) | 	err = m.db.GetWhere(ctx, []db.Where{{Key: "unconfirmed_email", Value: claims.Email}}, user) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		// user is unconfirmed so return an error | 		err := fmt.Errorf("user with email address %s is unconfirmed", claims.Email) | ||||||
| 		return nil, fmt.Errorf("user with email address %s is unconfirmed", claims.Email) | 		return nil, gtserror.NewErrorForbidden(err, err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err != db.ErrNoEntries { | 	if err != db.ErrNoEntries { | ||||||
| 		// we have an actual error in the database | 		err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err) | ||||||
| 		return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err) | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// we don't have a confirmed or unconfirmed user with the claimed email address | 	// we don't have a confirmed or unconfirmed user with the claimed email address | ||||||
|  | @ -138,10 +163,10 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i | ||||||
| 	// check if the email address is available for use; if it's not there's nothing we can so | 	// check if the email address is available for use; if it's not there's nothing we can so | ||||||
| 	emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email) | 	emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("email %s not available: %s", claims.Email, err) | 		return nil, gtserror.NewErrorBadRequest(err) | ||||||
| 	} | 	} | ||||||
| 	if !emailAvailable { | 	if !emailAvailable { | ||||||
| 		return nil, fmt.Errorf("email %s in use", claims.Email) | 		return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", claims.Email)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// now we need a username | 	// now we need a username | ||||||
|  | @ -149,12 +174,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i | ||||||
| 
 | 
 | ||||||
| 	// make sure claims.Name is defined since we'll be using that for the username | 	// make sure claims.Name is defined since we'll be using that for the username | ||||||
| 	if claims.Name == "" { | 	if claims.Name == "" { | ||||||
| 		return nil, errors.New("no name returned in claims") | 		err := errors.New("no name returned in claims") | ||||||
|  | 		return nil, gtserror.NewErrorBadRequest(err, err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// check if we can just use claims.Name as-is | 	// check if we can just use claims.Name as-is | ||||||
| 	err = validate.Username(claims.Name) | 	if err = validate.Username(claims.Name); err == nil { | ||||||
| 	if err == nil { |  | ||||||
| 		// the name we have on the claims is already a valid username | 		// the name we have on the claims is already a valid username | ||||||
| 		username = claims.Name | 		username = claims.Name | ||||||
| 	} else { | 	} else { | ||||||
|  | @ -166,12 +191,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i | ||||||
| 		// lowercase the whole thing | 		// lowercase the whole thing | ||||||
| 		lower := strings.ToLower(underscored) | 		lower := strings.ToLower(underscored) | ||||||
| 		// see if this is valid.... | 		// see if this is valid.... | ||||||
| 		if err := validate.Username(lower); err == nil { | 		if err := validate.Username(lower); err != nil { | ||||||
| 			// we managed to get a valid username | 			err := fmt.Errorf("couldn't parse a valid username from claims.Name value of %s: %s", claims.Name, err) | ||||||
| 			username = lower | 			return nil, gtserror.NewErrorBadRequest(err, err.Error()) | ||||||
| 		} else { |  | ||||||
| 			return nil, fmt.Errorf("couldn't parse a valid username from claims.Name value of %s", claims.Name) |  | ||||||
| 		} | 		} | ||||||
|  | 		// we managed to get a valid username | ||||||
|  | 		username = lower | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var iString string | 	var iString string | ||||||
|  | @ -185,7 +210,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i | ||||||
| 	for i := 1; !found; i++ { | 	for i := 1; !found; i++ { | ||||||
| 		usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString) | 		usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, gtserror.NewErrorInternalError(err) | ||||||
| 		} | 		} | ||||||
| 		if usernameAvailable { | 		if usernameAvailable { | ||||||
| 			// no error so we've found a username that works | 			// no error so we've found a username that works | ||||||
|  | @ -223,7 +248,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i | ||||||
| 	// create the user! this will also create an account and store it in the database so we don't need to do that here | 	// create the user! this will also create an account and store it in the database so we don't need to do that here | ||||||
| 	user, err = m.db.NewSignup(ctx, username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, admin) | 	user, err = m.db.NewSignup(ctx, username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, admin) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error creating user: %s", err) | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return user, nil | 	return user, nil | ||||||
|  |  | ||||||
|  | @ -21,14 +21,14 @@ package auth | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-contrib/sessions" | 	"github.com/gin-contrib/sessions" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
|  | @ -41,64 +41,62 @@ type login struct { | ||||||
| 
 | 
 | ||||||
| // SignInGETHandler should be served at https://example.org/auth/sign_in. | // SignInGETHandler should be served at https://example.org/auth/sign_in. | ||||||
| // The idea is to present a sign in page to the user, where they can enter their username and password. | // The idea is to present a sign in page to the user, where they can enter their username and password. | ||||||
| // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler | // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler. | ||||||
|  | // If an idp provider is set, then the user will be redirected to that to do their sign in. | ||||||
| func (m *Module) SignInGETHandler(c *gin.Context) { | func (m *Module) SignInGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "SignInGETHandler") |  | ||||||
| 	l.Trace("entering sign in handler") |  | ||||||
| 
 |  | ||||||
| 	if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if m.idp != nil { | 	if m.idp == nil { | ||||||
| 		s := sessions.Default(c) | 		// no idp provider, use our own funky little sign in page | ||||||
| 
 | 		c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{}) | ||||||
| 		stateI := s.Get(sessionState) |  | ||||||
| 		state, ok := stateI.(string) |  | ||||||
| 		if !ok { |  | ||||||
| 			m.clearSession(s) |  | ||||||
| 			c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"}) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		redirect := m.idp.AuthCodeURL(state) |  | ||||||
| 		l.Debugf("redirecting to external idp at %s", redirect) |  | ||||||
| 		c.Redirect(http.StatusSeeOther, redirect) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{}) | 
 | ||||||
|  | 	// idp provider is in use, so redirect to it | ||||||
|  | 	s := sessions.Default(c) | ||||||
|  | 
 | ||||||
|  | 	stateI := s.Get(sessionState) | ||||||
|  | 	state, ok := stateI.(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		m.clearSession(s) | ||||||
|  | 		err := fmt.Errorf("key %s was not found in session", sessionState) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.Redirect(http.StatusSeeOther, m.idp.AuthCodeURL(state)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SignInPOSTHandler should be served at https://example.org/auth/sign_in. | // SignInPOSTHandler should be served at https://example.org/auth/sign_in. | ||||||
| // The idea is to present a sign in page to the user, where they can enter their username and password. | // The idea is to present a sign in page to the user, where they can enter their username and password. | ||||||
| // The handler will then redirect to the auth handler served at /auth | // The handler will then redirect to the auth handler served at /auth | ||||||
| func (m *Module) SignInPOSTHandler(c *gin.Context) { | func (m *Module) SignInPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "SignInPOSTHandler") |  | ||||||
| 	s := sessions.Default(c) | 	s := sessions.Default(c) | ||||||
|  | 
 | ||||||
| 	form := &login{} | 	form := &login{} | ||||||
| 	if err := c.ShouldBind(form); err != nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 		m.clearSession(s) | 		m.clearSession(s) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Tracef("parsed form: %+v", form) |  | ||||||
| 
 | 
 | ||||||
| 	userid, err := m.ValidatePassword(c.Request.Context(), form.Email, form.Password) | 	userid, errWithCode := m.ValidatePassword(c.Request.Context(), form.Email, form.Password) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		c.String(http.StatusForbidden, err.Error()) | 		// don't clear session here, so the user can just press back and try again | ||||||
| 		m.clearSession(s) | 		// if they accidentally gave the wrong password or something | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	s.Set(sessionUserID, userid) | 	s.Set(sessionUserID, userid) | ||||||
| 	if err := s.Save(); err != nil { | 	if err := s.Save(); err != nil { | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | 		err := fmt.Errorf("error saving user id onto session: %s", err) | ||||||
| 		m.clearSession(s) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet) | ||||||
| 		return |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Trace("redirecting to auth page") |  | ||||||
| 	c.Redirect(http.StatusFound, OauthAuthorizePath) | 	c.Redirect(http.StatusFound, OauthAuthorizePath) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -106,42 +104,34 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) { | ||||||
| // The goal is to authenticate the password against the one for that email | // The goal is to authenticate the password against the one for that email | ||||||
| // address stored in the database. If OK, we return the userid (a ulid) for that user, | // address stored in the database. If OK, we return the userid (a ulid) for that user, | ||||||
| // so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. | // so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. | ||||||
| func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (userid string, err error) { | func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (string, gtserror.WithCode) { | ||||||
| 	l := logrus.WithField("func", "ValidatePassword") |  | ||||||
| 
 |  | ||||||
| 	// make sure an email/password was provided and bail if not |  | ||||||
| 	if email == "" || password == "" { | 	if email == "" || password == "" { | ||||||
| 		l.Debug("email or password was not provided") | 		err := errors.New("email or password was not provided") | ||||||
| 		return incorrectPassword() | 		return incorrectPassword(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// first we select the user from the database based on email address, bail if no user found for that email | 	user := >smodel.User{} | ||||||
| 	gtsUser := >smodel.User{} | 	if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, user); err != nil { | ||||||
| 
 | 		err := fmt.Errorf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) | ||||||
| 	if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, gtsUser); err != nil { | 		return incorrectPassword(err) | ||||||
| 		l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) |  | ||||||
| 		return incorrectPassword() |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// make sure a password is actually set and bail if not | 	if user.EncryptedPassword == "" { | ||||||
| 	if gtsUser.EncryptedPassword == "" { | 		err := fmt.Errorf("encrypted password for user %s was empty for some reason", user.Email) | ||||||
| 		l.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email) | 		return incorrectPassword(err) | ||||||
| 		return incorrectPassword() |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// compare the provided password with the encrypted one from the db, bail if they don't match | 	if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil { | ||||||
| 	if err := bcrypt.CompareHashAndPassword([]byte(gtsUser.EncryptedPassword), []byte(password)); err != nil { | 		err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err) | ||||||
| 		l.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err) | 		return incorrectPassword(err) | ||||||
| 		return incorrectPassword() |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// If we've made it this far the email/password is correct, so we can just return the id of the user. | 	return user.ID, nil | ||||||
| 	userid = gtsUser.ID |  | ||||||
| 	l.Tracef("returning (%s, %s)", userid, err) |  | ||||||
| 	return |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // incorrectPassword is just a little helper function to use in the ValidatePassword function | // incorrectPassword wraps the given error in a gtserror.WithCode, and returns | ||||||
| func incorrectPassword() (string, error) { | // only a generic 'safe' error message to the user, to not give any info away. | ||||||
| 	return "", errors.New("password/email combination was incorrect") | func incorrectPassword(err error) (string, gtserror.WithCode) { | ||||||
|  | 	safeErr := fmt.Errorf("password/email combination was incorrect") | ||||||
|  | 	return "", gtserror.NewErrorUnauthorized(err, safeErr.Error(), helpfulAdvice) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,11 +19,10 @@ | ||||||
| package auth | package auth | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/http" |  | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  | @ -40,38 +39,40 @@ type tokenBody struct { | ||||||
| // TokenPOSTHandler should be served as a POST at https://example.org/oauth/token | // TokenPOSTHandler should be served as a POST at https://example.org/oauth/token | ||||||
| // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. | // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. | ||||||
| func (m *Module) TokenPOSTHandler(c *gin.Context) { | func (m *Module) TokenPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "TokenPOSTHandler") |  | ||||||
| 	l.Trace("entered TokenPOSTHandler") |  | ||||||
| 
 |  | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	form := &tokenBody{} | 	form := &tokenBody{} | ||||||
| 	if err := c.ShouldBind(form); err == nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		c.Request.Form = url.Values{} | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) | ||||||
| 		if form.ClientID != nil { | 		return | ||||||
| 			c.Request.Form.Set("client_id", *form.ClientID) |  | ||||||
| 		} |  | ||||||
| 		if form.ClientSecret != nil { |  | ||||||
| 			c.Request.Form.Set("client_secret", *form.ClientSecret) |  | ||||||
| 		} |  | ||||||
| 		if form.Code != nil { |  | ||||||
| 			c.Request.Form.Set("code", *form.Code) |  | ||||||
| 		} |  | ||||||
| 		if form.GrantType != nil { |  | ||||||
| 			c.Request.Form.Set("grant_type", *form.GrantType) |  | ||||||
| 		} |  | ||||||
| 		if form.RedirectURI != nil { |  | ||||||
| 			c.Request.Form.Set("redirect_uri", *form.RedirectURI) |  | ||||||
| 		} |  | ||||||
| 		if form.Scope != nil { |  | ||||||
| 			c.Request.Form.Set("scope", *form.Scope) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	c.Request.Form = url.Values{} | ||||||
|  | 	if form.ClientID != nil { | ||||||
|  | 		c.Request.Form.Set("client_id", *form.ClientID) | ||||||
|  | 	} | ||||||
|  | 	if form.ClientSecret != nil { | ||||||
|  | 		c.Request.Form.Set("client_secret", *form.ClientSecret) | ||||||
|  | 	} | ||||||
|  | 	if form.Code != nil { | ||||||
|  | 		c.Request.Form.Set("code", *form.Code) | ||||||
|  | 	} | ||||||
|  | 	if form.GrantType != nil { | ||||||
|  | 		c.Request.Form.Set("grant_type", *form.GrantType) | ||||||
|  | 	} | ||||||
|  | 	if form.RedirectURI != nil { | ||||||
|  | 		c.Request.Form.Set("redirect_uri", *form.RedirectURI) | ||||||
|  | 	} | ||||||
|  | 	if form.Scope != nil { | ||||||
|  | 		c.Request.Form.Set("scope", *form.Scope) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// pass the writer and request into the oauth server handler, which will | ||||||
|  | 	// take care of writing the oauth token into the response etc | ||||||
| 	if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil { | 	if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil { | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,13 +19,13 @@ | ||||||
| package blocks | package blocks | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -80,24 +80,25 @@ import ( | ||||||
| //       type: array | //       type: array | ||||||
| //       items: | //       items: | ||||||
| //         "$ref": "#/definitions/account" | //         "$ref": "#/definitions/account" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) BlocksGETHandler(c *gin.Context) { | func (m *Module) BlocksGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "PublicTimelineGETHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error authing: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -118,8 +119,8 @@ func (m *Module) BlocksGETHandler(c *gin.Context) { | ||||||
| 	if limitString != "" { | 	if limitString != "" { | ||||||
| 		i, err := strconv.ParseInt(limitString, 10, 64) | 		i, err := strconv.ParseInt(limitString, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing limit string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", LimitKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		limit = int(i) | 		limit = int(i) | ||||||
|  | @ -127,8 +128,7 @@ func (m *Module) BlocksGETHandler(c *gin.Context) { | ||||||
| 
 | 
 | ||||||
| 	resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit) | 	resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error from processor BlocksGet: %s", errWithCode) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,18 +5,25 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // EmojisGETHandler returns a list of custom emojis enabled on the instance | // EmojisGETHandler returns a list of custom emojis enabled on the instance | ||||||
| func (m *Module) EmojisGETHandler(c *gin.Context) { | func (m *Module) EmojisGETHandler(c *gin.Context) { | ||||||
|  | 	if _, err := oauth.Authed(c, true, true, true, true); err != nil { | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	emojis, errWithCode := m.processor.CustomEmojisGet(c) | 	emojis, errWithCode := m.processor.CustomEmojisGet(c) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,29 +1,26 @@ | ||||||
| package favourites | package favourites | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // FavouritesGETHandler handles GETting favourites. | // FavouritesGETHandler handles GETting favourites. | ||||||
| func (m *Module) FavouritesGETHandler(c *gin.Context) { | func (m *Module) FavouritesGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "PublicTimelineGETHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error authing: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -44,8 +41,8 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) { | ||||||
| 	if limitString != "" { | 	if limitString != "" { | ||||||
| 		i, err := strconv.ParseInt(limitString, 10, 64) | 		i, err := strconv.ParseInt(limitString, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing limit string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", LimitKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		limit = int(i) | 		limit = int(i) | ||||||
|  | @ -53,13 +50,12 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) { | ||||||
| 
 | 
 | ||||||
| 	resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit) | 	resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error from processor FavedTimelineGet: %s", errWithCode) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if resp.LinkHeader != "" { | 	if resp.LinkHeader != "" { | ||||||
| 		c.Header("Link", resp.LinkHeader) | 		c.Header("Link", resp.LinkHeader) | ||||||
| 	} | 	} | ||||||
| 	c.JSON(http.StatusOK, resp.Statuses) | 	c.JSON(http.StatusOK, resp.Items) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ | ||||||
| package fileserver | package fileserver | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
|  | @ -26,6 +27,7 @@ import ( | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -34,17 +36,9 @@ import ( | ||||||
| // Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found". | // Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found". | ||||||
| // Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything. | // Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything. | ||||||
| func (m *FileServer) ServeFile(c *gin.Context) { | func (m *FileServer) ServeFile(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "ServeFile", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Trace("received request") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, false, false, false, false) | 	authed, err := oauth.Authed(c, false, false, false, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") | 		api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -53,29 +47,29 @@ func (m *FileServer) ServeFile(c *gin.Context) { | ||||||
| 	// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension. | 	// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension. | ||||||
| 	accountID := c.Param(AccountIDKey) | 	accountID := c.Param(AccountIDKey) | ||||||
| 	if accountID == "" { | 	if accountID == "" { | ||||||
| 		l.Debug("missing accountID from request") | 		err := fmt.Errorf("missing %s from request", AccountIDKey) | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") | 		api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mediaType := c.Param(MediaTypeKey) | 	mediaType := c.Param(MediaTypeKey) | ||||||
| 	if mediaType == "" { | 	if mediaType == "" { | ||||||
| 		l.Debug("missing mediaType from request") | 		err := fmt.Errorf("missing %s from request", MediaTypeKey) | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") | 		api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mediaSize := c.Param(MediaSizeKey) | 	mediaSize := c.Param(MediaSizeKey) | ||||||
| 	if mediaSize == "" { | 	if mediaSize == "" { | ||||||
| 		l.Debug("missing mediaSize from request") | 		err := fmt.Errorf("missing %s from request", MediaSizeKey) | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") | 		api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fileName := c.Param(FileNameKey) | 	fileName := c.Param(FileNameKey) | ||||||
| 	if fileName == "" { | 	if fileName == "" { | ||||||
| 		l.Debug("missing fileName from request") | 		err := fmt.Errorf("missing %s from request", FileNameKey) | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") | 		api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -86,8 +80,7 @@ func (m *FileServer) ServeFile(c *gin.Context) { | ||||||
| 		FileName:  fileName, | 		FileName:  fileName, | ||||||
| 	}) | 	}) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Errorf(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -95,7 +88,7 @@ func (m *FileServer) ServeFile(c *gin.Context) { | ||||||
| 		// if the content is a ReadCloser, close it when we're done | 		// if the content is a ReadCloser, close it when we're done | ||||||
| 		if closer, ok := content.Content.(io.ReadCloser); ok { | 		if closer, ok := content.Content.(io.ReadCloser); ok { | ||||||
| 			if err := closer.Close(); err != nil { | 			if err := closer.Close(); err != nil { | ||||||
| 				l.Errorf("error closing readcloser: %s", err) | 				logrus.Errorf("ServeFile: error closing readcloser: %s", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  | @ -103,9 +96,9 @@ func (m *FileServer) ServeFile(c *gin.Context) { | ||||||
| 	// TODO: if the requester only accepts text/html we should try to serve them *something*. | 	// TODO: if the requester only accepts text/html we should try to serve them *something*. | ||||||
| 	// This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will | 	// This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will | ||||||
| 	// attempt to look up the content to provide a preview of the link, and they ask for text/html. | 	// attempt to look up the content to provide a preview of the link, and they ask for text/html. | ||||||
| 	format, err := api.NegotiateAccept(c, api.Offer(content.ContentType)) | 	format, err := api.NegotiateAccept(c, api.MIME(content.ContentType)) | ||||||
| 	if errWithCode != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,12 +5,19 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // FiltersGETHandler returns a list of filters set by/for the authed account | // FiltersGETHandler returns a list of filters set by/for the authed account | ||||||
| func (m *Module) FiltersGETHandler(c *gin.Context) { | func (m *Module) FiltersGETHandler(c *gin.Context) { | ||||||
|  | 	if _, err := oauth.Authed(c, true, true, true, true); err != nil { | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,12 +19,12 @@ | ||||||
| package followrequest | package followrequest | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -62,43 +62,34 @@ import ( | ||||||
| //      description: bad request | //      description: bad request | ||||||
| //   '401': | //   '401': | ||||||
| //      description: unauthorized | //      description: unauthorized | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
| //   '500': | //   '500': | ||||||
| //      description: internal server error | //      description: internal server error | ||||||
| func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) { | func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "FollowRequestAuthorizePOSTHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { |  | ||||||
| 		l.Debugf("couldn't auth: %s", err) |  | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	originAccountID := c.Param(IDKey) | 	originAccountID := c.Param(IDKey) | ||||||
| 	if originAccountID == "" { | 	if originAccountID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"}) | 		err := errors.New("no account id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID) | 	relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debug(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -82,6 +82,34 @@ func (suite *AuthorizeTestSuite) TestAuthorize() { | ||||||
| 	suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b)) | 	suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (suite *AuthorizeTestSuite) TestAuthorizeNoFR() { | ||||||
|  | 	requestingAccount := suite.testAccounts["remote_account_2"] | ||||||
|  | 
 | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodPost, []byte{}, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", requestingAccount.ID), "") | ||||||
|  | 
 | ||||||
|  | 	ctx.Params = gin.Params{ | ||||||
|  | 		gin.Param{ | ||||||
|  | 			Key:   followrequest.IDKey, | ||||||
|  | 			Value: requestingAccount.ID, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.followRequestModule.FollowRequestAuthorizePOSTHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(http.StatusNotFound, recorder.Code) | ||||||
|  | 
 | ||||||
|  | 	result := recorder.Result() | ||||||
|  | 	defer result.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	// check the response | ||||||
|  | 	b, err := ioutil.ReadAll(result.Body) | ||||||
|  | 	assert.NoError(suite.T(), err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`{"error":"Not Found"}`, string(b)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestAuthorizeTestSuite(t *testing.T) { | func TestAuthorizeTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, &AuthorizeTestSuite{}) | 	suite.Run(t, &AuthorizeTestSuite{}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -21,10 +21,9 @@ package followrequest | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -71,34 +70,27 @@ import ( | ||||||
| //      description: bad request | //      description: bad request | ||||||
| //   '401': | //   '401': | ||||||
| //      description: unauthorized | //      description: unauthorized | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) FollowRequestGETHandler(c *gin.Context) { | func (m *Module) FollowRequestGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "FollowRequestGETHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { |  | ||||||
| 		l.Debugf("couldn't auth: %s", err) |  | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed) | 	accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package followrequest | package followrequest | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -59,43 +60,34 @@ import ( | ||||||
| //      description: bad request | //      description: bad request | ||||||
| //   '401': | //   '401': | ||||||
| //      description: unauthorized | //      description: unauthorized | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
| //   '500': | //   '500': | ||||||
| //      description: internal server error | //      description: internal server error | ||||||
| func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) { | func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "FollowRequestRejectPOSTHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { |  | ||||||
| 		l.Debugf("couldn't auth: %s", err) |  | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	originAccountID := c.Param(IDKey) | 	originAccountID := c.Param(IDKey) | ||||||
| 	if originAccountID == "" { | 	if originAccountID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"}) | 		err := errors.New("no account id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID) | 	relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debug(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,9 +3,9 @@ package instance | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  | @ -30,22 +30,19 @@ import ( | ||||||
| //     description: "Instance information." | //     description: "Instance information." | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/instance" | //       "$ref": "#/definitions/instance" | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
| //   '500': | //   '500': | ||||||
| //      description: internal error | //      description: internal error | ||||||
| func (m *Module) InstanceInformationGETHandler(c *gin.Context) { | func (m *Module) InstanceInformationGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "InstanceInformationGETHandler") |  | ||||||
| 
 |  | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	host := config.GetHost() | 	instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost()) | ||||||
| 
 | 	if errWithCode != nil { | ||||||
| 	instance, err := m.processor.InstanceGet(c.Request.Context(), host) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 	if err != nil { |  | ||||||
| 		l.Debugf("error getting instance from processor: %s", err) |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| package instance | package instance | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -82,52 +82,51 @@ import ( | ||||||
| //     description: "The newly updated instance." | //     description: "The newly updated instance." | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/instance" | //       "$ref": "#/definitions/instance" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) { | func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "InstanceUpdatePATCHHandler") |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// only admins can update instance settings |  | ||||||
| 	if !authed.User.Admin { | 	if !authed.User.Admin { | ||||||
| 		l.Debug("user is not an admin so cannot update instance settings") | 		err := errors.New("user is not an admin so cannot update instance settings") | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not an admin"}) | 		api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Debug("parsing request form") |  | ||||||
| 	form := &model.InstanceSettingsUpdateRequest{} | 	form := &model.InstanceSettingsUpdateRequest{} | ||||||
| 	if err := c.ShouldBind(&form); err != nil || form == nil { | 	if err := c.ShouldBind(&form); err != nil { | ||||||
| 		l.Debugf("could not parse form from request: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Debugf("parsed form: %+v", form) |  | ||||||
| 
 |  | ||||||
| 	// if everything on the form is nil, then nothing has been set and we shouldn't continue |  | ||||||
| 	if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && form.Terms == nil && form.Avatar == nil && form.Header == nil { | 	if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && form.Terms == nil && form.Avatar == nil && form.Header == nil { | ||||||
| 		l.Debugf("could not parse form from request") | 		err := errors.New("empty form submitted") | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"}) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form) | 	i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error with instance patch request: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/instance" | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/instance" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -125,6 +126,67 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() { | ||||||
| 	suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) | 	suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (suite *InstancePatchTestSuite) TestInstancePatch4() { | ||||||
|  | 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||||
|  | 		"", "", | ||||||
|  | 		map[string]string{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	bodyBytes := requestBody.Bytes() | ||||||
|  | 
 | ||||||
|  | 	// set up the request | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType()) | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.instanceModule.InstanceUpdatePATCHHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(http.StatusBadRequest, recorder.Code) | ||||||
|  | 
 | ||||||
|  | 	result := recorder.Result() | ||||||
|  | 	defer result.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	b, err := io.ReadAll(result.Body) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *InstancePatchTestSuite) TestInstancePatch5() { | ||||||
|  | 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||||
|  | 		"", "", | ||||||
|  | 		map[string]string{ | ||||||
|  | 			"short_description": "<p>This is some html, which is <em>allowed</em> in short descriptions.</p>", | ||||||
|  | 		}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	bodyBytes := requestBody.Bytes() | ||||||
|  | 
 | ||||||
|  | 	// set up the request | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType()) | ||||||
|  | 
 | ||||||
|  | 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | ||||||
|  | 	ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"])) | ||||||
|  | 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) | ||||||
|  | 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.instanceModule.InstanceUpdatePATCHHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(http.StatusForbidden, recorder.Code) | ||||||
|  | 
 | ||||||
|  | 	result := recorder.Result() | ||||||
|  | 	defer result.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	b, err := io.ReadAll(result.Body) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`{"error":"Forbidden: user is not an admin so cannot update instance settings"}`, string(b)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestInstancePatchTestSuite(t *testing.T) { | func TestInstancePatchTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, &InstancePatchTestSuite{}) | 	suite.Run(t, &InstancePatchTestSuite{}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,12 +5,19 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ListsGETHandler returns a list of lists created by/for the authed account | // ListsGETHandler returns a list of lists created by/for the authed account | ||||||
| func (m *Module) ListsGETHandler(c *gin.Context) { | func (m *Module) ListsGETHandler(c *gin.Context) { | ||||||
|  | 	if _, err := oauth.Authed(c, true, true, true, true); err != nil { | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,12 +23,11 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -80,46 +79,36 @@ import ( | ||||||
| //      description: bad request | //      description: bad request | ||||||
| //   '401': | //   '401': | ||||||
| //      description: unauthorized | //      description: unauthorized | ||||||
| //   '403': |  | ||||||
| //      description: forbidden |  | ||||||
| //   '422': | //   '422': | ||||||
| //      description: unprocessable | //      description: unprocessable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { | func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "statusCreatePOSTHandler") | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) // posting new media is serious business so we want *everything* |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// extract the media create form from the request context |  | ||||||
| 	l.Tracef("parsing request form: %s", c.Request.Form) |  | ||||||
| 	form := &model.AttachmentRequest{} | 	form := &model.AttachmentRequest{} | ||||||
| 	if err := c.ShouldBind(&form); err != nil { | 	if err := c.ShouldBind(&form); err != nil { | ||||||
| 		l.Debugf("error parsing form: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("could not parse form: %s", err)}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Give the fields on the request form a first pass to make sure the request is superficially valid. |  | ||||||
| 	l.Tracef("validating form %+v", form) |  | ||||||
| 	if err := validateCreateMedia(form); err != nil { | 	if err := validateCreateMedia(form); err != nil { | ||||||
| 		l.Debugf("error validating form: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Debug("calling processor media create func") | 	apiAttachment, errWithCode := m.processor.MediaCreate(c.Request.Context(), authed, form) | ||||||
| 	apiAttachment, err := m.processor.MediaCreate(c.Request.Context(), authed, form) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error creating attachment: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -143,6 +132,7 @@ func validateCreateMedia(form *model.AttachmentRequest) error { | ||||||
| 	if maxImageSize > maxSize { | 	if maxImageSize > maxSize { | ||||||
| 		maxSize = maxImageSize | 		maxSize = maxImageSize | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if form.File.Size > int64(maxSize) { | 	if form.File.Size > int64(maxSize) { | ||||||
| 		return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size) | 		return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -247,15 +247,14 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() { | ||||||
| 	suite.mediaModule.MediaCreatePOSTHandler(ctx) | 	suite.mediaModule.MediaCreatePOSTHandler(ctx) | ||||||
| 
 | 
 | ||||||
| 	// check response | 	// check response | ||||||
| 	suite.EqualValues(http.StatusUnprocessableEntity, recorder.Code) | 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | ||||||
| 
 | 
 | ||||||
| 	result := recorder.Result() | 	result := recorder.Result() | ||||||
| 	defer result.Body.Close() | 	defer result.Body.Close() | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	expectedErr := fmt.Sprintf(`{"error":"image description length must be between 0 and 500 characters (inclusive), but provided image description was %d chars"}`, len(description)) | 	suite.Equal(`{"error":"Bad Request: image description length must be between 0 and 500 characters (inclusive), but provided image description was 6667 chars"}`, string(b)) | ||||||
| 	suite.Equal(expectedErr, string(b)) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() { | func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() { | ||||||
|  |  | ||||||
|  | @ -19,12 +19,12 @@ | ||||||
| package media | package media | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -59,33 +59,34 @@ import ( | ||||||
| //      description: bad request | //      description: bad request | ||||||
| //   '401': | //   '401': | ||||||
| //      description: unauthorized | //      description: unauthorized | ||||||
| //   '403': | //   '404': | ||||||
| //      description: forbidden | //      description: not found | ||||||
| //   '422': | //   '406': | ||||||
| //      description: unprocessable | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) MediaGETHandler(c *gin.Context) { | func (m *Module) MediaGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "MediaGETHandler") |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	attachmentID := c.Param(IDKey) | 	attachmentID := c.Param(IDKey) | ||||||
| 	if attachmentID == "" { | 	if attachmentID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) | 		err := errors.New("no attachment id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID) | 	attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,12 +23,11 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -89,50 +88,45 @@ import ( | ||||||
| //      description: bad request | //      description: bad request | ||||||
| //   '401': | //   '401': | ||||||
| //      description: unauthorized | //      description: unauthorized | ||||||
| //   '403': | //   '404': | ||||||
| //      description: forbidden | //      description: not found | ||||||
| //   '422': | //   '406': | ||||||
| //      description: unprocessable | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) MediaPUTHandler(c *gin.Context) { | func (m *Module) MediaPUTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "MediaGETHandler") |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	attachmentID := c.Param(IDKey) | 	attachmentID := c.Param(IDKey) | ||||||
| 	if attachmentID == "" { | 	if attachmentID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) | 		err := errors.New("no attachment id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// extract the media update form from the request context | 	form := &model.AttachmentUpdateRequest{} | ||||||
| 	l.Tracef("parsing request form: %s", c.Request.Form) | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 	var form model.AttachmentUpdateRequest | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 	if err := c.ShouldBind(&form); err != nil { |  | ||||||
| 		l.Debugf("could not parse form from request: %s", err) |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Give the fields on the request form a first pass to make sure the request is superficially valid. | 	if err := validateUpdateMedia(form); err != nil { | ||||||
| 	l.Tracef("validating form %+v", form) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 	if err := validateUpdateMedia(&form); err != nil { |  | ||||||
| 		l.Debugf("error validating form: %s", err) |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, &form) | 	attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, form) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -232,7 +232,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() { | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	// reply should be an error message | 	// reply should be an error message | ||||||
| 	suite.Equal(`{"error":"image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b)) | 	suite.Equal(`{"error":"Bad Request: image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestMediaUpdateTestSuite(t *testing.T) { | func TestMediaUpdateTestSuite(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -19,34 +19,26 @@ | ||||||
| package notification | package notification | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters | // NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters | ||||||
| func (m *Module) NotificationsGETHandler(c *gin.Context) { | func (m *Module) NotificationsGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 		"func":        "NotificationsGETHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Errorf("error authing status faved by request: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -55,8 +47,8 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { | ||||||
| 	if limitString != "" { | 	if limitString != "" { | ||||||
| 		i, err := strconv.ParseInt(limitString, 10, 64) | 		i, err := strconv.ParseInt(limitString, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing limit string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", LimitKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		limit = int(i) | 		limit = int(i) | ||||||
|  | @ -74,12 +66,14 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { | ||||||
| 		sinceID = sinceIDString | 		sinceID = sinceIDString | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	notifs, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID) | 	resp, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing notifications get: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	c.JSON(http.StatusOK, notifs) | 	if resp.LinkHeader != "" { | ||||||
|  | 		c.Header("Link", resp.LinkHeader) | ||||||
|  | 	} | ||||||
|  | 	c.JSON(http.StatusOK, resp.Items) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,14 +19,15 @@ | ||||||
| package search | package search | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -52,50 +53,44 @@ import ( | ||||||
| //       type: array | //       type: array | ||||||
| //       items: | //       items: | ||||||
| //         "$ref": "#/definitions/searchResult" | //         "$ref": "#/definitions/searchResult" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) SearchGETHandler(c *gin.Context) { | func (m *Module) SearchGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 		"func":        "SearchGETHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Errorf("error authing search request: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	accountID := c.Query(AccountIDKey) |  | ||||||
| 	maxID := c.Query(MaxIDKey) |  | ||||||
| 	minID := c.Query(MinIDKey) |  | ||||||
| 	searchType := c.Query(TypeKey) |  | ||||||
| 
 |  | ||||||
| 	excludeUnreviewed := false | 	excludeUnreviewed := false | ||||||
| 	excludeUnreviewedString := c.Query(ExcludeUnreviewedKey) | 	excludeUnreviewedString := c.Query(ExcludeUnreviewedKey) | ||||||
| 	if excludeUnreviewedString != "" { | 	if excludeUnreviewedString != "" { | ||||||
| 		var err error | 		var err error | ||||||
| 		excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString) | 		excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", excludeUnreviewedString, err)}) | 			err := fmt.Errorf("error parsing %s: %s", ExcludeUnreviewedKey, err) | ||||||
|  | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	query := c.Query(QueryKey) | 	query := c.Query(QueryKey) | ||||||
| 	if query == "" { | 	if query == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter q was empty"}) | 		err := errors.New("query parameter q was empty") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -105,18 +100,19 @@ func (m *Module) SearchGETHandler(c *gin.Context) { | ||||||
| 		var err error | 		var err error | ||||||
| 		resolve, err = strconv.ParseBool(resolveString) | 		resolve, err = strconv.ParseBool(resolveString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", resolveString, err)}) | 			err := fmt.Errorf("error parsing %s: %s", ResolveKey, err) | ||||||
|  | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	limit := 20 | 	limit := 2 | ||||||
| 	limitString := c.Query(LimitKey) | 	limitString := c.Query(LimitKey) | ||||||
| 	if limitString != "" { | 	if limitString != "" { | ||||||
| 		i, err := strconv.ParseInt(limitString, 10, 64) | 		i, err := strconv.ParseInt(limitString, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing limit string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", LimitKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		limit = int(i) | 		limit = int(i) | ||||||
|  | @ -133,18 +129,12 @@ func (m *Module) SearchGETHandler(c *gin.Context) { | ||||||
| 	if offsetString != "" { | 	if offsetString != "" { | ||||||
| 		i, err := strconv.ParseInt(offsetString, 10, 64) | 		i, err := strconv.ParseInt(offsetString, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing offset string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", OffsetKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse offset query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		offset = int(i) | 		offset = int(i) | ||||||
| 	} | 	} | ||||||
| 	if limit > 40 { |  | ||||||
| 		limit = 40 |  | ||||||
| 	} |  | ||||||
| 	if limit < 1 { |  | ||||||
| 		limit = 1 |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	following := false | 	following := false | ||||||
| 	followingString := c.Query(FollowingKey) | 	followingString := c.Query(FollowingKey) | ||||||
|  | @ -152,16 +142,17 @@ func (m *Module) SearchGETHandler(c *gin.Context) { | ||||||
| 		var err error | 		var err error | ||||||
| 		following, err = strconv.ParseBool(followingString) | 		following, err = strconv.ParseBool(followingString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", followingString, err)}) | 			err := fmt.Errorf("error parsing %s: %s", FollowingKey, err) | ||||||
|  | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	searchQuery := &model.SearchQuery{ | 	searchQuery := &model.SearchQuery{ | ||||||
| 		AccountID:         accountID, | 		AccountID:         c.Query(AccountIDKey), | ||||||
| 		MaxID:             maxID, | 		MaxID:             c.Query(MaxIDKey), | ||||||
| 		MinID:             minID, | 		MinID:             c.Query(MinIDKey), | ||||||
| 		Type:              searchType, | 		Type:              c.Query(TypeKey), | ||||||
| 		ExcludeUnreviewed: excludeUnreviewed, | 		ExcludeUnreviewed: excludeUnreviewed, | ||||||
| 		Query:             query, | 		Query:             query, | ||||||
| 		Resolve:           resolve, | 		Resolve:           resolve, | ||||||
|  | @ -172,8 +163,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) { | ||||||
| 
 | 
 | ||||||
| 	results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery) | 	results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error searching: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package status | package status | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -66,37 +67,32 @@ import ( | ||||||
| //      description: forbidden | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) StatusBoostPOSTHandler(c *gin.Context) { | func (m *Module) StatusBoostPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "StatusBoostPOSTHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debug("not authed so can't boost status") | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetStatusID := c.Param(IDKey) | 	targetStatusID := c.Param(IDKey) | ||||||
| 	if targetStatusID == "" { | 	if targetStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | 		err := errors.New("no status id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID) | 	apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing status boost: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -134,13 +134,13 @@ func (suite *StatusBoostTestSuite) TestPostUnboostable() { | ||||||
| 	suite.statusModule.StatusBoostPOSTHandler(ctx) | 	suite.statusModule.StatusBoostPOSTHandler(ctx) | ||||||
| 
 | 
 | ||||||
| 	// check response | 	// check response | ||||||
| 	suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses | 	suite.Equal(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses | ||||||
| 
 | 
 | ||||||
| 	result := recorder.Result() | 	result := recorder.Result() | ||||||
| 	defer result.Body.Close() | 	defer result.Body.Close() | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	assert.NoError(suite.T(), err) | ||||||
| 	assert.Equal(suite.T(), `{"error":"forbidden"}`, string(b)) | 	assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // try to boost a status that's not visible to the user | // try to boost a status that's not visible to the user | ||||||
|  | @ -177,13 +177,7 @@ func (suite *StatusBoostTestSuite) TestPostNotVisible() { | ||||||
| 	suite.statusModule.StatusBoostPOSTHandler(ctx) | 	suite.statusModule.StatusBoostPOSTHandler(ctx) | ||||||
| 
 | 
 | ||||||
| 	// check response | 	// check response | ||||||
| 	suite.EqualValues(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible | 	suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible | ||||||
| 
 |  | ||||||
| 	result := recorder.Result() |  | ||||||
| 	defer result.Body.Close() |  | ||||||
| 	b, err := ioutil.ReadAll(result.Body) |  | ||||||
| 	assert.NoError(suite.T(), err) |  | ||||||
| 	assert.Equal(suite.T(), `{"error":"404 not found"}`, string(b)) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestStatusBoostTestSuite(t *testing.T) { | func TestStatusBoostTestSuite(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -84,10 +85,9 @@ func (m *Module) StatusBoostedByGETHandler(c *gin.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiAccounts, err := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID) | 	apiAccounts, errWithCode := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing status boosted by request: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package status | package status | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -65,37 +66,32 @@ import ( | ||||||
| //      description: forbidden | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) StatusContextGETHandler(c *gin.Context) { | func (m *Module) StatusContextGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "StatusContextGETHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Errorf("error authing status context request: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetStatusID := c.Param(IDKey) | 	targetStatusID := c.Param(IDKey) | ||||||
| 	if targetStatusID == "" { | 	if targetStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | 		err := errors.New("no status id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID) | 	statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error getting status context: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,12 +23,11 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/validate" | 	"github.com/superseriousbusiness/gotosocial/internal/validate" | ||||||
| ) | ) | ||||||
|  | @ -61,58 +60,44 @@ import ( | ||||||
| //     description: "The newly created status." | //     description: "The newly created status." | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/status" | //       "$ref": "#/definitions/status" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
| //   '500': | //   '500': | ||||||
| //      description: internal error | //      description: internal server error | ||||||
| func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { | func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "statusCreatePOSTHandler") |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// First check this user/account is permitted to post new statuses. |  | ||||||
| 	// There's no point continuing otherwise. |  | ||||||
| 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { |  | ||||||
| 		l.Debugf("couldn't auth: %s", err) |  | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// extract the status create form from the request context |  | ||||||
| 	l.Debugf("parsing request form: %s", c.Request.Form) |  | ||||||
| 	form := &model.AdvancedStatusCreateForm{} | 	form := &model.AdvancedStatusCreateForm{} | ||||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		l.Debugf("could not parse form from request: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Debugf("handling status request form: %+v", form) |  | ||||||
| 
 | 
 | ||||||
| 	// Give the fields on the request form a first pass to make sure the request is superficially valid. |  | ||||||
| 	l.Tracef("validating form %+v", form) |  | ||||||
| 	if err := validateCreateStatus(form); err != nil { | 	if err := validateCreateStatus(form); err != nil { | ||||||
| 		l.Debugf("error validating form: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiStatus, err := m.processor.StatusCreate(c.Request.Context(), authed, form) | 	apiStatus, errWithCode := m.processor.StatusCreate(c.Request.Context(), authed, form) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing status create: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -120,7 +105,6 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func validateCreateStatus(form *model.AdvancedStatusCreateForm) error { | func validateCreateStatus(form *model.AdvancedStatusCreateForm) error { | ||||||
| 	// validate that, structurally, we have a valid status/post |  | ||||||
| 	if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { | 	if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { | ||||||
| 		return errors.New("no status, media, or poll provided") | 		return errors.New("no status, media, or poll provided") | ||||||
| 	} | 	} | ||||||
|  | @ -135,19 +119,16 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error { | ||||||
| 	maxPollChars := config.GetStatusesPollOptionMaxChars() | 	maxPollChars := config.GetStatusesPollOptionMaxChars() | ||||||
| 	maxCwChars := config.GetStatusesCWMaxChars() | 	maxCwChars := config.GetStatusesCWMaxChars() | ||||||
| 
 | 
 | ||||||
| 	// validate status |  | ||||||
| 	if form.Status != "" { | 	if form.Status != "" { | ||||||
| 		if len(form.Status) > maxChars { | 		if len(form.Status) > maxChars { | ||||||
| 			return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), maxChars) | 			return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), maxChars) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// validate media attachments |  | ||||||
| 	if len(form.MediaIDs) > maxMediaFiles { | 	if len(form.MediaIDs) > maxMediaFiles { | ||||||
| 		return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles) | 		return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// validate poll |  | ||||||
| 	if form.Poll != nil { | 	if form.Poll != nil { | ||||||
| 		if form.Poll.Options == nil { | 		if form.Poll.Options == nil { | ||||||
| 			return errors.New("poll with no options") | 			return errors.New("poll with no options") | ||||||
|  | @ -162,14 +143,12 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// validate spoiler text/cw |  | ||||||
| 	if form.SpoilerText != "" { | 	if form.SpoilerText != "" { | ||||||
| 		if len(form.SpoilerText) > maxCwChars { | 		if len(form.SpoilerText) > maxCwChars { | ||||||
| 			return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), maxCwChars) | 			return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), maxCwChars) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// validate post language |  | ||||||
| 	if form.Language != "" { | 	if form.Language != "" { | ||||||
| 		if err := validate.Language(form.Language); err != nil { | 		if err := validate.Language(form.Language); err != nil { | ||||||
| 			return err | 			return err | ||||||
|  |  | ||||||
|  | @ -256,7 +256,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() { | ||||||
| 	defer result.Body.Close() | 	defer result.Body.Close() | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 	suite.Equal(`{"error":"bad request"}`, string(b)) | 	suite.Equal(`{"error":"Bad Request: status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Post a reply to the status of a local user that allows replies. | // Post a reply to the status of a local user that allows replies. | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package status | package status | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -65,43 +66,32 @@ import ( | ||||||
| //      description: forbidden | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) StatusDELETEHandler(c *gin.Context) { | func (m *Module) StatusDELETEHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "StatusDELETEHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debug("not authed so can't delete status") | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetStatusID := c.Param(IDKey) | 	targetStatusID := c.Param(IDKey) | ||||||
| 	if targetStatusID == "" { | 	if targetStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | 		err := errors.New("no status id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiStatus, err := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID) | 	apiStatus, errWithCode := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing status delete: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// the status was already gone/never existed |  | ||||||
| 	if apiStatus == nil { |  | ||||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package status | package status | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -62,37 +63,32 @@ import ( | ||||||
| //      description: forbidden | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) StatusFavePOSTHandler(c *gin.Context) { | func (m *Module) StatusFavePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "StatusFavePOSTHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debug("not authed so can't fave status") | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetStatusID := c.Param(IDKey) | 	targetStatusID := c.Param(IDKey) | ||||||
| 	if targetStatusID == "" { | 	if targetStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | 		err := errors.New("no status id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiStatus, err := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID) | 	apiStatus, errWithCode := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing status fave: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -118,13 +118,13 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() { | ||||||
| 	suite.statusModule.StatusFavePOSTHandler(ctx) | 	suite.statusModule.StatusFavePOSTHandler(ctx) | ||||||
| 
 | 
 | ||||||
| 	// check response | 	// check response | ||||||
| 	suite.EqualValues(http.StatusBadRequest, recorder.Code) | 	suite.EqualValues(http.StatusForbidden, recorder.Code) | ||||||
| 
 | 
 | ||||||
| 	result := recorder.Result() | 	result := recorder.Result() | ||||||
| 	defer result.Body.Close() | 	defer result.Body.Close() | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	assert.NoError(suite.T(), err) | ||||||
| 	assert.Equal(suite.T(), `{"error":"bad request"}`, string(b)) | 	assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestStatusFaveTestSuite(t *testing.T) { | func TestStatusFaveTestSuite(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package status | package status | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -63,37 +64,32 @@ import ( | ||||||
| //      description: forbidden | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) StatusFavedByGETHandler(c *gin.Context) { | func (m *Module) StatusFavedByGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 		"func":        "statusGETHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Errorf("error authing status faved by request: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetStatusID := c.Param(IDKey) | 	targetStatusID := c.Param(IDKey) | ||||||
| 	if targetStatusID == "" { | 	if targetStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | 		err := errors.New("no status id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiAccounts, err := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID) | 	apiAccounts, errWithCode := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing status faved by request: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package status | package status | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -54,45 +55,40 @@ import ( | ||||||
| //     description: "The requested created status." | //     description: "The requested created status." | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/status" | //       "$ref": "#/definitions/status" | ||||||
| //   '401': |  | ||||||
| //      description: unauthorized |  | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
| //   '500': | //   '500': | ||||||
| //      description: internal error | //      description: internal server error | ||||||
| func (m *Module) StatusGETHandler(c *gin.Context) { | func (m *Module) StatusGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 		"func":        "statusGETHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, false, false, false, false) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Errorf("error authing status faved by request: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetStatusID := c.Param(IDKey) | 	targetStatusID := c.Param(IDKey) | ||||||
| 	if targetStatusID == "" { | 	if targetStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | 		err := errors.New("no status id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiStatus, err := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID) | 	apiStatus, errWithCode := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing status get: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package status | package status | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -63,37 +64,32 @@ import ( | ||||||
| //      description: forbidden | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) { | func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "StatusUnboostPOSTHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debug("not authed so can't unboost status") | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetStatusID := c.Param(IDKey) | 	targetStatusID := c.Param(IDKey) | ||||||
| 	if targetStatusID == "" { | 	if targetStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | 		err := errors.New("no status id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID) | 	apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing status unboost: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,11 +19,12 @@ | ||||||
| package status | package status | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -62,37 +63,32 @@ import ( | ||||||
| //      description: forbidden | //      description: forbidden | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
|  | //   '406': | ||||||
|  | //      description: not acceptable | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
| func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) { | func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "StatusUnfavePOSTHandler", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 	l.Debugf("entering function") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debug("not authed so can't unfave status") | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	targetStatusID := c.Param(IDKey) | 	targetStatusID := c.Param(IDKey) | ||||||
| 	if targetStatusID == "" { | 	if targetStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) | 		err := errors.New("no status id specified") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiStatus, err := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID) | 	apiStatus, errWithCode := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error processing status unfave: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,14 +2,24 @@ package streaming | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
|  | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/gorilla/websocket" | 	"github.com/gorilla/websocket" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var wsUpgrader = websocket.Upgrader{ | ||||||
|  | 	ReadBufferSize:  1024, | ||||||
|  | 	WriteBufferSize: 1024, | ||||||
|  | 	// we expect cors requests (via eg., pinafore.social) so be lenient | ||||||
|  | 	CheckOrigin: func(r *http.Request) bool { return true }, | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // StreamGETHandler swagger:operation GET /api/v1/streaming streamGet | // StreamGETHandler swagger:operation GET /api/v1/streaming streamGet | ||||||
| // | // | ||||||
| // Initiate a websocket connection for live streaming of statuses and notifications. | // Initiate a websocket connection for live streaming of statuses and notifications. | ||||||
|  | @ -108,79 +118,78 @@ import ( | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
| func (m *Module) StreamGETHandler(c *gin.Context) { | func (m *Module) StreamGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "StreamGETHandler") |  | ||||||
| 
 |  | ||||||
| 	streamType := c.Query(StreamQueryKey) | 	streamType := c.Query(StreamQueryKey) | ||||||
| 	if streamType == "" { | 	if streamType == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("no stream type provided under query key %s", StreamQueryKey)}) | 		err := fmt.Errorf("no stream type provided under query key %s", StreamQueryKey) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	accessToken := c.Query(AccessTokenQueryKey) | 	accessToken := c.Query(AccessTokenQueryKey) | ||||||
| 	if accessToken == "" { | 	if accessToken == "" { | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("no access token provided under query key %s", AccessTokenQueryKey)}) | 		err := fmt.Errorf("no access token provided under query key %s", AccessTokenQueryKey) | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// make sure a valid token has been provided and obtain the associated account | 	account, errWithCode := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken) | ||||||
| 	account, err := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken) |  | ||||||
| 	if err != nil { |  | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "could not authorize with given token"}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// prepare to upgrade the connection to a websocket connection |  | ||||||
| 	upgrader := websocket.Upgrader{ |  | ||||||
| 		ReadBufferSize:  1024, |  | ||||||
| 		WriteBufferSize: 1024, |  | ||||||
| 		CheckOrigin: func(r *http.Request) bool { |  | ||||||
| 			// we fully expect cors requests (via something like pinafore.social) so we should be lenient here |  | ||||||
| 			return true |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// do the actual upgrade here |  | ||||||
| 	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) |  | ||||||
| 	if err != nil { |  | ||||||
| 		l.Infof("error upgrading websocket connection: %s", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	defer conn.Close() // whatever happens, when we leave this function we want to close the websocket connection |  | ||||||
| 
 |  | ||||||
| 	// inform the processor that we have a new connection and want a s for it |  | ||||||
| 	s, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType) |  | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		c.JSON(errWithCode.Code(), errWithCode.Safe()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	defer close(s.Hangup) // closing stream.Hangup indicates that we've finished with the connection (the client has gone), so we want to do this on exiting this handler |  | ||||||
| 
 | 
 | ||||||
| 	// spawn a new ticker for pinging the connection periodically | 	stream, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType) | ||||||
| 	t := time.NewTicker(30 * time.Second) | 	if errWithCode != nil { | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// we want to stay in the sendloop as long as possible while the client is connected -- the only thing that should break the loop is if the client leaves or something else goes wrong | 	l := logrus.WithFields(logrus.Fields{ | ||||||
| sendLoop: | 		"account":    account.Username, | ||||||
|  | 		"path":       BasePath, | ||||||
|  | 		"streamID":   stream.ID, | ||||||
|  | 		"streamType": streamType, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	wsConn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// If the upgrade fails, then Upgrade replies to the client with an HTTP error response. | ||||||
|  | 		// Because websocket issues are a pretty common source of headaches, we should also log | ||||||
|  | 		// this at Error to make this plenty visible and help admins out a bit. | ||||||
|  | 		l.Errorf("error upgrading websocket connection: %s", err) | ||||||
|  | 		close(stream.Hangup) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	defer func() { | ||||||
|  | 		// cleanup | ||||||
|  | 		wsConn.Close() | ||||||
|  | 		close(stream.Hangup) | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	streamTicker := time.NewTicker(30 * time.Second) | ||||||
|  | 
 | ||||||
|  | 	// We want to stay in the loop as long as possible while the client is connected. | ||||||
|  | 	// The only thing that should break the loop is if the client leaves or the connection becomes unhealthy. | ||||||
|  | 	// | ||||||
|  | 	// If the loop does break, we expect the client to reattempt connection, so it's cheap to leave + try again | ||||||
|  | wsLoop: | ||||||
| 	for { | 	for { | ||||||
| 		select { | 		select { | ||||||
| 		case m := <-s.Messages: | 		case m := <-stream.Messages: | ||||||
| 			// we've got a streaming message!! |  | ||||||
| 			l.Trace("received message from stream") | 			l.Trace("received message from stream") | ||||||
| 			if err := conn.WriteJSON(m); err != nil { | 			if err := wsConn.WriteJSON(m); err != nil { | ||||||
| 				l.Debugf("error writing json to websocket connection: %s", err) | 				l.Debugf("error writing json to websocket connection; breaking off: %s", err) | ||||||
| 				// if something is wrong we want to bail and drop the connection -- the client will create a new one | 				break wsLoop | ||||||
| 				break sendLoop |  | ||||||
| 			} | 			} | ||||||
| 			l.Trace("wrote message into websocket connection") | 			l.Trace("wrote message into websocket connection") | ||||||
| 		case <-t.C: | 		case <-streamTicker.C: | ||||||
| 			l.Trace("received TICK from ticker") | 			l.Trace("received TICK from ticker") | ||||||
| 			if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil { | 			if err := wsConn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil { | ||||||
| 				l.Debugf("error writing ping to websocket connection: %s", err) | 				l.Debugf("error writing ping to websocket connection; breaking off: %s", err) | ||||||
| 				// if something is wrong we want to bail and drop the connection -- the client will create a new one | 				break wsLoop | ||||||
| 				break sendLoop |  | ||||||
| 			} | 			} | ||||||
| 			l.Trace("wrote ping message into websocket connection") | 			l.Trace("wrote ping message into websocket connection") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	l.Trace("leaving StreamGETHandler") |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,13 +19,13 @@ | ||||||
| package timeline | package timeline | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -105,17 +105,14 @@ import ( | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
| func (m *Module) HomeTimelineGETHandler(c *gin.Context) { | func (m *Module) HomeTimelineGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "HomeTimelineGETHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error authing: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -142,8 +139,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { | ||||||
| 	if limitString != "" { | 	if limitString != "" { | ||||||
| 		i, err := strconv.ParseInt(limitString, 10, 64) | 		i, err := strconv.ParseInt(limitString, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing limit string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", LimitKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		limit = int(i) | 		limit = int(i) | ||||||
|  | @ -154,8 +151,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { | ||||||
| 	if localString != "" { | 	if localString != "" { | ||||||
| 		i, err := strconv.ParseBool(localString) | 		i, err := strconv.ParseBool(localString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing local string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", LocalKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		local = i | 		local = i | ||||||
|  | @ -163,13 +160,12 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { | ||||||
| 
 | 
 | ||||||
| 	resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local) | 	resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error from processor HomeTimelineGet: %s", errWithCode) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if resp.LinkHeader != "" { | 	if resp.LinkHeader != "" { | ||||||
| 		c.Header("Link", resp.LinkHeader) | 		c.Header("Link", resp.LinkHeader) | ||||||
| 	} | 	} | ||||||
| 	c.JSON(http.StatusOK, resp.Statuses) | 	c.JSON(http.StatusOK, resp.Items) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,13 +19,13 @@ | ||||||
| package timeline | package timeline | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 
 |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -105,17 +105,14 @@ import ( | ||||||
| //   '400': | //   '400': | ||||||
| //      description: bad request | //      description: bad request | ||||||
| func (m *Module) PublicTimelineGETHandler(c *gin.Context) { | func (m *Module) PublicTimelineGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "PublicTimelineGETHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error authing: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -142,8 +139,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { | ||||||
| 	if limitString != "" { | 	if limitString != "" { | ||||||
| 		i, err := strconv.ParseInt(limitString, 10, 64) | 		i, err := strconv.ParseInt(limitString, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing limit string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", LimitKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		limit = int(i) | 		limit = int(i) | ||||||
|  | @ -154,8 +151,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { | ||||||
| 	if localString != "" { | 	if localString != "" { | ||||||
| 		i, err := strconv.ParseBool(localString) | 		i, err := strconv.ParseBool(localString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing local string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", LocalKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		local = i | 		local = i | ||||||
|  | @ -163,13 +160,12 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { | ||||||
| 
 | 
 | ||||||
| 	resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local) | 	resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error from processor PublicTimelineGet: %s", errWithCode) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if resp.LinkHeader != "" { | 	if resp.LinkHeader != "" { | ||||||
| 		c.Header("Link", resp.LinkHeader) | 		c.Header("Link", resp.LinkHeader) | ||||||
| 	} | 	} | ||||||
| 	c.JSON(http.StatusOK, resp.Statuses) | 	c.JSON(http.StatusOK, resp.Items) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,12 +19,13 @@ | ||||||
| package user | package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -54,48 +55,48 @@ import ( | ||||||
| // responses: | // responses: | ||||||
| //   '200': | //   '200': | ||||||
| //     description: Change successful | //     description: Change successful | ||||||
|  | //   '400': | ||||||
|  | //      description: bad request | ||||||
| //   '401': | //   '401': | ||||||
| //      description: unauthorized | //      description: unauthorized | ||||||
| //   '403': | //   '403': | ||||||
| //      description: forbidden | //      description: forbidden | ||||||
| //   '400': | //   '406': | ||||||
| //      description: bad request | //      description: not acceptable | ||||||
| //   '500': | //   '500': | ||||||
| //      description: "internal error" | //      description: internal error | ||||||
| func (m *Module) PasswordChangePOSTHandler(c *gin.Context) { | func (m *Module) PasswordChangePOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "PasswordChangePOSTHandler") |  | ||||||
| 
 |  | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error authing: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// First check this user/account is active. |  | ||||||
| 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { |  | ||||||
| 		l.Debugf("couldn't auth: %s", err) |  | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	form := &model.PasswordChangeRequest{} | 	form := &model.PasswordChangeRequest{} | ||||||
| 	if err := c.ShouldBind(form); err != nil || form == nil || form.NewPassword == "" || form.OldPassword == "" { | 	if err := c.ShouldBind(form); err != nil { | ||||||
| 		if err != nil { | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			l.Debugf("could not parse form from request: %s", err) | 		return | ||||||
| 		} | 	} | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) | 
 | ||||||
|  | 	if form.OldPassword == "" { | ||||||
|  | 		err := errors.New("password change request missing field old_password") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if form.NewPassword == "" { | ||||||
|  | 		err := errors.New("password change request missing field new_password") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil { | 	if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil { | ||||||
| 		l.Debugf("error changing user password: %s", errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -49,7 +49,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() { | ||||||
| 	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) | 	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) | ||||||
| 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | ||||||
| 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | ||||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) | 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil) | ||||||
| 	ctx.Request.Header.Set("accept", "application/json") | 	ctx.Request.Header.Set("accept", "application/json") | ||||||
| 	ctx.Request.Form = url.Values{ | 	ctx.Request.Form = url.Values{ | ||||||
| 		"old_password": {"password"}, | 		"old_password": {"password"}, | ||||||
|  | @ -83,7 +83,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() { | ||||||
| 	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) | 	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) | ||||||
| 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | ||||||
| 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | ||||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) | 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil) | ||||||
| 	ctx.Request.Header.Set("accept", "application/json") | 	ctx.Request.Header.Set("accept", "application/json") | ||||||
| 	ctx.Request.Form = url.Values{ | 	ctx.Request.Form = url.Values{ | ||||||
| 		"new_password": {"peepeepoopoopassword"}, | 		"new_password": {"peepeepoopoopassword"}, | ||||||
|  | @ -97,7 +97,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() { | ||||||
| 	defer result.Body.Close() | 	defer result.Body.Close() | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 	suite.Equal(`{"error":"missing one or more required form values"}`, string(b)) | 	suite.Equal(`{"error":"Bad Request: password change request missing field old_password"}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() { | func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() { | ||||||
|  | @ -110,7 +110,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() { | ||||||
| 	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) | 	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) | ||||||
| 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | ||||||
| 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | ||||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) | 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil) | ||||||
| 	ctx.Request.Header.Set("accept", "application/json") | 	ctx.Request.Header.Set("accept", "application/json") | ||||||
| 	ctx.Request.Form = url.Values{ | 	ctx.Request.Form = url.Values{ | ||||||
| 		"old_password": {"notright"}, | 		"old_password": {"notright"}, | ||||||
|  | @ -125,7 +125,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() { | ||||||
| 	defer result.Body.Close() | 	defer result.Body.Close() | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 	suite.Equal(`{"error":"bad request: old password did not match"}`, string(b)) | 	suite.Equal(`{"error":"Bad Request: old password did not match"}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() { | func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() { | ||||||
|  | @ -138,7 +138,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() { | ||||||
| 	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) | 	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) | ||||||
| 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | ||||||
| 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | ||||||
| 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) | 	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil) | ||||||
| 	ctx.Request.Header.Set("accept", "application/json") | 	ctx.Request.Header.Set("accept", "application/json") | ||||||
| 	ctx.Request.Form = url.Values{ | 	ctx.Request.Form = url.Values{ | ||||||
| 		"old_password": {"password"}, | 		"old_password": {"password"}, | ||||||
|  | @ -153,7 +153,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() { | ||||||
| 	defer result.Body.Close() | 	defer result.Body.Close() | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 	suite.Equal(`{"error":"bad request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b)) | 	suite.Equal(`{"error":"Bad Request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestPasswordChangeTestSuite(t *testing.T) { | func TestPasswordChangeTestSuite(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -56,8 +56,8 @@ type UserStandardTestSuite struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *UserStandardTestSuite) SetupTest() { | func (suite *UserStandardTestSuite) SetupTest() { | ||||||
| 	testrig.InitTestLog() |  | ||||||
| 	testrig.InitTestConfig() | 	testrig.InitTestConfig() | ||||||
|  | 	testrig.InitTestLog() | ||||||
| 	fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) | 	fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) | ||||||
| 	clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) | 	clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) | ||||||
| 	suite.testTokens = testrig.NewTestTokens() | 	suite.testTokens = testrig.NewTestTokens() | ||||||
|  |  | ||||||
							
								
								
									
										127
									
								
								internal/api/errorhandling.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								internal/api/errorhandling.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,127 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    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 api | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // TODO: add more templated html pages here for different error types | ||||||
|  | 
 | ||||||
|  | // NotFoundHandler serves a 404 html page through the provided gin context, | ||||||
|  | // if accept is 'text/html', or just returns a json error if 'accept' is empty | ||||||
|  | // or application/json. | ||||||
|  | // | ||||||
|  | // When serving html, NotFoundHandler calls the provided InstanceGet function | ||||||
|  | // to fetch the apimodel representation of the instance, for serving in the | ||||||
|  | // 404 header and footer. | ||||||
|  | // | ||||||
|  | // If an error is returned by InstanceGet, the function will panic. | ||||||
|  | func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string) { | ||||||
|  | 	switch accept { | ||||||
|  | 	case string(TextHTML): | ||||||
|  | 		host := config.GetHost() | ||||||
|  | 		instance, err := instanceGet(c.Request.Context(), host) | ||||||
|  | 		if err != nil { | ||||||
|  | 			panic(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		c.HTML(http.StatusNotFound, "404.tmpl", gin.H{ | ||||||
|  | 			"instance": instance, | ||||||
|  | 		}) | ||||||
|  | 	default: | ||||||
|  | 		c.JSON(http.StatusNotFound, gin.H{"error": http.StatusText(http.StatusNotFound)}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // genericErrorHandler is a more general version of the NotFoundHandler, which can | ||||||
|  | // be used for serving either generic error pages with some rendered help text, | ||||||
|  | // or just some error json if the caller prefers (or has no preference). | ||||||
|  | func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string, errWithCode gtserror.WithCode) { | ||||||
|  | 	switch accept { | ||||||
|  | 	case string(TextHTML): | ||||||
|  | 		host := config.GetHost() | ||||||
|  | 		instance, err := instanceGet(c.Request.Context(), host) | ||||||
|  | 		if err != nil { | ||||||
|  | 			panic(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		c.HTML(errWithCode.Code(), "error.tmpl", gin.H{ | ||||||
|  | 			"instance": instance, | ||||||
|  | 			"code":     errWithCode.Code(), | ||||||
|  | 			"error":    errWithCode.Safe(), | ||||||
|  | 		}) | ||||||
|  | 	default: | ||||||
|  | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ErrorHandler takes the provided gin context and errWithCode and tries to serve | ||||||
|  | // a helpful error to the caller. It will do content negotiation to figure out if | ||||||
|  | // the caller prefers to see an html page with the error rendered there. If not, or | ||||||
|  | // if something goes wrong during the function, it will recover and just try to serve | ||||||
|  | // an appropriate application/json content-type error. | ||||||
|  | func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode)) { | ||||||
|  | 	path := c.Request.URL.Path | ||||||
|  | 	if raw := c.Request.URL.RawQuery; raw != "" { | ||||||
|  | 		path = path + "?" + raw | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	l := logrus.WithFields(logrus.Fields{ | ||||||
|  | 		"path":  path, | ||||||
|  | 		"error": errWithCode.Error(), | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	statusCode := errWithCode.Code() | ||||||
|  | 
 | ||||||
|  | 	if statusCode == http.StatusInternalServerError { | ||||||
|  | 		l.Error("Internal Server Error") | ||||||
|  | 	} else { | ||||||
|  | 		l.Debug("handling error") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// if we panic for any reason during error handling, | ||||||
|  | 	// we should still try to return a basic code | ||||||
|  | 	defer func() { | ||||||
|  | 		if p := recover(); p != nil { | ||||||
|  | 			l.Warnf("recovered from panic: %s", p) | ||||||
|  | 			c.JSON(statusCode, gin.H{"error": errWithCode.Safe()}) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	// discover if we're allowed to serve a nice html error page, | ||||||
|  | 	// or if we should just use a json. Normally we would want to | ||||||
|  | 	// check for a returned error, but if an error occurs here we | ||||||
|  | 	// can just fall back to default behavior (serve json error). | ||||||
|  | 	accept, _ := NegotiateAccept(c, HTMLOrJSONAcceptHeaders...) | ||||||
|  | 
 | ||||||
|  | 	if statusCode == http.StatusNotFound { | ||||||
|  | 		// use our special not found handler with useful status text | ||||||
|  | 		NotFoundHandler(c, instanceGet, accept) | ||||||
|  | 	} else { | ||||||
|  | 		genericErrorHandler(c, instanceGet, accept, errWithCode) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -16,4 +16,19 @@ | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| package gtserror | package api | ||||||
|  | 
 | ||||||
|  | // MIME represents a mime-type. | ||||||
|  | type MIME string | ||||||
|  | 
 | ||||||
|  | // MIME type | ||||||
|  | const ( | ||||||
|  | 	AppJSON           MIME = `application/json` | ||||||
|  | 	AppXML            MIME = `application/xml` | ||||||
|  | 	AppActivityJSON   MIME = `application/activity+json` | ||||||
|  | 	AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` | ||||||
|  | 	AppForm           MIME = `application/x-www-form-urlencoded` | ||||||
|  | 	MultipartForm     MIME = `multipart/form-data` | ||||||
|  | 	TextXML           MIME = `text/xml` | ||||||
|  | 	TextHTML          MIME = `text/html` | ||||||
|  | ) | ||||||
|  | @ -43,3 +43,24 @@ type Notification struct { | ||||||
| 	// Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls. | 	// Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls. | ||||||
| 	Status *Status `json:"status,omitempty"` | 	Status *Status `json:"status,omitempty"` | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 	The below functions are added onto the apimodel notification so that it satisfies | ||||||
|  | 	the Timelineable interface in internal/timeline. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | func (n *Notification) GetID() string { | ||||||
|  | 	return n.ID | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *Notification) GetAccountID() string { | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *Notification) GetBoostOfID() string { | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (n *Notification) GetBoostOfAccountID() string { | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -18,9 +18,11 @@ | ||||||
| 
 | 
 | ||||||
| package model | package model | ||||||
| 
 | 
 | ||||||
| // StatusTimelineResponse wraps a slice of statuses, ready to be serialized, along with the Link | import "github.com/superseriousbusiness/gotosocial/internal/timeline" | ||||||
|  | 
 | ||||||
|  | // TimelineResponse wraps a slice of timelineables, ready to be serialized, along with the Link | ||||||
| // header for the previous and next queries, to be returned to the client. | // header for the previous and next queries, to be returned to the client. | ||||||
| type StatusTimelineResponse struct { | type TimelineResponse struct { | ||||||
| 	Statuses   []*Status | 	Items      []timeline.Timelineable | ||||||
| 	LinkHeader string | 	LinkHeader string | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -25,33 +25,40 @@ import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Offer represents an offered mime-type. |  | ||||||
| type Offer string |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	AppJSON           Offer = `application/json`                                                     // AppJSON is the mime type for 'application/json'. |  | ||||||
| 	AppActivityJSON   Offer = `application/activity+json`                                            // AppActivityJSON is the mime type for 'application/activity+json'. |  | ||||||
| 	AppActivityLDJSON Offer = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` // AppActivityLDJSON is the mime type for 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' |  | ||||||
| 	TextHTML          Offer = `text/html`                                                            // TextHTML is the mime type for 'text/html'. |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // ActivityPubAcceptHeaders represents the Accept headers mentioned here: | // ActivityPubAcceptHeaders represents the Accept headers mentioned here: | ||||||
| // https://www.w3.org/TR/activitypub/#retrieving-objects | // | ||||||
| var ActivityPubAcceptHeaders = []Offer{ | var ActivityPubAcceptHeaders = []MIME{ | ||||||
| 	AppActivityJSON, | 	AppActivityJSON, | ||||||
| 	AppActivityLDJSON, | 	AppActivityLDJSON, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // JSONAcceptHeaders is a slice of offers that just contains application/json types. | // JSONAcceptHeaders is a slice of offers that just contains application/json types. | ||||||
| var JSONAcceptHeaders = []Offer{ | var JSONAcceptHeaders = []MIME{ | ||||||
|  | 	AppJSON, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HTMLOrJSONAcceptHeaders is a slice of offers that prefers TextHTML and will | ||||||
|  | // fall back to JSON if necessary. This is useful for error handling, since it can | ||||||
|  | // be used to serve a nice HTML page if the caller accepts that, or just JSON if not. | ||||||
|  | var HTMLOrJSONAcceptHeaders = []MIME{ | ||||||
|  | 	TextHTML, | ||||||
| 	AppJSON, | 	AppJSON, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // HTMLAcceptHeaders is a slice of offers that just contains text/html types. | // HTMLAcceptHeaders is a slice of offers that just contains text/html types. | ||||||
| var HTMLAcceptHeaders = []Offer{ | var HTMLAcceptHeaders = []MIME{ | ||||||
| 	TextHTML, | 	TextHTML, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // HTMLOrActivityPubHeaders matches text/html first, then activitypub types. | ||||||
|  | // This is useful for user URLs that a user might go to in their browser. | ||||||
|  | // https://www.w3.org/TR/activitypub/#retrieving-objects | ||||||
|  | var HTMLOrActivityPubHeaders = []MIME{ | ||||||
|  | 	TextHTML, | ||||||
|  | 	AppActivityJSON, | ||||||
|  | 	AppActivityLDJSON, | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // NegotiateAccept takes the *gin.Context from an incoming request, and a | // NegotiateAccept takes the *gin.Context from an incoming request, and a | ||||||
| // slice of Offers, and performs content negotiation for the given request | // slice of Offers, and performs content negotiation for the given request | ||||||
| // with the given content-type offers. It will return a string representation | // with the given content-type offers. It will return a string representation | ||||||
|  | @ -73,7 +80,7 @@ var HTMLAcceptHeaders = []Offer{ | ||||||
| // often-used Accept types. | // often-used Accept types. | ||||||
| // | // | ||||||
| // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation | // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation | ||||||
| func NegotiateAccept(c *gin.Context, offers ...Offer) (string, error) { | func NegotiateAccept(c *gin.Context, offers ...MIME) (string, error) { | ||||||
| 	if len(offers) == 0 { | 	if len(offers) == 0 { | ||||||
| 		return "", errors.New("no format offered") | 		return "", errors.New("no format offered") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -23,8 +23,8 @@ import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet | // NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet | ||||||
|  | @ -45,27 +45,22 @@ import ( | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/nodeinfo" | //       "$ref": "#/definitions/nodeinfo" | ||||||
| func (m *Module) NodeInfoGETHandler(c *gin.Context) { | func (m *Module) NodeInfoGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func":       "NodeInfoGETHandler", |  | ||||||
| 		"user-agent": c.Request.UserAgent(), |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ni, err := m.processor.GetNodeInfo(c.Request.Context(), c.Request) | 	ni, errWithCode := m.processor.GetNodeInfo(c.Request.Context(), c.Request) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	b, err := json.Marshal(ni) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error with get node info request: %s", err) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) | ||||||
| 		c.JSON(err.Code(), err.Safe()) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, jsonErr := json.Marshal(ni) |  | ||||||
| 	if jsonErr != nil { |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": jsonErr.Error()}) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b) | 	c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -22,8 +22,8 @@ import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet | // NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet | ||||||
|  | @ -45,19 +45,14 @@ import ( | ||||||
| //     schema: | //     schema: | ||||||
| //       "$ref": "#/definitions/wellKnownResponse" | //       "$ref": "#/definitions/wellKnownResponse" | ||||||
| func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) { | func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func": "NodeInfoWellKnownGETHandler", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | 	if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	niRel, err := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request) | 	niRel, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("error with get node info rel request: %s", err) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(err.Code(), err.Safe()) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,48 +20,45 @@ package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it. | // FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it. | ||||||
| func (m *Module) FollowersGETHandler(c *gin.Context) { | func (m *Module) FollowersGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	// usernames on our instance are always lowercase | ||||||
| 		"func": "FollowersGETHandler", | 	requestedUsername := strings.ToLower(c.Param(UsernameKey)) | ||||||
| 		"url":  c.Request.RequestURI, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	requestedUsername := c.Param(UsernameKey) |  | ||||||
| 	if requestedUsername == "" { | 	if requestedUsername == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | 		err := errors.New("no username specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) | 	format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Tracef("negotiated format: %s", format) |  | ||||||
| 
 | 
 | ||||||
| 	ctx := transferContext(c) | 	if format == string(api.TextHTML) { | ||||||
|  | 		// redirect to the user's profile | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	followers, errWithCode := m.processor.GetFediFollowers(ctx, requestedUsername, c.Request.URL) | 	resp, errWithCode := m.processor.GetFediFollowers(transferContext(c), requestedUsername, c.Request.URL) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Info(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, mErr := json.Marshal(followers) | 	b, err := json.Marshal(resp) | ||||||
| 	if mErr != nil { | 	if err != nil { | ||||||
| 		err := fmt.Errorf("could not marshal json: %s", mErr) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) | ||||||
| 		l.Error(err) |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,48 +20,45 @@ package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it. | // FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it. | ||||||
| func (m *Module) FollowingGETHandler(c *gin.Context) { | func (m *Module) FollowingGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	// usernames on our instance are always lowercase | ||||||
| 		"func": "FollowingGETHandler", | 	requestedUsername := strings.ToLower(c.Param(UsernameKey)) | ||||||
| 		"url":  c.Request.RequestURI, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	requestedUsername := c.Param(UsernameKey) |  | ||||||
| 	if requestedUsername == "" { | 	if requestedUsername == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | 		err := errors.New("no username specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) | 	format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Tracef("negotiated format: %s", format) |  | ||||||
| 
 | 
 | ||||||
| 	ctx := transferContext(c) | 	if format == string(api.TextHTML) { | ||||||
|  | 		// redirect to the user's profile | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	following, errWithCode := m.processor.GetFediFollowing(ctx, requestedUsername, c.Request.URL) | 	resp, errWithCode := m.processor.GetFediFollowing(transferContext(c), requestedUsername, c.Request.URL) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Info(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, mErr := json.Marshal(following) | 	b, err := json.Marshal(resp) | ||||||
| 	if mErr != nil { | 	if err != nil { | ||||||
| 		err := fmt.Errorf("could not marshal json: %s", mErr) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) | ||||||
| 		l.Error(err) |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,43 +19,33 @@ | ||||||
| package user | package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"errors" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // InboxPOSTHandler deals with incoming POST requests to an actor's inbox. | // InboxPOSTHandler deals with incoming POST requests to an actor's inbox. | ||||||
| // Eg., POST to https://example.org/users/whatever/inbox. | // Eg., POST to https://example.org/users/whatever/inbox. | ||||||
| func (m *Module) InboxPOSTHandler(c *gin.Context) { | func (m *Module) InboxPOSTHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	// usernames on our instance are always lowercase | ||||||
| 		"func": "InboxPOSTHandler", | 	requestedUsername := strings.ToLower(c.Param(UsernameKey)) | ||||||
| 		"url":  c.Request.RequestURI, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	requestedUsername := c.Param(UsernameKey) |  | ||||||
| 	if requestedUsername == "" { | 	if requestedUsername == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | 		err := errors.New("no username specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx := transferContext(c) | 	if posted, err := m.processor.InboxPost(transferContext(c), c.Writer, c.Request); err != nil { | ||||||
| 
 |  | ||||||
| 	posted, err := m.processor.InboxPost(ctx, c.Writer, c.Request) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if withCode, ok := err.(gtserror.WithCode); ok { | 		if withCode, ok := err.(gtserror.WithCode); ok { | ||||||
| 			l.Debugf("InboxPOSTHandler: %s", withCode.Error()) | 			api.ErrorHandler(c, withCode, m.processor.InstanceGet) | ||||||
| 			c.JSON(withCode.Code(), withCode.Safe()) | 		} else { | ||||||
| 			return | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		} | 		} | ||||||
| 		l.Debugf("InboxPOSTHandler: error processing request: %s", err) | 	} else if !posted { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) | 		err := errors.New("unable to process request") | ||||||
| 		return | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if !posted { |  | ||||||
| 		l.Debugf("InboxPOSTHandler: request could not be handled as an AP request; headers were: %+v", c.Request.Header) |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -20,13 +20,15 @@ package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // OutboxGETHandler swagger:operation GET /users/{username}/outbox s2sOutboxGet | // OutboxGETHandler swagger:operation GET /users/{username}/outbox s2sOutboxGet | ||||||
|  | @ -80,23 +82,31 @@ import ( | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
| func (m *Module) OutboxGETHandler(c *gin.Context) { | func (m *Module) OutboxGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	// usernames on our instance are always lowercase | ||||||
| 		"func": "OutboxGETHandler", | 	requestedUsername := strings.ToLower(c.Param(UsernameKey)) | ||||||
| 		"url":  c.Request.RequestURI, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	requestedUsername := c.Param(UsernameKey) |  | ||||||
| 	if requestedUsername == "" { | 	if requestedUsername == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | 		err := errors.New("no username specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if format == string(api.TextHTML) { | ||||||
|  | 		// redirect to the user's profile | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	var page bool | 	var page bool | ||||||
| 	if pageString := c.Query(PageKey); pageString != "" { | 	if pageString := c.Query(PageKey); pageString != "" { | ||||||
| 		i, err := strconv.ParseBool(pageString) | 		i, err := strconv.ParseBool(pageString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing page string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", PageKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse page query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		page = i | 		page = i | ||||||
|  | @ -114,27 +124,15 @@ func (m *Module) OutboxGETHandler(c *gin.Context) { | ||||||
| 		maxID = maxIDString | 		maxID = maxIDString | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) | 	resp, errWithCode := m.processor.GetFediOutbox(transferContext(c), requestedUsername, page, maxID, minID, c.Request.URL) | ||||||
| 	if err != nil { |  | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	l.Tracef("negotiated format: %s", format) |  | ||||||
| 
 |  | ||||||
| 	ctx := transferContext(c) |  | ||||||
| 
 |  | ||||||
| 	outbox, errWithCode := m.processor.GetFediOutbox(ctx, requestedUsername, page, maxID, minID, c.Request.URL) |  | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Info(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, mErr := json.Marshal(outbox) | 	b, err := json.Marshal(resp) | ||||||
| 	if mErr != nil { | 	if err != nil { | ||||||
| 		err := fmt.Errorf("could not marshal json: %s", mErr) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) | ||||||
| 		l.Error(err) |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,12 +20,13 @@ package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key. | // PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key. | ||||||
|  | @ -34,38 +35,34 @@ import ( | ||||||
| // in the form of a vocab.ActivityStreamsPerson. The account will only contain the id, | // in the form of a vocab.ActivityStreamsPerson. The account will only contain the id, | ||||||
| // public key, username, and type of the account. | // public key, username, and type of the account. | ||||||
| func (m *Module) PublicKeyGETHandler(c *gin.Context) { | func (m *Module) PublicKeyGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	// usernames on our instance are always lowercase | ||||||
| 		"func": "PublicKeyGETHandler", | 	requestedUsername := strings.ToLower(c.Param(UsernameKey)) | ||||||
| 		"url":  c.Request.RequestURI, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	requestedUsername := c.Param(UsernameKey) |  | ||||||
| 	if requestedUsername == "" { | 	if requestedUsername == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | 		err := errors.New("no username specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) | 	format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Tracef("negotiated format: %s", format) |  | ||||||
| 
 | 
 | ||||||
| 	ctx := transferContext(c) | 	if format == string(api.TextHTML) { | ||||||
|  | 		// redirect to the user's profile | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	user, errWithCode := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL) | 	resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Info(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, mErr := json.Marshal(user) | 	b, err := json.Marshal(resp) | ||||||
| 	if mErr != nil { | 	if err != nil { | ||||||
| 		err := fmt.Errorf("could not marshal json: %s", mErr) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) | ||||||
| 		l.Error(err) |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,13 +20,15 @@ package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // StatusRepliesGETHandler swagger:operation GET /users/{username}/statuses/{status}/replies s2sRepliesGet | // StatusRepliesGETHandler swagger:operation GET /users/{username}/statuses/{status}/replies s2sRepliesGet | ||||||
|  | @ -86,29 +88,39 @@ import ( | ||||||
| //   '404': | //   '404': | ||||||
| //      description: not found | //      description: not found | ||||||
| func (m *Module) StatusRepliesGETHandler(c *gin.Context) { | func (m *Module) StatusRepliesGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	// usernames on our instance are always lowercase | ||||||
| 		"func": "StatusRepliesGETHandler", | 	requestedUsername := strings.ToLower(c.Param(UsernameKey)) | ||||||
| 		"url":  c.Request.RequestURI, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	requestedUsername := c.Param(UsernameKey) |  | ||||||
| 	if requestedUsername == "" { | 	if requestedUsername == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | 		err := errors.New("no username specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	requestedStatusID := c.Param(StatusIDKey) | 	// status IDs on our instance are always uppercase | ||||||
|  | 	requestedStatusID := strings.ToUpper(c.Param(StatusIDKey)) | ||||||
| 	if requestedStatusID == "" { | 	if requestedStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"}) | 		err := errors.New("no status id specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if format == string(api.TextHTML) { | ||||||
|  | 		// redirect to the status | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	var page bool | 	var page bool | ||||||
| 	if pageString := c.Query(PageKey); pageString != "" { | 	if pageString := c.Query(PageKey); pageString != "" { | ||||||
| 		i, err := strconv.ParseBool(pageString) | 		i, err := strconv.ParseBool(pageString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing page string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", PageKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse page query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		page = i | 		page = i | ||||||
|  | @ -119,8 +131,8 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { | ||||||
| 	if onlyOtherAccountsString != "" { | 	if onlyOtherAccountsString != "" { | ||||||
| 		i, err := strconv.ParseBool(onlyOtherAccountsString) | 		i, err := strconv.ParseBool(onlyOtherAccountsString) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.Debugf("error parsing only_other_accounts string: %s", err) | 			err := fmt.Errorf("error parsing %s: %s", OnlyOtherAccountsKey, err) | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse only_other_accounts query param"}) | 			api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		onlyOtherAccounts = i | 		onlyOtherAccounts = i | ||||||
|  | @ -132,27 +144,15 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { | ||||||
| 		minID = minIDString | 		minID = minIDString | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) | 	resp, errWithCode := m.processor.GetFediStatusReplies(transferContext(c), requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) | ||||||
| 	if err != nil { |  | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	l.Tracef("negotiated format: %s", format) |  | ||||||
| 
 |  | ||||||
| 	ctx := transferContext(c) |  | ||||||
| 
 |  | ||||||
| 	replies, errWithCode := m.processor.GetFediStatusReplies(ctx, requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) |  | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Info(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, mErr := json.Marshal(replies) | 	b, err := json.Marshal(resp) | ||||||
| 	if mErr != nil { | 	if err != nil { | ||||||
| 		err := fmt.Errorf("could not marshal json: %s", mErr) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) | ||||||
| 		l.Error(err) |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,57 +20,53 @@ package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it. | // StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it. | ||||||
| func (m *Module) StatusGETHandler(c *gin.Context) { | func (m *Module) StatusGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func": "StatusGETHandler", |  | ||||||
| 		"url":  c.Request.RequestURI, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// usernames on our instance are always lowercase | 	// usernames on our instance are always lowercase | ||||||
| 	requestedUsername := strings.ToLower(c.Param(UsernameKey)) | 	requestedUsername := strings.ToLower(c.Param(UsernameKey)) | ||||||
| 	if requestedUsername == "" { | 	if requestedUsername == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | 		err := errors.New("no username specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// status IDs on our instance are always uppercase | 	// status IDs on our instance are always uppercase | ||||||
| 	requestedStatusID := strings.ToUpper(c.Param(StatusIDKey)) | 	requestedStatusID := strings.ToUpper(c.Param(StatusIDKey)) | ||||||
| 	if requestedStatusID == "" { | 	if requestedStatusID == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"}) | 		err := errors.New("no status id specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) | 	format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Tracef("negotiated format: %s", format) |  | ||||||
| 
 | 
 | ||||||
| 	ctx := transferContext(c) | 	if format == string(api.TextHTML) { | ||||||
|  | 		// redirect to the status | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	status, errWithCode := m.processor.GetFediStatus(ctx, requestedUsername, requestedStatusID, c.Request.URL) | 	resp, errWithCode := m.processor.GetFediStatus(transferContext(c), requestedUsername, requestedStatusID, c.Request.URL) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Info(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, mErr := json.Marshal(status) | 	b, err := json.Marshal(resp) | ||||||
| 	if mErr != nil { | 	if err != nil { | ||||||
| 		err := fmt.Errorf("could not marshal json: %s", mErr) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) | ||||||
| 		l.Error(err) |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,12 +20,13 @@ package user | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api" | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // UsersGETHandler should be served at https://example.org/users/:username. | // UsersGETHandler should be served at https://example.org/users/:username. | ||||||
|  | @ -38,38 +39,34 @@ import ( | ||||||
| // And of course, the request should be refused if the account or server making the | // And of course, the request should be refused if the account or server making the | ||||||
| // request is blocked. | // request is blocked. | ||||||
| func (m *Module) UsersGETHandler(c *gin.Context) { | func (m *Module) UsersGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ | 	// usernames on our instance are always lowercase | ||||||
| 		"func": "UsersGETHandler", | 	requestedUsername := strings.ToLower(c.Param(UsernameKey)) | ||||||
| 		"url":  c.Request.RequestURI, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	requestedUsername := c.Param(UsernameKey) |  | ||||||
| 	if requestedUsername == "" { | 	if requestedUsername == "" { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) | 		err := errors.New("no username specified in request") | ||||||
|  | 		api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) | 	format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) | 		api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	l.Tracef("negotiated format: %s", format) |  | ||||||
| 
 | 
 | ||||||
| 	ctx := transferContext(c) | 	if format == string(api.TextHTML) { | ||||||
|  | 		// redirect to the user's profile | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	user, errWithCode := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL) // GetFediUser handles auth as well | 	resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Info(errWithCode.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, mErr := json.Marshal(user) | 	b, err := json.Marshal(resp) | ||||||
| 	if mErr != nil { | 	if err != nil { | ||||||
| 		err := fmt.Errorf("could not marshal json: %s", mErr) | 		api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) | ||||||
| 		l.Error(err) |  | ||||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -27,6 +27,7 @@ import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -105,10 +106,9 @@ func (m *Module) WebfingerGETRequest(c *gin.Context) { | ||||||
| 		ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) | 		ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	resp, err := m.processor.GetWebfingerAccount(ctx, username) | 	resp, errWithCode := m.processor.GetWebfingerAccount(ctx, username) | ||||||
| 	if err != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debugf("aborting request with an error: %s", err.Error()) | 		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) | ||||||
| 		c.JSON(err.Code(), gin.H{"error": err.Safe()}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,3 +1,21 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    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 security | package security | ||||||
| 
 | 
 | ||||||
| import "github.com/gin-gonic/gin" | import "github.com/gin-gonic/gin" | ||||||
|  |  | ||||||
|  | @ -19,21 +19,17 @@ | ||||||
| package security | package security | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // UserAgentBlock blocks requests with undesired, empty, or invalid user-agent strings. | // UserAgentBlock aborts requests with empty user agent strings. | ||||||
| func (m *Module) UserAgentBlock(c *gin.Context) { | func (m *Module) UserAgentBlock(c *gin.Context) { | ||||||
| 	l := logrus.WithFields(logrus.Fields{ |  | ||||||
| 		"func": "UserAgentBlock", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	if ua := c.Request.UserAgent(); ua == "" { | 	if ua := c.Request.UserAgent(); ua == "" { | ||||||
| 		l.Debug("aborting request because there's no user-agent set") | 		code := http.StatusTeapot | ||||||
| 		c.AbortWithStatus(http.StatusTeapot) | 		err := errors.New(http.StatusText(code) + ": no user-agent sent with request") | ||||||
| 		return | 		c.AbortWithStatusJSON(code, gin.H{"error": err.Error()}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -130,12 +130,3 @@ func (cfg *Configuration) MarshalMap() (map[string]interface{}, error) { | ||||||
| 	} | 	} | ||||||
| 	return dst, nil | 	return dst, nil | ||||||
| } | } | ||||||
| 
 |  | ||||||
| // UnmarshalMap will unmarshal a map structure into the receiving Configuration. |  | ||||||
| func (cfg *Configuration) UnmarshalMap(src map[string]interface{}) error { |  | ||||||
| 	dec, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ |  | ||||||
| 		TagName: "name", |  | ||||||
| 		Result:  cfg, |  | ||||||
| 	}) |  | ||||||
| 	return dec.Decode(src) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/mitchellh/mapstructure" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 	"github.com/spf13/viper" | 	"github.com/spf13/viper" | ||||||
| ) | ) | ||||||
|  | @ -129,8 +130,10 @@ func (st *ConfigState) reloadToViper() { | ||||||
| 
 | 
 | ||||||
| // reloadFromViper will reload Configuration{} values from viper. | // reloadFromViper will reload Configuration{} values from viper. | ||||||
| func (st *ConfigState) reloadFromViper() { | func (st *ConfigState) reloadFromViper() { | ||||||
| 	err := st.config.UnmarshalMap(st.viper.AllSettings()) | 	if err := st.viper.Unmarshal(&st.config, func(c *mapstructure.DecoderConfig) { | ||||||
| 	if err != nil { | 		c.TagName = "name" | ||||||
|  | 		c.ZeroFields = true // empty the config struct before we marshal values into it | ||||||
|  | 	}); err != nil { | ||||||
| 		panic(err) | 		panic(err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -126,7 +126,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 	vi := ctx.Value(ap.ContextRequestingPublicKeyVerifier) | 	vi := ctx.Value(ap.ContextRequestingPublicKeyVerifier) | ||||||
| 	if vi == nil { | 	if vi == nil { | ||||||
| 		err := errors.New("http request wasn't signed or http signature was invalid") | 		err := errors.New("http request wasn't signed or http signature was invalid") | ||||||
| 		errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error()) | 		errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) | ||||||
| 		l.Debug(errWithCode) | 		l.Debug(errWithCode) | ||||||
| 		return nil, errWithCode | 		return nil, errWithCode | ||||||
| 	} | 	} | ||||||
|  | @ -134,7 +134,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 	verifier, ok := vi.(httpsig.Verifier) | 	verifier, ok := vi.(httpsig.Verifier) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		err := errors.New("http request wasn't signed or http signature was invalid") | 		err := errors.New("http request wasn't signed or http signature was invalid") | ||||||
| 		errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error()) | 		errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) | ||||||
| 		l.Debug(errWithCode) | 		l.Debug(errWithCode) | ||||||
| 		return nil, errWithCode | 		return nil, errWithCode | ||||||
| 	} | 	} | ||||||
|  | @ -143,7 +143,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 	si := ctx.Value(ap.ContextRequestingPublicKeySignature) | 	si := ctx.Value(ap.ContextRequestingPublicKeySignature) | ||||||
| 	if si == nil { | 	if si == nil { | ||||||
| 		err := errors.New("http request wasn't signed or http signature was invalid") | 		err := errors.New("http request wasn't signed or http signature was invalid") | ||||||
| 		errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error()) | 		errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) | ||||||
| 		l.Debug(errWithCode) | 		l.Debug(errWithCode) | ||||||
| 		return nil, errWithCode | 		return nil, errWithCode | ||||||
| 	} | 	} | ||||||
|  | @ -151,7 +151,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 	signature, ok := si.(string) | 	signature, ok := si.(string) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		err := errors.New("http request wasn't signed or http signature was invalid") | 		err := errors.New("http request wasn't signed or http signature was invalid") | ||||||
| 		errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error()) | 		errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) | ||||||
| 		l.Debug(errWithCode) | 		l.Debug(errWithCode) | ||||||
| 		return nil, errWithCode | 		return nil, errWithCode | ||||||
| 	} | 	} | ||||||
|  | @ -209,7 +209,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 		// The actual http call to the remote server is made right here in the Dereference function. | 		// The actual http call to the remote server is made right here in the Dereference function. | ||||||
| 		b, err := transport.Dereference(ctx, requestingPublicKeyID) | 		b, err := transport.Dereference(ctx, requestingPublicKeyID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("error dereferencing public key %s: %s", requestingPublicKeyID, err)) | 			errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error dereferencing public key %s: %s", requestingPublicKeyID, err)) | ||||||
| 			l.Debug(errWithCode) | 			l.Debug(errWithCode) | ||||||
| 			return nil, errWithCode | 			return nil, errWithCode | ||||||
| 		} | 		} | ||||||
|  | @ -217,7 +217,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 		// if the key isn't in the response, we can't authenticate the request | 		// if the key isn't in the response, we can't authenticate the request | ||||||
| 		requestingPublicKey, err := getPublicKeyFromResponse(ctx, b, requestingPublicKeyID) | 		requestingPublicKey, err := getPublicKeyFromResponse(ctx, b, requestingPublicKeyID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("error parsing public key %s: %s", requestingPublicKeyID, err)) | 			errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error parsing public key %s: %s", requestingPublicKeyID, err)) | ||||||
| 			l.Debug(errWithCode) | 			l.Debug(errWithCode) | ||||||
| 			return nil, errWithCode | 			return nil, errWithCode | ||||||
| 		} | 		} | ||||||
|  | @ -225,7 +225,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 		// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey | 		// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey | ||||||
| 		pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() | 		pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() | ||||||
| 		if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { | 		if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { | ||||||
| 			errWithCode := gtserror.NewErrorNotAuthorized(errors.New("publicKeyPem property is not provided or it is not embedded as a value")) | 			errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyPem property is not provided or it is not embedded as a value")) | ||||||
| 			l.Debug(errWithCode) | 			l.Debug(errWithCode) | ||||||
| 			return nil, errWithCode | 			return nil, errWithCode | ||||||
| 		} | 		} | ||||||
|  | @ -234,14 +234,14 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 		pubKeyPem := pkPemProp.Get() | 		pubKeyPem := pkPemProp.Get() | ||||||
| 		block, _ := pem.Decode([]byte(pubKeyPem)) | 		block, _ := pem.Decode([]byte(pubKeyPem)) | ||||||
| 		if block == nil || block.Type != "PUBLIC KEY" { | 		if block == nil || block.Type != "PUBLIC KEY" { | ||||||
| 			errWithCode := gtserror.NewErrorNotAuthorized(errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")) | 			errWithCode := gtserror.NewErrorUnauthorized(errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")) | ||||||
| 			l.Debug(errWithCode) | 			l.Debug(errWithCode) | ||||||
| 			return nil, errWithCode | 			return nil, errWithCode | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		publicKey, err = x509.ParsePKIXPublicKey(block.Bytes) | 		publicKey, err = x509.ParsePKIXPublicKey(block.Bytes) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("could not parse public key %s from block bytes: %s", requestingPublicKeyID, err)) | 			errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("could not parse public key %s from block bytes: %s", requestingPublicKeyID, err)) | ||||||
| 			l.Debug(errWithCode) | 			l.Debug(errWithCode) | ||||||
| 			return nil, errWithCode | 			return nil, errWithCode | ||||||
| 		} | 		} | ||||||
|  | @ -249,7 +249,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 		// all good! we just need the URI of the key owner to return | 		// all good! we just need the URI of the key owner to return | ||||||
| 		pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() | 		pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() | ||||||
| 		if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { | 		if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { | ||||||
| 			errWithCode := gtserror.NewErrorNotAuthorized(errors.New("publicKeyOwner property is not provided or it is not embedded as a value")) | 			errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyOwner property is not provided or it is not embedded as a value")) | ||||||
| 			l.Debug(errWithCode) | 			l.Debug(errWithCode) | ||||||
| 			return nil, errWithCode | 			return nil, errWithCode | ||||||
| 		} | 		} | ||||||
|  | @ -280,7 +280,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU | ||||||
| 		l.Tracef("authentication for %s NOT PASSED with algorithm %s: %s", pkOwnerURI, algo, err) | 		l.Tracef("authentication for %s NOT PASSED with algorithm %s: %s", pkOwnerURI, algo, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature)) | 	errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature)) | ||||||
| 	l.Debug(errWithCode) | 	l.Debug(errWithCode) | ||||||
| 	return nil, errWithCode | 	return nil, errWithCode | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -60,7 +60,7 @@ func (e withCode) Code() int { | ||||||
| 
 | 
 | ||||||
| // NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text. | // NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text. | ||||||
| func NewErrorBadRequest(original error, helpText ...string) WithCode { | func NewErrorBadRequest(original error, helpText ...string) WithCode { | ||||||
| 	safe := "bad request" | 	safe := http.StatusText(http.StatusBadRequest) | ||||||
| 	if helpText != nil { | 	if helpText != nil { | ||||||
| 		safe = safe + ": " + strings.Join(helpText, ": ") | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
| 	} | 	} | ||||||
|  | @ -71,9 +71,9 @@ func NewErrorBadRequest(original error, helpText ...string) WithCode { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text. | // NewErrorUnauthorized returns an ErrorWithCode 401 with the given original error and optional help text. | ||||||
| func NewErrorNotAuthorized(original error, helpText ...string) WithCode { | func NewErrorUnauthorized(original error, helpText ...string) WithCode { | ||||||
| 	safe := "not authorized" | 	safe := http.StatusText(http.StatusUnauthorized) | ||||||
| 	if helpText != nil { | 	if helpText != nil { | ||||||
| 		safe = safe + ": " + strings.Join(helpText, ": ") | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
| 	} | 	} | ||||||
|  | @ -86,7 +86,7 @@ func NewErrorNotAuthorized(original error, helpText ...string) WithCode { | ||||||
| 
 | 
 | ||||||
| // NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text. | // NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text. | ||||||
| func NewErrorForbidden(original error, helpText ...string) WithCode { | func NewErrorForbidden(original error, helpText ...string) WithCode { | ||||||
| 	safe := "forbidden" | 	safe := http.StatusText(http.StatusForbidden) | ||||||
| 	if helpText != nil { | 	if helpText != nil { | ||||||
| 		safe = safe + ": " + strings.Join(helpText, ": ") | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
| 	} | 	} | ||||||
|  | @ -99,7 +99,7 @@ func NewErrorForbidden(original error, helpText ...string) WithCode { | ||||||
| 
 | 
 | ||||||
| // NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text. | // NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text. | ||||||
| func NewErrorNotFound(original error, helpText ...string) WithCode { | func NewErrorNotFound(original error, helpText ...string) WithCode { | ||||||
| 	safe := "404 not found" | 	safe := http.StatusText(http.StatusNotFound) | ||||||
| 	if helpText != nil { | 	if helpText != nil { | ||||||
| 		safe = safe + ": " + strings.Join(helpText, ": ") | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
| 	} | 	} | ||||||
|  | @ -112,7 +112,7 @@ func NewErrorNotFound(original error, helpText ...string) WithCode { | ||||||
| 
 | 
 | ||||||
| // NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text. | // NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text. | ||||||
| func NewErrorInternalError(original error, helpText ...string) WithCode { | func NewErrorInternalError(original error, helpText ...string) WithCode { | ||||||
| 	safe := "internal server error" | 	safe := http.StatusText(http.StatusInternalServerError) | ||||||
| 	if helpText != nil { | 	if helpText != nil { | ||||||
| 		safe = safe + ": " + strings.Join(helpText, ": ") | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
| 	} | 	} | ||||||
|  | @ -125,7 +125,7 @@ func NewErrorInternalError(original error, helpText ...string) WithCode { | ||||||
| 
 | 
 | ||||||
| // NewErrorConflict returns an ErrorWithCode 409 with the given original error and optional help text. | // NewErrorConflict returns an ErrorWithCode 409 with the given original error and optional help text. | ||||||
| func NewErrorConflict(original error, helpText ...string) WithCode { | func NewErrorConflict(original error, helpText ...string) WithCode { | ||||||
| 	safe := "conflict" | 	safe := http.StatusText(http.StatusConflict) | ||||||
| 	if helpText != nil { | 	if helpText != nil { | ||||||
| 		safe = safe + ": " + strings.Join(helpText, ": ") | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
| 	} | 	} | ||||||
|  | @ -135,3 +135,29 @@ func NewErrorConflict(original error, helpText ...string) WithCode { | ||||||
| 		code:     http.StatusConflict, | 		code:     http.StatusConflict, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // NewErrorNotAcceptable returns an ErrorWithCode 406 with the given original error and optional help text. | ||||||
|  | func NewErrorNotAcceptable(original error, helpText ...string) WithCode { | ||||||
|  | 	safe := http.StatusText(http.StatusNotAcceptable) | ||||||
|  | 	if helpText != nil { | ||||||
|  | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
|  | 	} | ||||||
|  | 	return withCode{ | ||||||
|  | 		original: original, | ||||||
|  | 		safe:     errors.New(safe), | ||||||
|  | 		code:     http.StatusNotAcceptable, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewErrorUnprocessableEntity returns an ErrorWithCode 422 with the given original error and optional help text. | ||||||
|  | func NewErrorUnprocessableEntity(original error, helpText ...string) WithCode { | ||||||
|  | 	safe := http.StatusText(http.StatusUnprocessableEntity) | ||||||
|  | 	if helpText != nil { | ||||||
|  | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
|  | 	} | ||||||
|  | 	return withCode{ | ||||||
|  | 		original: original, | ||||||
|  | 		safe:     errors.New(safe), | ||||||
|  | 		code:     http.StatusUnprocessableEntity, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -24,24 +24,28 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, error) { | func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, gtserror.WithCode) { | ||||||
| 	l := logrus.WithField("func", "HandleCallback") | 	l := logrus.WithField("func", "HandleCallback") | ||||||
| 	if code == "" { | 	if code == "" { | ||||||
| 		return nil, errors.New("code was empty string") | 		err := errors.New("code was empty string") | ||||||
|  | 		return nil, gtserror.NewErrorBadRequest(err, err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Debug("exchanging code for oauth2token") | 	l.Debug("exchanging code for oauth2token") | ||||||
| 	oauth2Token, err := i.oauth2Config.Exchange(ctx, code) | 	oauth2Token, err := i.oauth2Config.Exchange(ctx, code) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error exchanging code for oauth2token: %s", err) | 		err := fmt.Errorf("error exchanging code for oauth2token: %s", err) | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Debug("extracting id_token") | 	l.Debug("extracting id_token") | ||||||
| 	rawIDToken, ok := oauth2Token.Extra("id_token").(string) | 	rawIDToken, ok := oauth2Token.Extra("id_token").(string) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return nil, errors.New("no id_token in oauth2token") | 		err := errors.New("no id_token in oauth2token") | ||||||
|  | 		return nil, gtserror.NewErrorBadRequest(err, err.Error()) | ||||||
| 	} | 	} | ||||||
| 	l.Debugf("raw id token: %s", rawIDToken) | 	l.Debugf("raw id token: %s", rawIDToken) | ||||||
| 
 | 
 | ||||||
|  | @ -50,13 +54,15 @@ func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, error) | ||||||
| 	idTokenVerifier := i.provider.Verifier(i.oidcConf) | 	idTokenVerifier := i.provider.Verifier(i.oidcConf) | ||||||
| 	idToken, err := idTokenVerifier.Verify(ctx, rawIDToken) | 	idToken, err := idTokenVerifier.Verify(ctx, rawIDToken) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("could not verify id token: %s", err) | 		err = fmt.Errorf("could not verify id token: %s", err) | ||||||
|  | 		return nil, gtserror.NewErrorUnauthorized(err, err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Debug("extracting claims from id_token") | 	l.Debug("extracting claims from id_token") | ||||||
| 	claims := &Claims{} | 	claims := &Claims{} | ||||||
| 	if err := idToken.Claims(claims); err != nil { | 	if err := idToken.Claims(claims); err != nil { | ||||||
| 		return nil, fmt.Errorf("could not parse claims from idToken: %s", err) | 		err := fmt.Errorf("could not parse claims from idToken: %s", err) | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err, err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return claims, nil | 	return claims, nil | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/coreos/go-oidc/v3/oidc" | 	"github.com/coreos/go-oidc/v3/oidc" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"golang.org/x/oauth2" | 	"golang.org/x/oauth2" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -39,7 +40,7 @@ type IDP interface { | ||||||
| 	// with a set of claims. | 	// with a set of claims. | ||||||
| 	// | 	// | ||||||
| 	// Note that this function *does not* verify state. That should be handled by the caller *before* this function is called. | 	// Note that this function *does not* verify state. That should be handled by the caller *before* this function is called. | ||||||
| 	HandleCallback(ctx context.Context, code string) (*Claims, error) | 	HandleCallback(ctx context.Context, code string) (*Claims, gtserror.WithCode) | ||||||
| 	// AuthCodeURL returns the proper redirect URL for this IDP, for redirecting requesters to the correct OIDC endpoint. | 	// AuthCodeURL returns the proper redirect URL for this IDP, for redirecting requesters to the correct OIDC endpoint. | ||||||
| 	AuthCodeURL(state string) string | 	AuthCodeURL(state string) string | ||||||
| } | } | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue