namespace Supercell.Laser.Server.SCID { using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http; using Supercell.Laser.Server.Database; using Supercell.Laser.Server.Database.Models; internal static partial class SCIDApi { private static IResult HandleSocial(HttpRequest request, string path, Dictionary body) { switch (path) { case "social/v1/profile.get": return ProfileGet(request, body); case "social/v1/profile.info": return ProfileInfo(request, body); case "social/v1/profile.set": return ProfileSet(request, body); case "social/v1/profile.validate": return Ok(); case "social/v1/profile.report": case "social/v1/ingame.invitetoplay": return Ok(); case "social/v1/profiles.list": return ProfilesList(request, body); case "social/v1/friends.remove": return FriendRemove(request, body); case "social/v1/friends.block": return FriendBlock(request, body); case "social/v1/friends.unblock": return FriendUnblock(request, body); case "social/v1/friends.rejectRequest": return FriendReject(request, body); case "social/v1/friends.acceptRequest": return FriendAccept(request, body); case "social/v2/friends.createRequest": return FriendCreateRequest(request, body); case "social/v2/friends.cancelRequest": return FriendCancelRequest(request, body); case "social/v2/friends.list": return FriendsList(request, body); default: return Ok(); } } private static IResult ProfileGet(HttpRequest request, Dictionary body) { string scid = Get(body, "scid") ?? Get(body, "profileId") ?? Get(body, "profile_id"); SCIDAccount account = string.IsNullOrEmpty(scid) ? SCIDAuthentication.FromRequest(request, body) : SCIDBase.LoadByScid(scid); if (account != null) return Ok(ProfileData(account)); return Ok(ProfileData(scid ?? "mock_scid", "veist", null)); } private static IResult ProfileInfo(HttpRequest request, Dictionary body) { SCIDAccount account = SCIDAuthentication.FromRequest(request, body); return account == null ? Fail("invalid_token") : Ok(ProfileData(account)); } private static IResult ProfileSet(HttpRequest request, Dictionary body) { SCIDAccount account = SCIDAuthentication.FromRequest(request, body); if (account == null) return Fail("invalid_token"); if (body.TryGetValue("avatarImage", out string avatarImage)) account.AvatarImage = avatarImage; if (body.TryGetValue("blockIncomingFriendRequests", out string blockIncomingFriendRequests) && TryParseBoolean(blockIncomingFriendRequests, out bool blockIncoming)) { account.BlockIncomingFriendRequests = blockIncoming; } if (account.GameAccountId > 0) { Account gameAccount = Accounts.Load(account.GameAccountId); account.ProfileName = gameAccount?.Avatar?.Name; } SCIDBase.Save(account); return Ok(ProfileData(account)); } private static readonly Regex ColorTagRegex = new Regex(@"|", RegexOptions.Singleline); private static object ProfileData(SCIDAccount account) { string name = account.GameAccountId > 0 ? Accounts.Load(account.GameAccountId)?.Avatar?.Name : account.ProfileName; return ProfileData(account.Scid, ColorTagRegex.Replace(name ?? "Player", ""), account.AvatarImage, account.BlockIncomingFriendRequests, account.GameAccountId > 0); } private static object ProfileData(string scid, string name, string avatarImage, bool blockIncomingFriendRequests = false, bool connected = true) { return new { scid, name = new { name, changeAllowed = 9999999999999L }, avatarImage = new { image = avatarImage, changeAllowed = 9999999999999L }, imageURL = new { URL = (string)null, changeAllowed = 9999999999999L }, qrCodeURL = (string)null, universalLink = "https://supercell.com/", forcedOfflineStatus = false, blockIncomingFriendRequests, availableSystems = new[] { new { system = "laser", environment = "prod" } }, connectedSystems = connected ? new[] { new { system = "laser", environment = "prod" } } : Array.Empty(), connectedSystem = connected ? new { system = "laser", username = name, progress = new[] { new { system = "laser", current = 1, max = 1 } }, deeplink = "" } : (object)null, supportTier = 1 }; } private static IResult ProfilesList(HttpRequest request, Dictionary body) { // The request may contain a comma-separated `scids` value or a // JSON array encoded as the `profiles` field. Unknown profiles // are omitted, matching the behaviour of the real endpoint. string raw = Get(body, "scids") ?? Get(body, "profileIds") ?? Get(body, "profiles"); List profiles = new(); if (!string.IsNullOrWhiteSpace(raw)) { IEnumerable ids; try { ids = raw.TrimStart().StartsWith("[") ? System.Text.Json.JsonSerializer.Deserialize(raw) ?? Array.Empty() : raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } catch (System.Text.Json.JsonException) { ids = Array.Empty(); } foreach (string id in ids.Distinct(StringComparer.Ordinal)) { SCIDAccount account = SCIDBase.LoadByScid(id); if (account != null) profiles.Add(ProfileData(account)); } } return Ok(new { profiles = profiles.ToArray() }); } private static IResult FriendsList(HttpRequest request, Dictionary body) { SCIDAccount account = SCIDAuthentication.FromRequest(request, body); if (account == null) return Fail("invalid_token"); EnsureCollections(account); return Ok(new { friends = ProfilesFor(account.Friends), incomingRequests = ProfilesFor(account.IncomingRequests), outgoingRequests = ProfilesFor(account.OutgoingRequests), blocked = ProfilesFor(account.Blocked) }); } private static IResult FriendCreateRequest(HttpRequest request, Dictionary body) { SCIDAccount owner = SCIDAuthentication.FromRequest(request, body); SCIDAccount target = LoadTarget(body); if (owner == null) return Fail("invalid_token"); if (target == null || target.Scid == owner.Scid) return Fail("profile_not_found"); EnsureCollections(owner); EnsureCollections(target); if (owner.Blocked.Contains(target.Scid, StringComparer.Ordinal) || target.Blocked.Contains(owner.Scid, StringComparer.Ordinal)) return Fail("blocked"); if (target.BlockIncomingFriendRequests) return Fail("friend_requests_blocked"); if (owner.Friends.Contains(target.Scid, StringComparer.Ordinal)) return Ok(); if (!target.IncomingRequests.Contains(owner.Scid, StringComparer.Ordinal)) target.IncomingRequests.Add(owner.Scid); if (!owner.OutgoingRequests.Contains(target.Scid, StringComparer.Ordinal)) owner.OutgoingRequests.Add(target.Scid); SCIDBase.Save(owner); SCIDBase.Save(target); return Ok(); } private static IResult FriendCancelRequest(HttpRequest request, Dictionary body) { SCIDAccount owner = SCIDAuthentication.FromRequest(request, body); SCIDAccount target = LoadTarget(body); if (owner == null) return Fail("invalid_token"); if (target == null) return Fail("profile_not_found"); EnsureCollections(owner); EnsureCollections(target); owner.OutgoingRequests.RemoveAll(x => string.Equals(x, target.Scid, StringComparison.Ordinal)); target.IncomingRequests.RemoveAll(x => string.Equals(x, owner.Scid, StringComparison.Ordinal)); SCIDBase.Save(owner); SCIDBase.Save(target); return Ok(); } private static IResult FriendAccept(HttpRequest request, Dictionary body) { SCIDAccount owner = SCIDAuthentication.FromRequest(request, body); SCIDAccount target = LoadTarget(body); if (owner == null) return Fail("invalid_token"); if (target == null) return Fail("profile_not_found"); EnsureCollections(owner); EnsureCollections(target); if (!owner.IncomingRequests.Contains(target.Scid, StringComparer.Ordinal)) return Fail("request_not_found"); owner.IncomingRequests.RemoveAll(x => string.Equals(x, target.Scid, StringComparison.Ordinal)); target.OutgoingRequests.RemoveAll(x => string.Equals(x, owner.Scid, StringComparison.Ordinal)); if (!owner.Friends.Contains(target.Scid, StringComparer.Ordinal)) owner.Friends.Add(target.Scid); if (!target.Friends.Contains(owner.Scid, StringComparer.Ordinal)) target.Friends.Add(owner.Scid); SCIDBase.Save(owner); SCIDBase.Save(target); return Ok(); } private static IResult FriendReject(HttpRequest request, Dictionary body) { SCIDAccount owner = SCIDAuthentication.FromRequest(request, body); SCIDAccount target = LoadTarget(body); if (owner == null) return Fail("invalid_token"); if (target == null) return Fail("profile_not_found"); EnsureCollections(owner); EnsureCollections(target); owner.IncomingRequests.RemoveAll(x => string.Equals(x, target.Scid, StringComparison.Ordinal)); target.OutgoingRequests.RemoveAll(x => string.Equals(x, owner.Scid, StringComparison.Ordinal)); SCIDBase.Save(owner); SCIDBase.Save(target); return Ok(); } private static IResult FriendRemove(HttpRequest request, Dictionary body) { SCIDAccount owner = SCIDAuthentication.FromRequest(request, body); SCIDAccount target = LoadTarget(body); if (owner == null) return Fail("invalid_token"); if (target == null) return Fail("profile_not_found"); EnsureCollections(owner); EnsureCollections(target); owner.Friends.RemoveAll(x => string.Equals(x, target.Scid, StringComparison.Ordinal)); target.Friends.RemoveAll(x => string.Equals(x, owner.Scid, StringComparison.Ordinal)); SCIDBase.Save(owner); SCIDBase.Save(target); return Ok(); } private static IResult FriendBlock(HttpRequest request, Dictionary body) { SCIDAccount owner = SCIDAuthentication.FromRequest(request, body); SCIDAccount target = LoadTarget(body); if (owner == null) return Fail("invalid_token"); if (target == null) return Fail("profile_not_found"); EnsureCollections(owner); EnsureCollections(target); owner.Friends.RemoveAll(x => string.Equals(x, target.Scid, StringComparison.Ordinal)); target.Friends.RemoveAll(x => string.Equals(x, owner.Scid, StringComparison.Ordinal)); owner.IncomingRequests.RemoveAll(x => string.Equals(x, target.Scid, StringComparison.Ordinal)); owner.OutgoingRequests.RemoveAll(x => string.Equals(x, target.Scid, StringComparison.Ordinal)); target.IncomingRequests.RemoveAll(x => string.Equals(x, owner.Scid, StringComparison.Ordinal)); target.OutgoingRequests.RemoveAll(x => string.Equals(x, owner.Scid, StringComparison.Ordinal)); if (!owner.Blocked.Contains(target.Scid, StringComparer.Ordinal)) owner.Blocked.Add(target.Scid); SCIDBase.Save(owner); SCIDBase.Save(target); return Ok(); } private static IResult FriendUnblock(HttpRequest request, Dictionary body) { SCIDAccount owner = SCIDAuthentication.FromRequest(request, body); SCIDAccount target = LoadTarget(body); if (owner == null) return Fail("invalid_token"); if (target == null) return Fail("profile_not_found"); EnsureCollections(owner); owner.Blocked.RemoveAll(x => string.Equals(x, target.Scid, StringComparison.Ordinal)); SCIDBase.Save(owner); return Ok(); } private static SCIDAccount LoadTarget(Dictionary body) { string scid = Get(body, "scid") ?? Get(body, "profileId") ?? Get(body, "friendScid") ?? Get(body, "targetScid"); return string.IsNullOrWhiteSpace(scid) ? null : SCIDBase.LoadByScid(scid.Trim()); } private static object[] ProfilesFor(IEnumerable scids) { return scids?.Select(SCIDBase.LoadByScid).Where(x => x != null).Select(account => ProfileData(account)).ToArray() ?? Array.Empty(); } private static void EnsureCollections(SCIDAccount account) { account.Friends ??= new(); account.IncomingRequests ??= new(); account.OutgoingRequests ??= new(); account.Blocked ??= new(); } private static bool TryParseBoolean(string value, out bool result) { if (bool.TryParse(value, out result)) return true; if (value == "1" || string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase)) { result = true; return true; } if (value == "0" || string.Equals(value, "no", StringComparison.OrdinalIgnoreCase)) { result = false; return true; } result = false; return false; } } }