the files
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Campus_live_rewrite.api;
|
||||
using Campus_live_rewrite.api.dbs;
|
||||
using Campus_live_rewrite.api.server;
|
||||
|
||||
namespace Campus_live_rewrite.Controllers
|
||||
{
|
||||
// Real invention SAVE (Maker Pen "save selection as invention") + publish + my-list.
|
||||
// The save endpoints used to ack with the wrong shape, so the client reported
|
||||
// "Error uploading invention data". The client's decoder is lenient (it reads
|
||||
// Invention.InventionId + the Status/Success envelope and drops unknown fields), so a
|
||||
// correctly-STRUCTURED reply is what it needs. The circuit blob is uploaded separately and
|
||||
// referenced here by inventionDataFilename.
|
||||
[ApiController]
|
||||
public class InventionController : ControllerBase
|
||||
{
|
||||
private static ContentResult J(object o, int status = 200) =>
|
||||
new() { Content = JsonConvert.SerializeObject(o), ContentType = "application/json", StatusCode = status };
|
||||
|
||||
private async Task<JObject?> ReadBody()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var r = new StreamReader(Request.Body);
|
||||
var s = await r.ReadToEndAsync();
|
||||
return string.IsNullOrWhiteSpace(s) ? new JObject() : JObject.Parse(s);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static InventionDB.Invention BuildFromBody(ulong creator, JObject b)
|
||||
{
|
||||
string name = (b.Value<string>("name") ?? "").Trim(); if (name.Length == 0) name = "Untitled";
|
||||
string desc = (b.Value<string>("description") ?? "").Trim(); if (desc.Length == 0) desc = "No description yet";
|
||||
var inv = new InventionDB.Invention
|
||||
{
|
||||
CreatorPlayerId = creator,
|
||||
Name = name.Length > 24 ? name.Substring(0, 24) : name,
|
||||
Description = desc.Length > 512 ? desc.Substring(0, 512) : desc,
|
||||
ImageName = b.Value<string>("imageName") ?? "",
|
||||
BlobName = b.Value<string>("inventionDataFilename") ?? "",
|
||||
InstantiationCost = b.Value<int?>("instantiationCost") ?? 0,
|
||||
LightsCost = b.Value<int?>("lightsCost") ?? 0,
|
||||
ChipsCost = b.Value<int?>("chipsCost") ?? 0,
|
||||
CloudVariablesCost = b.Value<int?>("cloudVariablesCost") ?? 0,
|
||||
AICost = b.Value<int?>("aiCost") ?? 0,
|
||||
CreationRoomId = b.Value<int?>("creationRoomId") ?? 0,
|
||||
UgcVersion = b.Value<int?>("ugcVersion") ?? 0,
|
||||
HasBetaContent = b.Value<bool?>("hasBetaContent") ?? false,
|
||||
LongDescription = b.Value<string>("longDescription"),
|
||||
DisplayMetadataJson = b.Value<string>("displayMetadataJson"),
|
||||
ReferencedInventions = b["referencedInventions"]?.ToObject<List<int>>() ?? new(),
|
||||
ReferencedUnityAssetIds = b["referencedUnityAssetIds"]?.ToObject<List<string>>() ?? new(),
|
||||
};
|
||||
if (b["tagsRequest"] is JObject tr)
|
||||
{
|
||||
var custom = tr["CustomTags"]?.ToObject<List<string>>() ?? new();
|
||||
var auto = tr["AutoTags"]?.ToObject<List<string>>() ?? new();
|
||||
inv.Tags = custom.Concat(auto).Where(t => !string.IsNullOrWhiteSpace(t)).Distinct().ToList();
|
||||
}
|
||||
return InventionDB.Create(inv);
|
||||
}
|
||||
|
||||
// GET /api/inventions/v1/version?inventionId=N&version=V - the bare RRInventionVersion,
|
||||
// which carries the blob name the client downloads. Only the CURRENT version exists here
|
||||
// (nothing writes history), so any other number 404s rather than naming a blob that is not
|
||||
// there. version=0 means "whichever is current". (Reference: api/routes/avatar.ts.)
|
||||
[HttpGet("/api/inventions/v1/version")]
|
||||
public ContentResult GetVersion([FromQuery] int inventionId = 0, [FromQuery] int version = 0)
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
if (inventionId <= 0) return J(new { error = "inventionId is required" }, 400);
|
||||
|
||||
var inv = InventionDB.GetById(inventionId);
|
||||
if (inv == null) return new ContentResult { Content = "", ContentType = "application/json", StatusCode = 404 };
|
||||
if (version != 0 && version != inv.CurrentVersionNumber)
|
||||
return new ContentResult { Content = "", ContentType = "application/json", StatusCode = 404 };
|
||||
|
||||
return J(VersionDto(inv));
|
||||
}
|
||||
|
||||
private static object VersionDto(InventionDB.Invention i) => new
|
||||
{
|
||||
InventionId = i.InventionId,
|
||||
ReplicationId = i.ReplicationId,
|
||||
VersionNumber = i.CurrentVersionNumber,
|
||||
BlobName = i.BlobName,
|
||||
BlobHash = (string?)null,
|
||||
InstantiationCost = i.InstantiationCost,
|
||||
LightsCost = i.LightsCost,
|
||||
ChipsCost = i.ChipsCost,
|
||||
CloudVariablesCost = i.CloudVariablesCost,
|
||||
AICost = i.AICost,
|
||||
HasBetaContent = i.HasBetaContent,
|
||||
};
|
||||
|
||||
// The stored-record shape the read endpoints (v2/mine) + v6 save envelope use.
|
||||
public static object InventionDto(InventionDB.Invention i) => new
|
||||
{
|
||||
InventionId = i.InventionId,
|
||||
ReplicationId = i.ReplicationId,
|
||||
CreatorPlayerId = i.CreatorPlayerId,
|
||||
Name = i.Name,
|
||||
Description = i.Description,
|
||||
ImageName = i.ImageName,
|
||||
CurrentVersionNumber = i.CurrentVersionNumber,
|
||||
CurrentVersion = VersionDto(i),
|
||||
Accessibility = i.Accessibility,
|
||||
IsPublished = i.IsPublished,
|
||||
IsFeatured = i.IsFeatured,
|
||||
ModifiedAt = i.ModifiedAt,
|
||||
CreatedAt = i.CreatedAt,
|
||||
FirstPublishedAt = i.FirstPublishedAt,
|
||||
CreationRoomId = i.CreationRoomId,
|
||||
NumPlayersHaveUsedInRoom = i.NumDownloads,
|
||||
NumDownloads = i.NumDownloads,
|
||||
CheerCount = i.CheerCount,
|
||||
// Permissions: KGCKBAIOBNL - Unassigned 0, LimitedOneUseOnly 10, DisallowKeyLock 15,
|
||||
// UseOnly 20, EditAndSave 40, Publish 60, Charge 80, Unlimited 100. Both were 0
|
||||
// (Unassigned), which is not a permission the maker pen will spawn from. The creator
|
||||
// gets Unlimited over their own invention; everyone else gets what publishing implies.
|
||||
CreatorPermission = 100,
|
||||
GeneralPermission = i.IsPublished ? 20 : 0,
|
||||
IsAGInvention = false,
|
||||
IsCertifiedInvention = false,
|
||||
Price = i.Price,
|
||||
AllowTrial = false,
|
||||
HideFromPlayer = false,
|
||||
ReferencedInventions = i.ReferencedInventions ?? new(),
|
||||
|
||||
// The six keys the client's 28-key Invention DTO carries that this DTO never sent
|
||||
// (wire-shapes: inventions-v2-search). UgcVersion is the important one: the maker pen
|
||||
// palette filters its list by it, so an invention that arrives without one matches
|
||||
// neither CV1 nor CV2 and never appears in the Inventions tab. 0 is not a valid
|
||||
// version - default to 1 (CV1) for records saved before the field existed.
|
||||
UgcVersion = i.UgcVersion > 0 ? i.UgcVersion : 1,
|
||||
LatestVersionNumber = i.CurrentVersionNumber,
|
||||
LongDescription = i.LongDescription ?? "",
|
||||
ForceCannotPublish = false,
|
||||
IsRecRoomApproved = false,
|
||||
DisplayMetadataJson = i.DisplayMetadataJson,
|
||||
};
|
||||
|
||||
private static object SaveResultV6(InventionDB.Invention i) => new
|
||||
{
|
||||
Status = 0,
|
||||
Invention = InventionDto(i),
|
||||
InventionVersion = VersionDto(i),
|
||||
};
|
||||
|
||||
// POST /api/inventions/v6/save — JSON body (camelCase); returns { Status, Invention, InventionVersion }.
|
||||
[HttpPost("/api/inventions/v6/save")]
|
||||
public async Task<ContentResult> SaveV6()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return J(new { error = "Unauthorized" }, 401);
|
||||
var b = await ReadBody();
|
||||
if (b == null) return J(new { error = "Invalid request body" }, 400);
|
||||
var inv = BuildFromBody(account.Id, b);
|
||||
Console.WriteLine($"[invention] {account.Id} saved '{inv.Name}' (#{inv.InventionId}) blob={inv.BlobName}");
|
||||
return J(SaveResultV6(inv));
|
||||
}
|
||||
|
||||
// POST /api/inventions/v9/save — same save, ENVELOPED { Value, Success, Error, error_id }.
|
||||
[HttpPost("/api/inventions/v9/save")]
|
||||
public async Task<ContentResult> SaveV9()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return J(new { Value = (object?)null, Success = false, Error = "Unauthorized", error_id = (string?)null });
|
||||
var b = await ReadBody();
|
||||
if (b == null) return J(new { Value = (object?)null, Success = false, Error = "Invalid request body", error_id = (string?)null });
|
||||
var inv = BuildFromBody(account.Id, b);
|
||||
Console.WriteLine($"[invention] {account.Id} saved (v9) '{inv.Name}' (#{inv.InventionId}) blob={inv.BlobName}");
|
||||
return J(new
|
||||
{
|
||||
Value = new { Status = 0, Invention = InventionDto(inv), InventionVersion = VersionDto(inv), TagsResponse = new { Result = 0, Tags = inv.Tags ?? new List<string>() } },
|
||||
Success = true,
|
||||
Error = "",
|
||||
error_id = (string?)null,
|
||||
});
|
||||
}
|
||||
|
||||
// GET /api/inventions/v1/details?inventionId=X — the invention's tags card. Fetched right
|
||||
// after a save to open the invention; was 404ing. Returns { Tags:[{Tag,Type}] } (404 if none).
|
||||
[HttpGet("/api/inventions/v1/details")]
|
||||
public ContentResult Details()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
if (!int.TryParse(Request.Query["inventionId"], out var id))
|
||||
return J(new { error = "inventionId is required" }, 400);
|
||||
var inv = InventionDB.GetById(id);
|
||||
if (inv == null) return J(new { error = "No such invention" }, 404);
|
||||
var tags = (inv.Tags ?? new List<string>()).Select(t => new { Tag = t, Type = 0 }).ToList();
|
||||
return J(new { Tags = tags });
|
||||
}
|
||||
|
||||
private static object V9(object? value, string? error = null) => new
|
||||
{
|
||||
Value = value, Success = error == null, Error = error ?? "", error_id = (string?)null
|
||||
};
|
||||
|
||||
private static bool Staff(ulong id) => PlayerDB.CheckDevFlag(id) || PlayerDB.CheckModFlag(id);
|
||||
|
||||
// ---- browse / store -----------------------------------------------------------------
|
||||
// Every one of these answered "[]" (search 468 times a day), so the invention store, the
|
||||
// "inventions in this room" list and every batch lookup (room offers, wishlists, nested
|
||||
// inventions) came up empty even with inventions published.
|
||||
|
||||
// GET /api/inventions/v2/search?value=&ugcVersion=&skip=&take= - bare array (wire-shapes:
|
||||
// inventions-v2-search). "#name" is a tag or a creator's @name.
|
||||
[HttpGet("/api/inventions/v2/search")]
|
||||
public ContentResult Search([FromQuery] string? value, [FromQuery] int? ugcVersion, [FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
var hits = InventionStore.Search(value, skip, take);
|
||||
if (ugcVersion.HasValue && ugcVersion.Value > 0)
|
||||
hits = hits.Where(i => (i.UgcVersion > 0 ? i.UgcVersion : 1) == ugcVersion.Value).ToList();
|
||||
return J(hits.Select(InventionDto).ToList());
|
||||
}
|
||||
|
||||
// GET /api/inventions/v2/batch?id=1&id=2 - the ones the caller may see (published, or theirs).
|
||||
[HttpGet("/api/inventions/v2/batch")]
|
||||
public ContentResult Batch([FromQuery] List<long> id)
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
var ids = (id ?? new()).Where(x => x > 0 && x <= int.MaxValue).Select(x => (int)x);
|
||||
var owned = InventionStore.OwnedIds(account.Id);
|
||||
var list = InventionStore.GetByIds(ids)
|
||||
.Where(i => i.IsPublished || i.CreatorPlayerId == account.Id || owned.Contains(i.InventionId) || Staff(account.Id))
|
||||
.Select(InventionDto).ToList();
|
||||
return J(list);
|
||||
}
|
||||
|
||||
// GET /api/inventions/v2/mine - created + bought, newest first.
|
||||
[HttpGet("/api/inventions/v2/mine")]
|
||||
public ContentResult Mine()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
return J(InventionStore.Mine(account.Id).Select(InventionDto).ToList());
|
||||
}
|
||||
|
||||
// GET /api/inventions/v1/room?id=N - published inventions made in that room.
|
||||
[HttpGet("/api/inventions/v1/room")]
|
||||
public ContentResult ByRoom([FromQuery] long id, [FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
return J(InventionStore.ByRoom(id).Skip(Math.Max(0, skip)).Take(take <= 0 ? 100 : take).Select(InventionDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/featured")]
|
||||
[HttpGet("/api/inventions/v1/toptoday")]
|
||||
public ContentResult Featured([FromQuery] int skip = 0, [FromQuery] int take = 50)
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
return J(InventionStore.Top(skip, take).Select(InventionDto).ToList());
|
||||
}
|
||||
|
||||
// GET /api/inventions/v1/personaldetails/{id} - the caller's own relation (cheering?). Was 404.
|
||||
[HttpGet("/api/inventions/v1/personaldetails/{inventionId:int}")]
|
||||
public ContentResult PersonalDetails(int inventionId)
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
ulong me = error == null && account != null ? account.Id : 0;
|
||||
return J(new { IsCheering = me != 0 && InventionStore.IsCheering(me, inventionId), IsOwned = me != 0 && InventionStore.Owns(me, inventionId) });
|
||||
}
|
||||
|
||||
// GET /api/inventions/v1/fulllineageowner?id=1&id=2 - does the caller own every piece? bare bool.
|
||||
[HttpGet("/api/inventions/v1/fulllineageowner")]
|
||||
public ContentResult FullLineageOwner([FromQuery] List<string> id)
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return J(false);
|
||||
var ids = (id ?? new()).SelectMany(x => x.Split(',')).Select(x => int.TryParse(x.Trim(), out var n) ? n : 0).Where(n => n > 0).ToList();
|
||||
return J(ids.Count > 0 && InventionStore.OwnsAll(account.Id, ids));
|
||||
}
|
||||
|
||||
// POST /api/inventions/v2/cheer - {InventionId, Cheer?} (form or JSON). Toggles when no flag.
|
||||
[HttpPost("/api/inventions/v2/cheer")]
|
||||
[HttpPost("/api/inventions/v1/cheer")]
|
||||
public ContentResult CheerInvention()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
var f = Campus_live_rewrite.api.server.RoomModeration.Fields(Request);
|
||||
int id = (int)Campus_live_rewrite.api.server.RoomModeration.Long(f, "InventionId", "inventionId", "id");
|
||||
string flag = Campus_live_rewrite.api.server.RoomModeration.Str(f, "Cheer", "cheer", "IsCheering", "value") ?? "";
|
||||
bool cheer = flag.Length == 0 ? !InventionStore.IsCheering(account.Id, id) : (flag.Equals("true", StringComparison.OrdinalIgnoreCase) || flag == "1");
|
||||
var inv = InventionStore.SetCheer(account.Id, id, cheer);
|
||||
if (inv == null) return J(V9(null, "No such invention"));
|
||||
return J(V9(new { Status = 0, Invention = InventionDto(inv), InventionVersion = VersionDto(inv), IsCheering = cheer, CheerCount = inv.CheerCount }));
|
||||
}
|
||||
|
||||
// ---- creator writes -----------------------------------------------------------------
|
||||
|
||||
// POST /api/inventions/v1/updateprice {InventionId, Price} -> the v6 save result.
|
||||
[HttpPost("/api/inventions/v1/updateprice")]
|
||||
[HttpPost("/api/inventions/v2/updateprice")]
|
||||
public async Task<ContentResult> UpdatePrice()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
var b = await ReadBody() ?? new JObject();
|
||||
int id = b.Value<int?>("InventionId") ?? b.Value<int?>("inventionId") ?? 0;
|
||||
if (id == 0 && int.TryParse(Request.Query["inventionId"], out var qid)) id = qid;
|
||||
int price = b.Value<int?>("Price") ?? b.Value<int?>("price") ?? (int.TryParse(Request.Query["price"], out var qp) ? qp : -1);
|
||||
if (price < 0) return J(new { error = "Price must be >= 0" }, 400);
|
||||
var inv = InventionStore.Edit(id, account.Id, i => { i.Price = price; }, Staff(account.Id));
|
||||
if (inv == null) return J(new { error = "No such invention" }, 404);
|
||||
Console.WriteLine($"[invention] {account.Id} priced #{id} at {price}");
|
||||
return J(SaveResultV6(inv));
|
||||
}
|
||||
|
||||
// PUT /api/inventions/v2/metadata {InventionId, Name?, Description?, LongDescription?, ImageName?, TagsRequest?} -> v9 envelope.
|
||||
[HttpPut("/api/inventions/v2/metadata")]
|
||||
[HttpPost("/api/inventions/v2/metadata")]
|
||||
[HttpPut("/api/inventions/v2/update")] // the editor's "save details" - same body (was a 404)
|
||||
[HttpPost("/api/inventions/v2/update")]
|
||||
public async Task<ContentResult> Metadata()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return J(V9(null, "Unauthorized"), 401);
|
||||
var b = await ReadBody();
|
||||
if (b == null) return J(V9(null, "Invalid request body"));
|
||||
int id = b.Value<int?>("InventionId") ?? b.Value<int?>("inventionId") ?? 0;
|
||||
var inv = InventionStore.Edit(id, account.Id, i =>
|
||||
{
|
||||
string? S(string k) => b[k]?.Type == JTokenType.String ? b.Value<string>(k) : null;
|
||||
var name = (S("Name") ?? S("name"))?.Trim();
|
||||
if (!string.IsNullOrEmpty(name)) i.Name = name.Length > 24 ? name[..24] : name;
|
||||
var desc = S("Description") ?? S("description");
|
||||
if (desc != null) i.Description = desc.Length > 512 ? desc[..512] : desc;
|
||||
var ld = S("LongDescription") ?? S("longDescription");
|
||||
if (ld != null) i.LongDescription = ld;
|
||||
var img = S("ImageName") ?? S("imageName");
|
||||
if (!string.IsNullOrEmpty(img)) i.ImageName = img;
|
||||
var dm = S("DisplayMetadataJson") ?? S("displayMetadataJson");
|
||||
if (dm != null) i.DisplayMetadataJson = dm;
|
||||
var price = b.Value<int?>("Price") ?? b.Value<int?>("price");
|
||||
if (price is >= 0) i.Price = price.Value;
|
||||
if ((b["TagsRequest"] ?? b["tagsRequest"]) is JObject tr)
|
||||
{
|
||||
var custom = tr["CustomTags"]?.ToObject<List<string>>() ?? new();
|
||||
var auto = tr["AutoTags"]?.ToObject<List<string>>() ?? new();
|
||||
i.Tags = custom.Concat(auto).Where(t => !string.IsNullOrWhiteSpace(t)).Distinct().ToList();
|
||||
}
|
||||
}, Staff(account.Id));
|
||||
if (inv == null) return J(V9(null, "No such invention, or not yours"));
|
||||
return J(V9(new { Status = 0, Invention = InventionDto(inv), InventionVersion = VersionDto(inv), TagsResponse = new { Result = 0, Tags = inv.Tags ?? new List<string>() } }));
|
||||
}
|
||||
|
||||
// POST /api/inventions/v1/settags {InventionId, CustomTags[], AutoTags[]} -> {Result, Tags}.
|
||||
[HttpPost("/api/inventions/v1/settags")]
|
||||
public async Task<ContentResult> SetTags()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
var b = await ReadBody() ?? new JObject();
|
||||
int id = b.Value<int?>("InventionId") ?? b.Value<int?>("inventionId") ?? 0;
|
||||
var custom = (b["CustomTags"] ?? b["customTags"])?.ToObject<List<string>>() ?? new();
|
||||
var auto = (b["AutoTags"] ?? b["autoTags"])?.ToObject<List<string>>() ?? new();
|
||||
var inv = InventionStore.Edit(id, account.Id, i => { i.Tags = auto.Concat(custom).Where(t => !string.IsNullOrWhiteSpace(t)).Distinct().ToList(); }, Staff(account.Id));
|
||||
if (inv == null) return J(new { Result = 2, Tags = new List<string>() });
|
||||
return J(new { Result = 0, Tags = inv.Tags ?? new List<string>() });
|
||||
}
|
||||
|
||||
// GET /api/inventions/v1?inventionId= - one invention, bare (reference). Unpublished ones
|
||||
// are served too, the way the reference does.
|
||||
[HttpGet("/api/inventions/v1")]
|
||||
public ContentResult One([FromQuery] int inventionId)
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
var inv = inventionId > 0 ? InventionStore.GetByIds(new[] { inventionId }).FirstOrDefault() : null;
|
||||
if (inv == null) return J(new { error = "No such invention" }, 404);
|
||||
return J(InventionDto(inv));
|
||||
}
|
||||
|
||||
// GET|POST /api/inventions/v1/update - the older edit: the fields ride as QUERY params
|
||||
// (inventionId, name, description, imageName); absent ones keep their value, an empty
|
||||
// description clears it, an empty name / imageName is ignored (reference).
|
||||
[HttpGet("/api/inventions/v1/update")]
|
||||
[HttpPost("/api/inventions/v1/update")]
|
||||
public ContentResult UpdateV1()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
var f = RoomModeration.Fields(Request);
|
||||
int id = (int)RoomModeration.Long(f, "inventionId", "InventionId", "id");
|
||||
var inv = InventionStore.Edit(id, account.Id, i =>
|
||||
{
|
||||
string? name = RoomModeration.Str(f, "name", "Name")?.Trim();
|
||||
if (!string.IsNullOrEmpty(name)) i.Name = name.Length > 24 ? name[..24] : name;
|
||||
if (f.TryGetValue("description", out var d) || f.TryGetValue("Description", out d)) i.Description = (d ?? "").Length > 512 ? d![..512] : (d ?? "");
|
||||
string? img = RoomModeration.Str(f, "imageName", "ImageName");
|
||||
if (!string.IsNullOrEmpty(img)) i.ImageName = img;
|
||||
}, Staff(account.Id));
|
||||
if (inv == null) return J(new { Success = false, Error = "No such invention, or not yours" });
|
||||
return J(InventionDto(inv));
|
||||
}
|
||||
|
||||
// POST /api/inventions/v1/report - filed against the invention's CREATOR (reference).
|
||||
[HttpPost("/api/inventions/v1/report")]
|
||||
public async Task<ContentResult> Report()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
var b = await ReadBody() ?? new JObject();
|
||||
var f = RoomModeration.Fields(Request);
|
||||
int id = b.Value<int?>("InventionId") ?? b.Value<int?>("inventionId") ?? (int)RoomModeration.Long(f, "InventionId", "inventionId");
|
||||
var inv = id > 0 ? InventionStore.GetByIds(new[] { id }).FirstOrDefault() : null;
|
||||
if (inv == null) return J(new { success = false, error = "No such invention" });
|
||||
int category = b.Value<int?>("ReportCategory") ?? (int)RoomModeration.Long(f, "ReportCategory");
|
||||
string details = b.Value<string>("Details") ?? b.Value<string>("ReportDescription") ?? RoomModeration.Str(f, "Details", "ReportDescription") ?? "";
|
||||
Campus_live_rewrite.api.dbs.ReportsDB.Create(new Campus_live_rewrite.api.dbs.ReportsDB.Report
|
||||
{
|
||||
ReporterPlayerId = account.Id,
|
||||
ReportedPlayerId = inv.CreatorPlayerId,
|
||||
ReportCategory = category,
|
||||
Details = $"[invention {inv.InventionId} '{inv.Name}'] {details}".Trim(),
|
||||
});
|
||||
Console.WriteLine($"[report] {account.Id} reported invention {inv.InventionId} by {inv.CreatorPlayerId}");
|
||||
return J(new { success = true, error = "" });
|
||||
}
|
||||
|
||||
// POST /api/inventions/v2/unpublish | v1/unpublish {InventionId} -> v9 envelope.
|
||||
[HttpPost("/api/inventions/v2/unpublish")]
|
||||
[HttpPost("/api/inventions/v1/unpublish")]
|
||||
public async Task<ContentResult> Unpublish()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return J(V9(null, "Unauthorized"), 401);
|
||||
var b = await ReadBody() ?? new JObject();
|
||||
int id = b.Value<int?>("InventionId") ?? b.Value<int?>("inventionId") ?? 0;
|
||||
if (id == 0 && int.TryParse(Request.Query["inventionId"], out var qid)) id = qid;
|
||||
var inv = InventionStore.Edit(id, account.Id, i => { i.IsPublished = false; i.Accessibility = 0; }, Staff(account.Id));
|
||||
if (inv == null) return J(V9(null, "No such invention"));
|
||||
Console.WriteLine($"[invention] {account.Id} unpublished #{id}");
|
||||
return J(V9(SaveResultV6(inv)));
|
||||
}
|
||||
|
||||
// POST /api/inventions/v2/accessibility {InventionId, Accessibility} (0 private, 1 public, 2 unlisted).
|
||||
[HttpPost("/api/inventions/v2/accessibility")]
|
||||
[HttpPut("/api/inventions/v2/accessibility")]
|
||||
public async Task<ContentResult> SetAccessibility()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return J(V9(null, "Unauthorized"), 401);
|
||||
var b = await ReadBody() ?? new JObject();
|
||||
int id = b.Value<int?>("InventionId") ?? b.Value<int?>("inventionId") ?? 0;
|
||||
int acc = b.Value<int?>("Accessibility") ?? b.Value<int?>("accessibility") ?? 0;
|
||||
var inv = InventionStore.Edit(id, account.Id, i =>
|
||||
{
|
||||
i.Accessibility = acc;
|
||||
i.IsPublished = acc != 0;
|
||||
if (i.IsPublished && i.FirstPublishedAt == null) i.FirstPublishedAt = DateTime.UtcNow;
|
||||
}, Staff(account.Id));
|
||||
if (inv == null) return J(V9(null, "No such invention"));
|
||||
return J(V9(SaveResultV6(inv)));
|
||||
}
|
||||
|
||||
// POST /api/inventions/v2/delete {InventionId} -> {Value:null, Success, Error, error_id}.
|
||||
[HttpPost("/api/inventions/v2/delete")]
|
||||
[HttpDelete("/api/inventions/v2/delete")]
|
||||
public async Task<ContentResult> DeleteInvention()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return J(V9(null, "Unauthorized"), 401);
|
||||
var b = await ReadBody() ?? new JObject();
|
||||
int id = b.Value<int?>("InventionId") ?? b.Value<int?>("inventionId") ?? 0;
|
||||
if (id == 0 && int.TryParse(Request.Query["inventionId"], out var qid)) id = qid;
|
||||
bool ok = InventionStore.Delete(id, account.Id, Staff(account.Id));
|
||||
Console.WriteLine($"[invention] {account.Id} delete #{id}: {(ok ? "deleted" : "refused")}");
|
||||
return J(V9(null, ok ? null : "No such invention, or not yours"));
|
||||
}
|
||||
|
||||
// POST /api/inventions/v4/publish - the same publish, v9 envelope.
|
||||
[HttpPost("/api/inventions/v4/publish")]
|
||||
public Task<ContentResult> PublishV4() => PublishV3();
|
||||
|
||||
// ---- buying ---------------------------------------------------------------------------
|
||||
// POST /api/storefronts/v3/buyInvention {InventionId, RequestedPrice} (2025) and
|
||||
// GET /api/storefronts/v2/buyInvention?inventionId=&requestedPrice= (2023). Charges Rec
|
||||
// Center Tokens at the STORED price, records ownership (it lands in v2/mine and counts as
|
||||
// a download) and answers the bare {InventionResponse, BalanceUpdateResponse} pair
|
||||
// (wire-shapes: storefronts-v3-buyinvention). Neither route existed - the 2025 one 404'd.
|
||||
[HttpPost("/api/storefronts/v3/buyInvention")]
|
||||
[HttpGet("/api/storefronts/v2/buyInvention")]
|
||||
[HttpPost("/api/storefronts/v2/buyInvention")]
|
||||
public async Task<ContentResult> BuyInvention()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return error;
|
||||
var b = await ReadBody() ?? new JObject();
|
||||
int id = b.Value<int?>("InventionId") ?? b.Value<int?>("inventionId") ?? 0;
|
||||
if (id == 0 && int.TryParse(Request.Query["inventionId"], out var qid)) id = qid;
|
||||
int requested = b.Value<int?>("RequestedPrice") ?? b.Value<int?>("requestedPrice") ?? (int.TryParse(Request.Query["requestedPrice"], out var qp) ? qp : 0);
|
||||
const int Tokens = (int)Campus_live_rewrite.api.server.Classes.CurrencyType.RecCenterTokens;
|
||||
long balance = PlayerDB.GetBalance(account.Id, Tokens);
|
||||
|
||||
ContentResult Refuse(string why, int status = 200)
|
||||
{
|
||||
Console.WriteLine($"[invention-buy] {account.Id} #{id}: {why}");
|
||||
return J(new { InventionResponse = V9(null, why), BalanceUpdateResponse = (object?)null }, status);
|
||||
}
|
||||
|
||||
var inv = InventionDB.GetById(id);
|
||||
if (inv == null) return Refuse("No such invention");
|
||||
if (!inv.IsPublished && inv.CreatorPlayerId != account.Id) return Refuse("This invention is not for sale");
|
||||
if (inv.CreatorPlayerId == account.Id) return Refuse("You already own your own invention");
|
||||
if (InventionStore.Owns(account.Id, id)) return Refuse("Already owned");
|
||||
int price = Math.Max(0, inv.Price);
|
||||
if (requested != price)
|
||||
Console.WriteLine($"[invention-buy] {account.Id} #{id}: client rendered {requested}, charging the stored {price}");
|
||||
if (price > 0 && balance < price) return Refuse("Not enough tokens");
|
||||
if (price > 0) balance = PlayerDB.AddBalance(account.Id, Tokens, -price);
|
||||
InventionStore.AddOwned(account.Id, id, price);
|
||||
EarningsDB.Record(inv.CreatorPlayerId, account.Id, "invention", id.ToString(), inv.Name, Tokens, price);
|
||||
inv = InventionDB.GetById(id) ?? inv;
|
||||
Campus_live_rewrite.api.server.GiftBoxes.PushBalance(account.Id, Tokens, balance, "invention purchase");
|
||||
Console.WriteLine($"[invention-buy] @{account.Username} bought #{id} '{inv.Name}' for {price} -> balance {balance}");
|
||||
return J(new
|
||||
{
|
||||
InventionResponse = V9(new { Status = 0, Invention = InventionDto(inv), InventionVersion = VersionDto(inv), TagsResponse = new { Result = 0, Tags = inv.Tags ?? new List<string>() } }),
|
||||
BalanceUpdateResponse = new
|
||||
{
|
||||
BalanceUpdates = new[] { new { UpdateResponse = 0, Data = InventionDto(inv) } },
|
||||
Balance = balance,
|
||||
CurrencyType = Tokens,
|
||||
Platform = Campus_live_rewrite.api.server.Classes.GameCurrency.AllPlatformsBucket
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// POST /api/inventions/v3/publish — flip the invention public. Body/query carries the id.
|
||||
[HttpPost("/api/inventions/v3/publish")]
|
||||
public async Task<ContentResult> PublishV3()
|
||||
{
|
||||
var (account, error) = AuthHelper.ValidateToken(HttpContext);
|
||||
if (error != null) return J(new { Value = (object?)null, Success = false, Error = "Unauthorized", error_id = (string?)null });
|
||||
var b = await ReadBody() ?? new JObject();
|
||||
int id = b.Value<int?>("inventionId") ?? b.Value<int?>("InventionId") ?? 0;
|
||||
if (id == 0 && int.TryParse(Request.Query["inventionId"], out var qid)) id = qid;
|
||||
var inv = InventionDB.Publish(id, account.Id, true);
|
||||
if (inv == null) return J(new { Value = (object?)null, Success = false, Error = "No such invention", error_id = (string?)null });
|
||||
Console.WriteLine($"[invention] {account.Id} published #{id}");
|
||||
return J(new { Value = SaveResultV6(inv), Success = true, Error = "", error_id = (string?)null });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user