Server-backed friends, chat, IAB scaffold + EF migrations/Postgres
- Social: EF-backed friends graph + chat (SocialService/SocialModels); REST endpoints (friends add/accept/decline/remove/list/requests, chat conversations/messages/send) with real-time hub events (friendRequest/social/chat). GameManager tracks online users for presence. - Client SignalrService: friends + chat now hit the server and react to hub events (refetch + emit); no longer delegated to the mock. - IAB: /api/coins/iab/verify endpoint + IabVerifyReq for Cafe Bazaar/Myket (token verification is a documented TODO pending store accounts/SKUs). - Persistence: EF Core Design package + DesignTimeDbContextFactory (Postgres), Program auto-migrate/EnsureCreated, appsettings.Production.json.example with Supabase connection + live ZarinPal template. Verified end-to-end (two users, SQLite dev): request -> accept -> bidirectional friends, chat send with per-user fromMe, unread count + read-on-fetch. Server + client builds clean (dotnet build, tsc, next build). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System.Text.Json;
|
||||
using Hokm.Server.Data;
|
||||
using Hokm.Server.Game;
|
||||
using Hokm.Server.Hubs;
|
||||
using Hokm.Server.Profiles;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Hokm.Server.Social;
|
||||
|
||||
public class SocialService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly GameManager _mgr;
|
||||
private readonly IHubContext<GameHub> _hub;
|
||||
|
||||
public SocialService(AppDbContext db, GameManager mgr, IHubContext<GameHub> hub)
|
||||
{
|
||||
_db = db;
|
||||
_mgr = mgr;
|
||||
_hub = hub;
|
||||
}
|
||||
|
||||
private async Task<FriendDto> FriendDtoFor(string userId)
|
||||
{
|
||||
var row = await _db.Profiles.FindAsync(userId);
|
||||
var p = row != null ? JsonSerializer.Deserialize<ProfileDto>(row.Json, JsonOpts.Default) : null;
|
||||
return new FriendDto
|
||||
{
|
||||
Id = userId,
|
||||
Username = p?.Username ?? userId,
|
||||
DisplayName = p?.DisplayName ?? userId,
|
||||
Avatar = p?.Avatar ?? "a-fox",
|
||||
Level = p?.Level ?? 1,
|
||||
Rating = p?.Rating ?? 1000,
|
||||
Status = _mgr.IsOnline(userId) ? "online" : "offline",
|
||||
};
|
||||
}
|
||||
|
||||
/* ----------------------------- friends ----------------------------- */
|
||||
|
||||
public async Task<List<FriendDto>> ListFriends(string uid)
|
||||
{
|
||||
var ids = await _db.Friends.Where(f => f.UserId == uid).Select(f => f.FriendId).ToListAsync();
|
||||
var list = new List<FriendDto>();
|
||||
foreach (var id in ids) list.Add(await FriendDtoFor(id));
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<List<FriendRequestDto>> ListRequests(string uid)
|
||||
{
|
||||
var reqs = await _db.FriendRequests.Where(r => r.ToUserId == uid).ToListAsync();
|
||||
var list = new List<FriendRequestDto>();
|
||||
foreach (var r in reqs)
|
||||
list.Add(new FriendRequestDto { Id = r.Id.ToString(), From = await FriendDtoFor(r.FromUserId), CreatedAt = new DateTimeOffset(r.CreatedAt).ToUnixTimeMilliseconds() });
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<(bool ok, string messageFa, string messageEn)> AddFriend(string uid, string query)
|
||||
{
|
||||
var digits = new string(query.Where(char.IsDigit).ToArray());
|
||||
var targetId = query.Contains(':') ? query.Trim() : (digits.Length >= 4 ? "phone:" + digits : query.Trim());
|
||||
|
||||
var target = await _db.Profiles.FindAsync(targetId);
|
||||
if (target == null || targetId == uid)
|
||||
return (false, "کاربر پیدا نشد", "User not found");
|
||||
if (await _db.Friends.AnyAsync(f => f.UserId == uid && f.FriendId == targetId))
|
||||
return (false, "از قبل دوست هستید", "Already friends");
|
||||
if (!await _db.FriendRequests.AnyAsync(r => r.FromUserId == uid && r.ToUserId == targetId))
|
||||
{
|
||||
_db.FriendRequests.Add(new FriendRequestRow { FromUserId = uid, ToUserId = targetId, CreatedAt = DateTime.UtcNow });
|
||||
await _db.SaveChangesAsync();
|
||||
await _hub.Clients.User(targetId).SendAsync("friendRequest", await FriendDtoFor(uid));
|
||||
}
|
||||
return (true, "درخواست دوستی ارسال شد", "Friend request sent");
|
||||
}
|
||||
|
||||
public async Task Accept(string uid, long requestId)
|
||||
{
|
||||
var req = await _db.FriendRequests.FirstOrDefaultAsync(r => r.Id == requestId && r.ToUserId == uid);
|
||||
if (req == null) return;
|
||||
_db.Friends.Add(new FriendEdgeRow { UserId = uid, FriendId = req.FromUserId });
|
||||
_db.Friends.Add(new FriendEdgeRow { UserId = req.FromUserId, FriendId = uid });
|
||||
_db.FriendRequests.Remove(req);
|
||||
await _db.SaveChangesAsync();
|
||||
await _hub.Clients.User(req.FromUserId).SendAsync("social", "friend-added");
|
||||
}
|
||||
|
||||
public async Task Decline(string uid, long requestId)
|
||||
{
|
||||
var req = await _db.FriendRequests.FirstOrDefaultAsync(r => r.Id == requestId && r.ToUserId == uid);
|
||||
if (req != null) { _db.FriendRequests.Remove(req); await _db.SaveChangesAsync(); }
|
||||
}
|
||||
|
||||
public async Task Remove(string uid, string friendId)
|
||||
{
|
||||
var edges = await _db.Friends.Where(f =>
|
||||
(f.UserId == uid && f.FriendId == friendId) || (f.UserId == friendId && f.FriendId == uid)).ToListAsync();
|
||||
_db.Friends.RemoveRange(edges);
|
||||
await _db.SaveChangesAsync();
|
||||
await _hub.Clients.User(friendId).SendAsync("social", "friend-removed");
|
||||
}
|
||||
|
||||
/* ------------------------------- chat ------------------------------ */
|
||||
|
||||
public async Task<List<ConversationDto>> Conversations(string uid)
|
||||
{
|
||||
var msgs = await _db.Messages.Where(m => m.UserId == uid || m.PeerId == uid).ToListAsync();
|
||||
var byPartner = msgs.GroupBy(m => m.UserId == uid ? m.PeerId : m.UserId);
|
||||
var convs = new List<ConversationDto>();
|
||||
foreach (var g in byPartner)
|
||||
{
|
||||
var last = g.OrderByDescending(m => m.CreatedAt).First();
|
||||
convs.Add(new ConversationDto
|
||||
{
|
||||
Friend = await FriendDtoFor(g.Key),
|
||||
LastMessage = ToDto(last, uid),
|
||||
Unread = g.Count(m => m.PeerId == uid && !m.ReadByPeer),
|
||||
});
|
||||
}
|
||||
return convs.OrderByDescending(c => c.LastMessage?.Ts ?? 0).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<ChatMessageDto>> Messages(string uid, string peerId)
|
||||
{
|
||||
var msgs = await _db.Messages
|
||||
.Where(m => (m.UserId == uid && m.PeerId == peerId) || (m.UserId == peerId && m.PeerId == uid))
|
||||
.OrderBy(m => m.CreatedAt).ToListAsync();
|
||||
var unread = msgs.Where(m => m.UserId == peerId && m.PeerId == uid && !m.ReadByPeer).ToList();
|
||||
if (unread.Count > 0) { unread.ForEach(m => m.ReadByPeer = true); await _db.SaveChangesAsync(); }
|
||||
return msgs.Select(m => ToDto(m, uid)).ToList();
|
||||
}
|
||||
|
||||
public async Task<ChatMessageDto> Send(string uid, string peerId, string text)
|
||||
{
|
||||
var m = new MessageRow { UserId = uid, PeerId = peerId, Text = text.Trim(), CreatedAt = DateTime.UtcNow };
|
||||
_db.Messages.Add(m);
|
||||
await _db.SaveChangesAsync();
|
||||
await _hub.Clients.User(peerId).SendAsync("chat", new { peerId = uid });
|
||||
return ToDto(m, uid);
|
||||
}
|
||||
|
||||
private static ChatMessageDto ToDto(MessageRow m, string uid) => new()
|
||||
{
|
||||
Id = m.Id.ToString(),
|
||||
FromMe = m.UserId == uid,
|
||||
Text = m.Text,
|
||||
Ts = new DateTimeOffset(m.CreatedAt).ToUnixTimeMilliseconds(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user