mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Refactor ImagesController: enhance image metadata parsing and update public visibility logic for uploaded images
This commit is contained in:
@@ -29,10 +29,10 @@ namespace DorkNet.Server.Controllers.API.Images;
|
|||||||
/// Returning [] (the previous catch-all) made the watch silently
|
/// Returning [] (the previous catch-all) made the watch silently
|
||||||
/// drop the photo.
|
/// drop the photo.
|
||||||
///
|
///
|
||||||
/// Saved photos are stored as <see cref="PhotoEntity"/> rows with
|
/// Saved photos are stored as <see cref="PhotoEntity"/> rows. The 2020
|
||||||
/// IsPublic=false by default. A separate Share button (POST
|
/// client sends camera share state inside an imgMeta JSON form field
|
||||||
/// api/images/v4/share/{id}) flips IsPublic=true and surfaces them on
|
/// (accessibility=1 for public ShareCamera uploads), while newer callers
|
||||||
/// feed.rec.net.
|
/// can still send top-level IsPublic.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class ImagesController(
|
public class ImagesController(
|
||||||
@@ -82,12 +82,17 @@ public class ImagesController(
|
|||||||
// Pull optional metadata from the form (the 2020 watch sometimes
|
// Pull optional metadata from the form (the 2020 watch sometimes
|
||||||
// includes a caption / room context, sometimes posts bytes only).
|
// includes a caption / room context, sometimes posts bytes only).
|
||||||
var form = Request.HasFormContentType ? await Request.ReadFormAsync() : null;
|
var form = Request.HasFormContentType ? await Request.ReadFormAsync() : null;
|
||||||
|
var imageMeta = ParseImageMeta(form);
|
||||||
var caption = form?["Caption"].ToString() ?? form?["Description"].ToString() ?? string.Empty;
|
var caption = form?["Caption"].ToString() ?? form?["Description"].ToString() ?? string.Empty;
|
||||||
var taggedIds = form?["TaggedPlayerIds"].ToString() ?? string.Empty;
|
var taggedIds = form?["TaggedPlayerIds"].ToString() ?? string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(taggedIds) && imageMeta?.PlayerIds.Count > 0)
|
||||||
|
taggedIds = string.Join(",", imageMeta.PlayerIds);
|
||||||
long roomId = 0;
|
long roomId = 0;
|
||||||
if (form is not null && long.TryParse(form["RoomId"].ToString(), out var rid)) roomId = rid;
|
if (form is not null && long.TryParse(form["RoomId"].ToString(), out var rid)) roomId = rid;
|
||||||
|
if (roomId == 0 && imageMeta?.RoomId is > 0) roomId = imageMeta.RoomId.Value;
|
||||||
if (roomId == 0) roomId = presence.GetRoom(pid)?.RoomId ?? 0;
|
if (roomId == 0) roomId = presence.GetRoom(pid)?.RoomId ?? 0;
|
||||||
var isPublic = string.Equals(form?["IsPublic"].ToString(), "true", StringComparison.OrdinalIgnoreCase);
|
var explicitPublic = form is null ? null : TryReadBool(form["IsPublic"].ToString());
|
||||||
|
var isPublic = explicitPublic ?? (imageMeta?.Accessibility == 1 || imageMeta?.SavedImageType == 1);
|
||||||
var isProfileThumbnail = IsProfileThumbnail(form);
|
var isProfileThumbnail = IsProfileThumbnail(form);
|
||||||
LogUploadForm("uploadsaved", pid, form, isProfileThumbnail);
|
LogUploadForm("uploadsaved", pid, form, isProfileThumbnail);
|
||||||
|
|
||||||
@@ -280,13 +285,23 @@ public class ImagesController(
|
|||||||
/// <summary>GET <c>api/images/v1/slideshow</c> — the watch's lobby
|
/// <summary>GET <c>api/images/v1/slideshow</c> — the watch's lobby
|
||||||
/// slideshow info. Response shape per <c>SlideshowInfoDTO.Deserialize</c>:
|
/// slideshow info. Response shape per <c>SlideshowInfoDTO.Deserialize</c>:
|
||||||
/// <c>{ValidTill (DateTime, required), Images (list, required)}</c>.
|
/// <c>{ValidTill (DateTime, required), Images (list, required)}</c>.
|
||||||
/// Far-future <c>ValidTill</c> stops the watch from re-polling.</summary>
|
/// A short <c>ValidTill</c> lets the Rec Center board pick up newly
|
||||||
|
/// shared camera photos without needing a server restart.</summary>
|
||||||
[HttpGet("api/images/v1/slideshow")]
|
[HttpGet("api/images/v1/slideshow")]
|
||||||
public IActionResult SlideshowInfo() => Ok(new
|
public async Task<IActionResult> SlideshowInfo()
|
||||||
{
|
{
|
||||||
ValidTill = new DateTime(9999, 12, 31, 0, 0, 0, DateTimeKind.Utc),
|
var rows = await db.Photos
|
||||||
Images = Array.Empty<object>(),
|
.Where(p => p.IsPublic && p.DeletedAt == null)
|
||||||
|
.OrderByDescending(p => p.CreatedAt)
|
||||||
|
.Take(50)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
ValidTill = DateTime.UtcNow.AddMinutes(5),
|
||||||
|
Images = rows.Select(p => BuildImageInfo(p, p.UploaderPlayerId)),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Binary image fallback for clients that request image bytes
|
/// <summary>Binary image fallback for clients that request image bytes
|
||||||
/// through api/images/{name} instead of img.rec.net/{name}. Specific JSON
|
/// through api/images/{name} instead of img.rec.net/{name}. Specific JSON
|
||||||
@@ -793,6 +808,104 @@ public class ImagesController(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class ParsedImageMeta
|
||||||
|
{
|
||||||
|
public int? SavedImageType { get; init; }
|
||||||
|
public int? Accessibility { get; init; }
|
||||||
|
public long? RoomId { get; init; }
|
||||||
|
public IReadOnlyList<long> PlayerIds { get; init; } = Array.Empty<long>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ParsedImageMeta? ParseImageMeta(IFormCollection? form)
|
||||||
|
{
|
||||||
|
if (form is null) return null;
|
||||||
|
|
||||||
|
var meta = form["imgMeta"].ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(meta))
|
||||||
|
meta = form["imageMeta"].ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(meta))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(meta);
|
||||||
|
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new ParsedImageMeta
|
||||||
|
{
|
||||||
|
SavedImageType = TryGetInt(doc.RootElement, "savedImageType", "SavedImageType", "imageType", "ImageType"),
|
||||||
|
Accessibility = TryGetInt(doc.RootElement, "accessibility", "Accessibility"),
|
||||||
|
RoomId = TryGetLong(doc.RootElement, "roomId", "RoomId"),
|
||||||
|
PlayerIds = TryGetLongArray(doc.RootElement, "playerIds", "PlayerIds"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool? TryReadBool(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||||
|
if (bool.TryParse(value, out var parsed)) return parsed;
|
||||||
|
if (value == "1") return true;
|
||||||
|
if (value == "0") return false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? TryGetInt(JsonElement root, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty(name, out var value))
|
||||||
|
continue;
|
||||||
|
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var n))
|
||||||
|
return n;
|
||||||
|
if (value.ValueKind == JsonValueKind.String && int.TryParse(value.GetString(), out n))
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long? TryGetLong(JsonElement root, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty(name, out var value))
|
||||||
|
continue;
|
||||||
|
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var n))
|
||||||
|
return n;
|
||||||
|
if (value.ValueKind == JsonValueKind.String && long.TryParse(value.GetString(), out n))
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<long> TryGetLongArray(JsonElement root, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return value.EnumerateArray()
|
||||||
|
.Select(x => x.ValueKind == JsonValueKind.Number && x.TryGetInt64(out var n)
|
||||||
|
? n
|
||||||
|
: x.ValueKind == JsonValueKind.String && long.TryParse(x.GetString(), out n)
|
||||||
|
? n
|
||||||
|
: 0L)
|
||||||
|
.Where(x => x > 0)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.Empty<long>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Build the response shape every images/v* endpoint
|
/// <summary>Build the response shape every images/v* endpoint
|
||||||
/// returns. PascalCase keys + redundant aliases so whichever JSON
|
/// returns. PascalCase keys + redundant aliases so whichever JSON
|
||||||
/// parser the 2020 watch uses, it finds the field it wants. The
|
/// parser the 2020 watch uses, it finds the field it wants. The
|
||||||
|
|||||||
Reference in New Issue
Block a user