forked from LittleBigRefresh/Refresh
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathResourceApiEndpoints.cs
More file actions
225 lines (191 loc) · 11.7 KB
/
Copy pathResourceApiEndpoints.cs
File metadata and controls
225 lines (191 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
using AttribDoc.Attributes;
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.RateLimit;
using Bunkum.Core.Responses;
using Bunkum.Core.Storage;
using Bunkum.Listener.Protocol;
using Bunkum.Protocols.Http;
using Refresh.Common.Verification;
using Refresh.Core.Authentication.Permission;
using Refresh.Core.Configuration;
using Refresh.Core.Importing;
using Refresh.Core.Services;
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Assets;
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes.Errors;
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Data;
namespace Refresh.Interfaces.APIv3.Endpoints;
public class ResourceApiEndpoints : EndpointGroup
{
private const int RequestTimeoutDuration = 60;
private const int RawAssetRequestAmount = 70;
private const int ImageAssetRequestAmount = 100;
private const int AssetInfoRequestAmount = 40;
private const int RequestBlockDuration = 30;
private const string RawAssetBucket = "raw-asset-api";
private const string ImageAssetBucket = "image-asset-api";
private const string AssetInfoBucket = "asset-info-api";
[ApiV3Endpoint("assets/{hash}/download"), Authentication(false)]
[ClientCacheResponse(31556952)] // 1 year, we don't expect the data to change
[DocSummary("Downloads the raw data for an asset hash. Sent as application/octet-stream")]
[DocError(typeof(ApiNotFoundError), "The asset could not be found")]
[DocError(typeof(ApiInternalError), ApiInternalError.CouldNotGetAssetErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.HashMissingErrorWhen)]
[RateLimitSettings(RequestTimeoutDuration, RawAssetRequestAmount, RequestBlockDuration, RawAssetBucket)]
public Response DownloadGameAsset(RequestContext context, IDataStore dataStore,
[DocSummary("The SHA1 hash of the asset")] string hash)
{
bool isPspAsset = hash.StartsWith("psp/");
string realHash = isPspAsset ? hash[4..] : hash;
if (!CommonPatterns.Sha1Regex().IsMatch(realHash)) return ApiValidationError.HashInvalidError;
if (string.IsNullOrWhiteSpace(realHash)) return ApiValidationError.HashMissingError;
if (!dataStore.ExistsInStore(hash)) return ApiNotFoundError.Instance;
bool gotData = dataStore.TryGetDataFromStore(hash, out byte[]? data);
if (data == null || !gotData) return ApiInternalError.CouldNotGetAssetError;
return new Response(data, ContentType.BinaryData);
}
[ApiV3Endpoint("assets/psp/{hash}/download"), Authentication(false)]
[ClientCacheResponse(31556952)] // 1 year, we don't expect the data to change
[DocSummary("Downloads the raw data for a PSP asset hash. Sent as application/octet-stream")]
[DocError(typeof(ApiNotFoundError), "The asset could not be found")]
[DocError(typeof(ApiInternalError), ApiInternalError.CouldNotGetAssetErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.HashMissingErrorWhen)]
[RateLimitSettings(RequestTimeoutDuration, RawAssetRequestAmount, RequestBlockDuration, RawAssetBucket)]
public Response DownloadPspGameAsset(RequestContext context, IDataStore dataStore,
[DocSummary("The SHA1 hash of the asset")] string hash)
=> this.DownloadGameAsset(context, dataStore, $"psp/{hash}");
[ApiV3Endpoint("assets/{hash}/image", ContentType.Png), Authentication(false)]
[ClientCacheResponse(9204111)] // 1 week, data may or may not change
[DocSummary("Downloads any game texture (if it can be converted) as a PNG. Sent as image/png")]
[DocError(typeof(ApiNotFoundError), "The asset could not be found")]
[DocError(typeof(ApiInternalError), ApiInternalError.CouldNotGetAssetErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.HashMissingErrorWhen)]
[RateLimitSettings(RequestTimeoutDuration, ImageAssetRequestAmount, RequestBlockDuration, ImageAssetBucket)]
public Response DownloadGameAssetAsImage(RequestContext context, IDataStore dataStore, GameDatabaseContext database,
[DocSummary("The SHA1 hash of the asset")] string hash, ImageImporter imageImport, AssetImporter assetImport, DataContext dataContext)
{
bool isPspAsset = hash.StartsWith("psp/");
string realHash = isPspAsset ? hash[4..] : hash;
if (!CommonPatterns.Sha1Regex().IsMatch(realHash)) return ApiValidationError.HashInvalidError;
if (string.IsNullOrWhiteSpace(realHash)) return ApiValidationError.HashMissingError;
if (!dataStore.ExistsInStore(hash)) return ApiNotFoundError.Instance;
if (!dataStore.ExistsInStore("png/" + realHash))
{
GameAssetType? convertedType = database.GetConvertedType(realHash);
//If we found that the hash is a converted hash
if (convertedType != null)
{
//Import the converted hash from the data store
imageImport.ImportAsset(realHash, isPspAsset, convertedType.Value, dataStore);
}
//If not,
else
{
//Import the asset as normal
GameAsset? asset = dataContext.Cache.GetAssetInfo(realHash, database);
imageImport.ImportAsset(realHash, isPspAsset, asset?.AssetType, dataStore);
}
}
bool gotData = dataStore.TryGetDataFromStore("png/" + realHash, out byte[]? data);
if (data == null || !gotData) return ApiInternalError.CouldNotGetAssetError;
return new Response(data, ContentType.Png);
}
[ApiV3Endpoint("assets/psp/{hash}/image", ContentType.Png), Authentication(false)]
[ClientCacheResponse(9204111)] // 1 week, data may or may not change
[DocSummary("Downloads any PSP game texture (if it can be converted) as a PNG. Sent as image/png")]
[DocError(typeof(ApiNotFoundError), "The asset could not be found")]
[DocError(typeof(ApiInternalError), ApiInternalError.CouldNotGetAssetErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.HashMissingErrorWhen)]
[RateLimitSettings(RequestTimeoutDuration, ImageAssetRequestAmount, RequestBlockDuration, ImageAssetBucket)]
public Response DownloadPspGameAssetAsImage(RequestContext context, IDataStore dataStore, GameDatabaseContext database,
[DocSummary("The SHA1 hash of the asset")] string hash, ImageImporter imageImport, AssetImporter assetImport, DataContext dataContext)
=> this.DownloadGameAssetAsImage(context, dataStore, database, $"psp/{hash}", imageImport, assetImport, dataContext);
[ApiV3Endpoint("assets/{hash}"), Authentication(false)]
[DocSummary("Gets information from the database about a particular hash. Includes user who uploaded, dependencies, timestamps, etc.")]
[DocError(typeof(ApiNotFoundError), "The asset could not be found")]
[DocError(typeof(ApiValidationError), ApiValidationError.HashMissingErrorWhen)]
[RateLimitSettings(RequestTimeoutDuration, AssetInfoRequestAmount, RequestBlockDuration, AssetInfoBucket)]
public ApiResponse<ApiGameAssetResponse> GetAssetInfo(RequestContext context, GameDatabaseContext database,
IDataStore dataStore,
[DocSummary("The SHA1 hash of the asset")]
string hash, DataContext dataContext)
{
bool isPspAsset = hash.StartsWith("psp/");
string realHash = isPspAsset ? hash[4..] : hash;
if (!CommonPatterns.Sha1Regex().IsMatch(realHash)) return ApiValidationError.HashInvalidError;
if (string.IsNullOrWhiteSpace(realHash)) return ApiValidationError.HashMissingError;
GameAsset? asset = dataContext.Cache.GetAssetInfo(realHash, database);
if (asset == null) return ApiNotFoundError.Instance;
return ApiGameAssetResponse.FromOld(asset, dataContext);
}
[ApiV3Endpoint("assets/psp/{hash}"), Authentication(false)]
[DocSummary("Gets information from the database about a particular PSP hash. Includes user who uploaded, dependencies, timestamps, etc.")]
[DocError(typeof(ApiValidationError), ApiValidationError.HashMissingErrorWhen)]
[DocError(typeof(ApiNotFoundError), "The asset could not be found")]
[RateLimitSettings(RequestTimeoutDuration, AssetInfoRequestAmount, RequestBlockDuration, AssetInfoBucket)]
public ApiResponse<ApiGameAssetResponse> GetPspAssetInfo(RequestContext context, GameDatabaseContext database,
IDataStore dataStore,
[DocSummary("The SHA1 hash of the asset")]
string hash, DataContext dataContext) => this.GetAssetInfo(context, database, dataStore, $"psp/{hash}", dataContext);
[ApiV3Endpoint("assets/{hash}", HttpMethods.Post)]
[RequireEmailVerified]
[DocSummary("Uploads an image (PNG/JPEG) asset")]
[DocError(typeof(ApiValidationError), ApiValidationError.HashMissingErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.BodyTooLongErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.CannotReadAssetErrorWhen)]
[DocError(typeof(ApiValidationError), ApiValidationError.BodyMustBeImageErrorWhen)]
[RateLimitSettings(420, 10, 300, "image-upload-api")]
public ApiResponse<ApiGameAssetResponse> UploadImageAsset(RequestContext context, GameDatabaseContext database,
IDataStore dataStore, AssetImporter importer, GameServerConfig config,
[DocSummary("The SHA1 hash of the asset")]
string hash,
byte[] body, GameUser user, DataContext dataContext,
AipiService? aipi,
DiscordStaffService? discord,
IntegrationConfig integration
)
{
// If we're blocking asset uploads, throw unless the user is an admin.
// We also have the ability to block asset uploads for trusted users (when they would normally bypass this)
if (user.IsWriteBlocked(config))
return ApiAuthenticationError.ReadOnlyError;
if (!CommonPatterns.Sha1Regex().IsMatch(hash)) return ApiValidationError.HashInvalidError;
if (dataStore.ExistsInStore(hash))
{
GameAsset? existingAsset = dataContext.Cache.GetAssetInfo(hash, database);
if (existingAsset == null)
return ApiInternalError.HashNotFoundInDatabaseError;
return ApiGameAssetResponse.FromOld(existingAsset, dataContext);
}
if (body.Length > 1_048_576 * 2)
{
return new ApiValidationError($"The asset must be under 2MB. Your file was {body.Length:N0} bytes.");
}
if (body.Length + user.FilesizeQuotaUsage > config.UserFilesizeQuota)
{
context.Logger.LogWarning(BunkumCategory.UserContent, "User {0} has hit the filesize quota ({1} bytes), rejecting.", user.Username, config.UserFilesizeQuota);
return new ApiValidationError($"You have exceeded your filesize quota.");
}
GameAsset? gameAsset = importer.ReadAndVerifyAsset(hash, body, TokenPlatform.Website, database);
if (gameAsset == null)
return ApiValidationError.CannotReadAssetError;
if (gameAsset.AssetType is not GameAssetType.Jpeg and not GameAssetType.Png)
return ApiValidationError.BodyMustBeImageError;
if (!dataStore.WriteToStore(hash, body))
return ApiInternalError.CouldNotWriteAssetError;
gameAsset.OriginalUploader = user;
if (aipi != null && aipi.ScanAndHandleAsset(dataContext, gameAsset))
{
return ApiModerationError.Instance;
}
database.AddAssetToDatabase(gameAsset);
dataContext.Cache.CacheAsset(gameAsset.AssetHash, gameAsset);
database.IncrementUserFilesizeQuota(user, body.Length);
return new ApiResponse<ApiGameAssetResponse>(ApiGameAssetResponse.FromOld(gameAsset, dataContext)!, Created);
}
}