Add webhook support for card transactions and notifications.
Some optimizations and fixes Signed-off-by: Dmitrii <computer@yawaflua.tech> Took 2 hours 4 minutes
This commit is contained in:
@@ -71,13 +71,35 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I
|
|||||||
|
|
||||||
var dto = await resp.Content.ReadFromJsonAsync<MojangDto>();
|
var dto = await resp.Content.ReadFromJsonAsync<MojangDto>();
|
||||||
if (dto == null || dto.Name != session.UserName || Guid.Parse(dto.Id) != session.UserId) throw new Exception("Session expired, or dto is not acceptable.");
|
if (dto == null || dto.Name != session.UserName || Guid.Parse(dto.Id) != session.UserId) throw new Exception("Session expired, or dto is not acceptable.");
|
||||||
|
|
||||||
|
|
||||||
var token = tokenService.GenerateAccessToken(session.UserName, body.userUUID);
|
var token = tokenService.GenerateAccessToken(session.UserName, body.userUUID);
|
||||||
var user = await dbContext.Users.FirstOrDefaultAsync(k => k.Id == session.UserId);
|
var user = await dbContext.Users.FirstOrDefaultAsync(k => k.Id == session.UserId);
|
||||||
|
|
||||||
|
var shortId = string.Empty;
|
||||||
|
|
||||||
|
if (user?.ShortId == null)
|
||||||
|
{
|
||||||
|
shortId = Program.GenerateRandomString(2);
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var a = await dbContext.Users.FirstOrDefaultAsync(k => k.ShortId == shortId);
|
||||||
|
if (a != null)
|
||||||
|
{
|
||||||
|
shortId = Program.GenerateRandomString(2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
user.Token = token;
|
user.Token = token;
|
||||||
user.Username = session.UserName;
|
user.Username = session.UserName;
|
||||||
user.UpdatedAt = DateTime.UtcNow;
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
user.ShortId ??= shortId;
|
||||||
|
|
||||||
dbContext.Update(user);
|
dbContext.Update(user);
|
||||||
}
|
}
|
||||||
@@ -88,7 +110,8 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I
|
|||||||
Id = body.userUUID,
|
Id = body.userUUID,
|
||||||
Username = session.UserName,
|
Username = session.UserName,
|
||||||
Token = token,
|
Token = token,
|
||||||
UpdatedAt = DateTime.UtcNow
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
ShortId = shortId,
|
||||||
};
|
};
|
||||||
await dbContext.AddAsync(user);
|
await dbContext.AddAsync(user);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class TelemetryController : ControllerBase
|
|||||||
private static readonly ActivitySource Source = new("SpMega.ModTelemetry");
|
private static readonly ActivitySource Source = new("SpMega.ModTelemetry");
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize]
|
[AllowAnonymous]
|
||||||
public IActionResult Post([FromBody] ModTelemetryBatchDto batch)
|
public IActionResult Post([FromBody] ModTelemetryBatchDto batch)
|
||||||
{
|
{
|
||||||
if (batch?.Events == null || batch.Events.Count == 0)
|
if (batch?.Events == null || batch.Events.Count == 0)
|
||||||
@@ -28,7 +28,7 @@ public class TelemetryController : ControllerBase
|
|||||||
|
|
||||||
foreach (var e in batch.Events)
|
foreach (var e in batch.Events)
|
||||||
{
|
{
|
||||||
using var activity = Source.StartActivity("mod.telemetry.event", ActivityKind.Internal);
|
using var activity = Source.StartActivity("mod.telemetry.event", ActivityKind.Client);
|
||||||
if (activity is null) continue;
|
if (activity is null) continue;
|
||||||
|
|
||||||
activity.SetTag("user.id", userId);
|
activity.SetTag("user.id", userId);
|
||||||
@@ -38,6 +38,7 @@ public class TelemetryController : ControllerBase
|
|||||||
activity.SetTag("event.timestamp", e.Timestamp.ToString("O"));
|
activity.SetTag("event.timestamp", e.Timestamp.ToString("O"));
|
||||||
activity.SetTag("event.payload", JsonSerializer.Serialize(e.Payload));
|
activity.SetTag("event.payload", JsonSerializer.Serialize(e.Payload));
|
||||||
activity.SetTag("batch.sent_at", batch.SentAt.ToString("O"));
|
activity.SetTag("batch.sent_at", batch.SentAt.ToString("O"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(new { received = batch.Events.Count });
|
return Ok(new { received = batch.Events.Count });
|
||||||
|
|||||||
@@ -37,12 +37,7 @@ public class TransactionsController(AppDbContext context, ILogger<TransactionsCo
|
|||||||
ShortId = "00000000",
|
ShortId = "00000000",
|
||||||
ReceiverName = "yawaflua",
|
ReceiverName = "yawaflua",
|
||||||
ReceiverCardNumber = "00000",
|
ReceiverCardNumber = "00000",
|
||||||
Sender = new User
|
SenderMinecraftName = "yawaflua",
|
||||||
{
|
|
||||||
Id = default,
|
|
||||||
Username = "yawaflua",
|
|
||||||
Token = "123",
|
|
||||||
},
|
|
||||||
SenderCardNumber = "010101",
|
SenderCardNumber = "010101",
|
||||||
Amount = 42,
|
Amount = 42,
|
||||||
Comment = "API FIRST",
|
Comment = "API FIRST",
|
||||||
@@ -76,13 +71,13 @@ public class TransactionsController(AppDbContext context, ILogger<TransactionsCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var shortId = Program.GenerateRandomString(5);
|
var shortId = Program.GenerateRandomString(2);
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var a = await context.Transactions.FirstOrDefaultAsync(k => k.ShortId == shortId);
|
var a = await context.Transactions.FirstOrDefaultAsync(k => k.ShortId == shortId && EF.Property<Guid>(k, "SenderId") == user.Id);
|
||||||
if (a != null)
|
if (a != null)
|
||||||
{
|
{
|
||||||
shortId = Program.GenerateRandomString(5);
|
shortId = Program.GenerateRandomString(2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -92,24 +87,23 @@ public class TransactionsController(AppDbContext context, ILogger<TransactionsCo
|
|||||||
var transaction = new Transaction
|
var transaction = new Transaction
|
||||||
{
|
{
|
||||||
ReceiverName = body.receiverName,
|
ReceiverName = body.receiverName,
|
||||||
ShortId = shortId,
|
ShortId = $"{user.ShortId}{shortId}",
|
||||||
ReceiverCardNumber = body.receiverCard,
|
ReceiverCardNumber = body.receiverCard,
|
||||||
Sender = user,
|
Sender = user,
|
||||||
SenderCardNumber = body.cardId,
|
SenderCardNumber = cardToUse.SpworldsID,
|
||||||
|
SenderMinecraftName = user.Username,
|
||||||
Amount = body.amount,
|
Amount = body.amount,
|
||||||
Comment = body.comment,
|
Comment = body.comment,
|
||||||
};
|
};
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var uri = "s.ywfl.dev" + "/" + shortId;
|
var uri = "ywfl.dev" + "/s" + shortId;
|
||||||
var transitionInfo = new Dictionary<string, object>
|
var transitionInfo = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "receiver", body.receiverCard },
|
{ "receiver", body.receiverCard },
|
||||||
{ "amount", body.amount },
|
{ "amount", body.amount },
|
||||||
{ "comment", (body.comment)[8..] + "..;Чек:"+ uri }
|
{ "comment", (body.comment)[10..] + "..;Чек:"+ uri }
|
||||||
};
|
};
|
||||||
Console.WriteLine(((body.comment)[8..] + "..;Чек:"+ uri).Length);
|
|
||||||
Console.WriteLine(((body.comment)[8..] + "..;Чек:"+ uri));
|
|
||||||
var resp = await SendRequest(endpoint: "transactions", body: transitionInfo, AuthHeader: new("Bearer", cardToUse.Token));
|
var resp = await SendRequest(endpoint: "transactions", body: transitionInfo, AuthHeader: new("Bearer", cardToUse.Token));
|
||||||
var balance = (int?)JsonNode.Parse(resp)?["balance"];
|
var balance = (int?)JsonNode.Parse(resp)?["balance"];
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
@@ -131,6 +125,12 @@ public class TransactionsController(AppDbContext context, ILogger<TransactionsCo
|
|||||||
{
|
{
|
||||||
Id = transaction.Id,
|
Id = transaction.Id,
|
||||||
ReceiverName = transaction.ReceiverName,
|
ReceiverName = transaction.ReceiverName,
|
||||||
|
ReceiverCardNumber = transaction.ReceiverCardNumber,
|
||||||
|
SenderMinecraftName = transaction.SenderMinecraftName,
|
||||||
|
SenderCardNumber = transaction.SenderCardNumber,
|
||||||
|
Amount = transaction.Amount,
|
||||||
|
Comment = transaction.Comment,
|
||||||
|
TransactionDate = transaction.TransactionDate
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MongoDB.Driver.Linq;
|
||||||
|
using SpMega.Backend.Persistent.Database;
|
||||||
|
using SpMega.Backend.Persistent.Models.Transactions;
|
||||||
|
using SpMega.Backend.Persistent.Models.Users;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Controllers.v1;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/api/v1/webhook")]
|
||||||
|
public class WebhookController( AppDbContext dbContext, ILogger<WebhookController> logger, IConfiguration config) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string BASE_URL = "https://spworlds.ru/api/public/";
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPut("{cardId}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> RegisterWebhookForCard(Guid cardId)
|
||||||
|
{
|
||||||
|
var user = HttpContext.Items["@me"] as User;
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var cardToUse = user.Cards.FirstOrDefault(c => c.Id == cardId);
|
||||||
|
if (cardToUse == null)
|
||||||
|
return BadRequest("Card not found");
|
||||||
|
|
||||||
|
var shortId = Program.GenerateRandomString(6);
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var a = user.Cards.FirstOrDefault(k => k.ShortId == shortId);
|
||||||
|
if (a != null)
|
||||||
|
{
|
||||||
|
shortId = Program.GenerateRandomString(6);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cardToUse.ShortId = shortId;
|
||||||
|
cardToUse.WebhookConnected = true;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var webhookUrl = new UriBuilder();
|
||||||
|
webhookUrl.Scheme = "https";
|
||||||
|
webhookUrl.Host = config["Url"];
|
||||||
|
webhookUrl.Path = $"api/v1/webhook/{user.ShortId}/{cardToUse.ShortId}/{cardId}";
|
||||||
|
|
||||||
|
var resp = await SendRequest($"card/webhook", new AuthenticationHeaderValue("Bearer", cardToUse.Token), HttpMethod.Put, new { url = webhookUrl.ToString() });
|
||||||
|
var jsonResponse = JsonSerializer.Deserialize<JsonObject>(resp);
|
||||||
|
if (jsonResponse != null && jsonResponse.TryGetPropertyValue("id", out var respId)
|
||||||
|
&& respId?.GetValue<Guid>() == cardId) return Ok();
|
||||||
|
logger.LogError("Failed to register webhook for card {CardId}. Response: {Response}", cardId, resp);
|
||||||
|
cardToUse.WebhookConnected = false;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
return BadRequest("Failed to register webhook for card");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{userShortId}/{cardShortId}/{cardId}")]
|
||||||
|
public async Task<IActionResult> ReceiveWebhook(string userShortId, string cardShortId, Guid cardId)
|
||||||
|
{
|
||||||
|
var user = await dbContext.Users.FirstOrDefaultAsync(k => k.ShortId == userShortId);
|
||||||
|
if (user == null)
|
||||||
|
return BadRequest("User not found");
|
||||||
|
var cardToUse = user.Cards.FirstOrDefault(c => c.ShortId == cardShortId && c.Id == cardId && c.WebhookConnected);
|
||||||
|
if (cardToUse == null)
|
||||||
|
return BadRequest("Card not found");
|
||||||
|
|
||||||
|
var bodyHash = Request.Headers["X-Body-Hash"].ToString();
|
||||||
|
var rawBody = await new StreamReader(Request.Body).ReadToEndAsync();
|
||||||
|
var keyBytes = Encoding.UTF8.GetBytes(cardToUse.Token);
|
||||||
|
var messageBytes = Encoding.UTF8.GetBytes(rawBody);
|
||||||
|
|
||||||
|
using var hmac = new HMACSHA256(keyBytes);
|
||||||
|
var hashBytes = hmac.ComputeHash(messageBytes);
|
||||||
|
|
||||||
|
byte[] receivedHash;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
receivedHash = Convert.FromBase64String(bodyHash);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid body hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CryptographicOperations.FixedTimeEquals(hashBytes, receivedHash))
|
||||||
|
{
|
||||||
|
logger.LogError("Invalid body hash for webhook. Expected: {ExpectedHash}, Received: {ReceivedHash}", Convert.ToBase64String(hashBytes), bodyHash);
|
||||||
|
return BadRequest("Invalid body hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = JsonSerializer.Deserialize<Webhook>(rawBody, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||||
|
if (body == null || !Guid.TryParse(body.Id, out var notificationId))
|
||||||
|
return BadRequest("Invalid webhook body");
|
||||||
|
|
||||||
|
if (await dbContext.Notifications.FirstOrDefaultAsync(notification => notification.Id == notificationId) != null)
|
||||||
|
return Ok();
|
||||||
|
|
||||||
|
var notify = new Notification
|
||||||
|
{
|
||||||
|
Id = notificationId,
|
||||||
|
ReceiverId = user.Id,
|
||||||
|
ReceiverName = body.Receiver.Username,
|
||||||
|
ReceiverNumber = body.Receiver.Number,
|
||||||
|
SenderName = body.Sender.Username,
|
||||||
|
SenderNumber = body.Sender.Number,
|
||||||
|
Comment = body.Comment,
|
||||||
|
Amount = body.Amount,
|
||||||
|
Type = body.Type,
|
||||||
|
IsRead = false,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.Notifications.AddAsync(notify);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("read")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<List<Notification>>> ReadAllNotifications()
|
||||||
|
{
|
||||||
|
var user = HttpContext.Items["@me"] as User;
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var notifications = await dbContext.Notifications
|
||||||
|
.Where(notification => notification.ReceiverId == user.Id && !notification.IsRead)
|
||||||
|
.ToListAsync();
|
||||||
|
foreach (var notification in notifications)
|
||||||
|
{
|
||||||
|
notification.IsRead = true;
|
||||||
|
notification.UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
if (notifications.Count > 0) await dbContext.SaveChangesAsync();
|
||||||
|
return Ok(notifications);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("all")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<List<Notification>>> GetAllNotifications([FromQuery] int after, [FromQuery] int limit = 20)
|
||||||
|
{
|
||||||
|
var user = HttpContext.Items["@me"] as User;
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
var notification = await dbContext.Notifications.Where(k => k.ReceiverId == user.Id).Skip(after).Take(limit).ToListAsync();
|
||||||
|
return Ok(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonAction]
|
||||||
|
internal async Task<string> SendRequest(string endpoint, AuthenticationHeaderValue AuthHeader, HttpMethod method = null, object body = null)
|
||||||
|
{
|
||||||
|
method ??= body == null ? HttpMethod.Get : HttpMethod.Post;
|
||||||
|
|
||||||
|
HttpResponseMessage message;
|
||||||
|
var client = new System.Net.Http.HttpClient();
|
||||||
|
|
||||||
|
using (var requestMessage = new HttpRequestMessage(method, BASE_URL + endpoint))
|
||||||
|
{
|
||||||
|
requestMessage.Content = new StringContent(
|
||||||
|
JsonSerializer.Serialize(body),
|
||||||
|
Encoding.UTF8, "application/json"
|
||||||
|
);
|
||||||
|
|
||||||
|
requestMessage.Headers.Authorization = AuthHeader;
|
||||||
|
|
||||||
|
message = await client.SendAsync(requestMessage);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
return await message.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Webhook(
|
||||||
|
string Id,
|
||||||
|
int Amount,
|
||||||
|
string Type,
|
||||||
|
WebhookUser Sender,
|
||||||
|
WebhookUser Receiver,
|
||||||
|
string Comment,
|
||||||
|
string CreatedAt
|
||||||
|
);
|
||||||
|
|
||||||
|
public record WebhookUser(string Username, string Number);
|
||||||
@@ -12,8 +12,7 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
|||||||
internal DbSet<Transaction> Transactions { get; set; }
|
internal DbSet<Transaction> Transactions { get; set; }
|
||||||
internal DbSet<User> Users { get; set; }
|
internal DbSet<User> Users { get; set; }
|
||||||
internal DbSet<UserSession> UserSessions { get; set; }
|
internal DbSet<UserSession> UserSessions { get; set; }
|
||||||
internal DbSet<ModTelemetryDocument> ModTelemetry { get; set; }
|
internal DbSet<Notification> Notifications { get; set; }
|
||||||
internal DbSet<BackendRequestTelemetryDocument> BackendRequestTelemetry { get; set; }
|
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using SpMega.Backend.Persistent.Models.Users;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Persistent.Models.Transactions;
|
||||||
|
|
||||||
|
public class Notification
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid ReceiverId { get; set; }
|
||||||
|
public string ReceiverName { get; set; }
|
||||||
|
public string ReceiverNumber { get; set; }
|
||||||
|
public string SenderName { get; set; }
|
||||||
|
public string SenderNumber { get; set; }
|
||||||
|
|
||||||
|
public string Comment { get; set; }
|
||||||
|
public int Amount { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public bool IsRead { get; set; } = false;
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using SpMega.Backend.Persistent.Models.Users;
|
using SpMega.Backend.Persistent.Models.Users;
|
||||||
|
|
||||||
@@ -7,13 +8,15 @@ public class Transaction
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string ShortId { get; set; } = Program.GenerateRandomString(8);
|
public string ShortId { get; set; } = Program.GenerateRandomString(8);
|
||||||
public string ReceiverName { get; set; }
|
[MaxLength(2048)] public string ReceiverName { get; set; }
|
||||||
public string ReceiverCardNumber { get; set; }
|
[MaxLength(2048)] public string ReceiverCardNumber { get; set; }
|
||||||
[JsonIgnore] public User Sender { get; set; }
|
[JsonIgnore] public User Sender { get; set; }
|
||||||
public string SenderCardNumber { get; set; }
|
|
||||||
|
[MaxLength(2048)] public string SenderMinecraftName { get; set; }
|
||||||
|
[MaxLength(2048)] public string SenderCardNumber { get; set; }
|
||||||
|
|
||||||
public int Amount { get; set; } = 0;
|
public int Amount { get; set; } = 0;
|
||||||
public string Comment { get; set; } = "";
|
[MaxLength(2048)] public string Comment { get; set; } = "";
|
||||||
|
|
||||||
public DateTime TransactionDate { get; set; } = DateTime.UtcNow;
|
public DateTime TransactionDate { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,8 @@ public class Card
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string SpworldsID { get; set; }
|
public string SpworldsID { get; set; }
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
public string ShortId { get; set; } = "";
|
||||||
|
public bool WebhookConnected { get; set; } = false;
|
||||||
|
|
||||||
public int Balance { get; set; } = 0;
|
public int Balance { get; set; } = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using SpMega.Backend.Persistent.Models.Transactions;
|
using SpMega.Backend.Persistent.Models.Transactions;
|
||||||
|
|
||||||
namespace SpMega.Backend.Persistent.Models.Users;
|
namespace SpMega.Backend.Persistent.Models.Users;
|
||||||
@@ -5,8 +6,10 @@ namespace SpMega.Backend.Persistent.Models.Users;
|
|||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string Username { get; set; }
|
[MaxLength(2048)] public string Username { get; set; }
|
||||||
public string Token { get; set; }
|
[MaxLength(2048)] public string Token { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(2048)] public string? ShortId { get; set; }
|
||||||
|
|
||||||
public List<Card> Cards { get; set; } = [];
|
public List<Card> Cards { get; set; } = [];
|
||||||
public List<Transaction> Transactions { get; set; } = [];
|
public List<Transaction> Transactions { get; set; } = [];
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace SpMega.Backend.Persistent.Models.Users;
|
namespace SpMega.Backend.Persistent.Models.Users;
|
||||||
|
|
||||||
public class UserSession
|
public class UserSession
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string Token { get; set; }
|
[MaxLength(2048)] public string Token { get; set; }
|
||||||
public User? User { get; set; } = null;
|
public User? User { get; set; } = null;
|
||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
public string UserName { get; set; }
|
[MaxLength(2048)] public string UserName { get; set; }
|
||||||
|
|
||||||
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ using System.Text;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
using OpenTelemetry.Exporter;
|
||||||
|
using OpenTelemetry.Metrics;
|
||||||
using OpenTelemetry.Resources;
|
using OpenTelemetry.Resources;
|
||||||
using OpenTelemetry.Trace;
|
using OpenTelemetry.Trace;
|
||||||
using SpMega.Backend.Authetication;
|
using SpMega.Backend.Authetication;
|
||||||
@@ -68,7 +70,10 @@ public class Program
|
|||||||
options.UseMongoDB(mongoClient, "spmega");
|
options.UseMongoDB(mongoClient, "spmega");
|
||||||
});
|
});
|
||||||
|
|
||||||
var otlpEndpoint = conf["Otlp:Endpoint"] ?? "http://localhost:4317";
|
var otlpEndpoint = (conf["Otlp__Endpoint"] ?? "http://curiosity:5080/api/default").TrimEnd('/');
|
||||||
|
var otlpHeaders = conf["Otlp__Headers"] ?? throw new InvalidOperationException("Otlp__Headers is not configured.");
|
||||||
|
var tracesEndpoint = new Uri($"{otlpEndpoint}/v1/traces");
|
||||||
|
var metricsEndpoint = new Uri($"{otlpEndpoint}/v1/metrics");
|
||||||
builder.Services.AddOpenTelemetry()
|
builder.Services.AddOpenTelemetry()
|
||||||
.ConfigureResource(res => res.AddService("SpMega.Backend"))
|
.ConfigureResource(res => res.AddService("SpMega.Backend"))
|
||||||
.WithTracing(tracing =>
|
.WithTracing(tracing =>
|
||||||
@@ -79,9 +84,24 @@ public class Program
|
|||||||
.AddAspNetCoreInstrumentation()
|
.AddAspNetCoreInstrumentation()
|
||||||
.AddOtlpExporter(opts =>
|
.AddOtlpExporter(opts =>
|
||||||
{
|
{
|
||||||
opts.Endpoint = new Uri(otlpEndpoint);
|
opts.Protocol = OtlpExportProtocol.HttpProtobuf;
|
||||||
|
opts.Endpoint = tracesEndpoint;
|
||||||
|
opts.Headers = otlpHeaders;
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
.WithMetrics(metrics => metrics
|
||||||
|
.AddAspNetCoreInstrumentation()
|
||||||
|
.AddMeter(
|
||||||
|
"Microsoft.AspNetCore.Hosting",
|
||||||
|
"Microsoft.AspNetCore.Server.Kestrel",
|
||||||
|
"System.Net.Http",
|
||||||
|
"System.Runtime")
|
||||||
|
.AddOtlpExporter(opts =>
|
||||||
|
{
|
||||||
|
opts.Protocol = OtlpExportProtocol.HttpProtobuf;
|
||||||
|
opts.Endpoint = metricsEndpoint;
|
||||||
|
opts.Headers = otlpHeaders;
|
||||||
|
}));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -113,4 +133,4 @@ public class Program
|
|||||||
|
|
||||||
return result.ToString();
|
return result.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,317 +0,0 @@
|
|||||||
{
|
|
||||||
"runtimeTarget": {
|
|
||||||
"name": ".NETCoreApp,Version=v10.0",
|
|
||||||
"signature": ""
|
|
||||||
},
|
|
||||||
"compilationOptions": {},
|
|
||||||
"targets": {
|
|
||||||
".NETCoreApp,Version=v10.0": {
|
|
||||||
"SpMega.Backend/1.0.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.9",
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational": "10.0.9",
|
|
||||||
"Microsoft.IdentityModel.Tokens": "8.19.1",
|
|
||||||
"MongoDB.EntityFrameworkCore": "10.0.2",
|
|
||||||
"System.IdentityModel.Tokens.Jwt": "8.19.1",
|
|
||||||
"spworlds-api": "1.0.3"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"SpMega.Backend.dll": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DnsClient/1.6.1": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net5.0/DnsClient.dll": {
|
|
||||||
"assemblyVersion": "1.6.1.0",
|
|
||||||
"fileVersion": "1.6.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Bcl.Cryptography/10.0.2": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.Bcl.Cryptography.dll": {
|
|
||||||
"assemblyVersion": "10.0.0.2",
|
|
||||||
"fileVersion": "10.0.225.61305"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore/10.0.9": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.9"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.EntityFrameworkCore.dll": {
|
|
||||||
"assemblyVersion": "10.0.9.0",
|
|
||||||
"fileVersion": "10.0.926.27113"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions/10.0.9": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
|
|
||||||
"assemblyVersion": "10.0.9.0",
|
|
||||||
"fileVersion": "10.0.926.27113"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational/10.0.9": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore": "10.0.9"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll": {
|
|
||||||
"assemblyVersion": "10.0.9.0",
|
|
||||||
"fileVersion": "10.0.926.27113"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Abstractions/8.19.1": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.IdentityModel.Abstractions.dll": {
|
|
||||||
"assemblyVersion": "8.19.1.0",
|
|
||||||
"fileVersion": "8.19.1.26153"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.JsonWebTokens/8.19.1": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.IdentityModel.Tokens": "8.19.1"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.IdentityModel.JsonWebTokens.dll": {
|
|
||||||
"assemblyVersion": "8.19.1.0",
|
|
||||||
"fileVersion": "8.19.1.26153"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Logging/8.19.1": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.IdentityModel.Abstractions": "8.19.1"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.IdentityModel.Logging.dll": {
|
|
||||||
"assemblyVersion": "8.19.1.0",
|
|
||||||
"fileVersion": "8.19.1.26153"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Tokens/8.19.1": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Bcl.Cryptography": "10.0.2",
|
|
||||||
"Microsoft.IdentityModel.Logging": "8.19.1"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/Microsoft.IdentityModel.Tokens.dll": {
|
|
||||||
"assemblyVersion": "8.19.1.0",
|
|
||||||
"fileVersion": "8.19.1.26153"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MongoDB.Bson/3.9.0": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net6.0/MongoDB.Bson.dll": {
|
|
||||||
"assemblyVersion": "3.9.0.0",
|
|
||||||
"fileVersion": "3.9.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MongoDB.Driver/3.9.0": {
|
|
||||||
"dependencies": {
|
|
||||||
"DnsClient": "1.6.1",
|
|
||||||
"MongoDB.Bson": "3.9.0",
|
|
||||||
"SharpCompress": "0.48.1",
|
|
||||||
"Snappier": "1.3.1",
|
|
||||||
"ZstdSharp.Port": "0.7.3"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net6.0/MongoDB.Driver.dll": {
|
|
||||||
"assemblyVersion": "3.9.0.0",
|
|
||||||
"fileVersion": "3.9.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MongoDB.EntityFrameworkCore/10.0.2": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore": "10.0.9",
|
|
||||||
"MongoDB.Driver": "3.9.0"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/MongoDB.EntityFrameworkCore.dll": {
|
|
||||||
"assemblyVersion": "10.0.2.0",
|
|
||||||
"fileVersion": "10.0.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SharpCompress/0.48.1": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/SharpCompress.dll": {
|
|
||||||
"assemblyVersion": "0.48.1.0",
|
|
||||||
"fileVersion": "0.48.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Snappier/1.3.1": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/Snappier.dll": {
|
|
||||||
"assemblyVersion": "1.3.1.0",
|
|
||||||
"fileVersion": "1.3.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"spworlds-api/1.0.3": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net8.0/spworlds-api.dll": {
|
|
||||||
"assemblyVersion": "1.0.3.0",
|
|
||||||
"fileVersion": "1.0.3.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.IdentityModel.Tokens.Jwt/8.19.1": {
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.IdentityModel.JsonWebTokens": "8.19.1",
|
|
||||||
"Microsoft.IdentityModel.Tokens": "8.19.1"
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"lib/net10.0/System.IdentityModel.Tokens.Jwt.dll": {
|
|
||||||
"assemblyVersion": "8.19.1.0",
|
|
||||||
"fileVersion": "8.19.1.26153"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ZstdSharp.Port/0.7.3": {
|
|
||||||
"runtime": {
|
|
||||||
"lib/net7.0/ZstdSharp.dll": {
|
|
||||||
"assemblyVersion": "0.7.3.0",
|
|
||||||
"fileVersion": "0.7.3.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"libraries": {
|
|
||||||
"SpMega.Backend/1.0.0": {
|
|
||||||
"type": "project",
|
|
||||||
"serviceable": false,
|
|
||||||
"sha512": ""
|
|
||||||
},
|
|
||||||
"DnsClient/1.6.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==",
|
|
||||||
"path": "dnsclient/1.6.1",
|
|
||||||
"hashPath": "dnsclient.1.6.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.Bcl.Cryptography/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-LG9Yll3B5aNpxv0+D47g6LiOiKBIlodhcHdQwcYzo8VeexFLGqx5ymetmA2aBRyo9cCcWsQWrFsdbsr8LvmWDw==",
|
|
||||||
"path": "microsoft.bcl.cryptography/10.0.2",
|
|
||||||
"hashPath": "microsoft.bcl.cryptography.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore/10.0.9": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-tu85SRzOT021V7EQlViCiAE7TqldVn469Y6lt5TEn/+XC4/MeNCHgMRSxqYuWqvF4zAQZUhCmtNEZuM3ss4LeA==",
|
|
||||||
"path": "microsoft.entityframeworkcore/10.0.9",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.10.0.9.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions/10.0.9": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-GRMaiPkqYna/gCsyDffYDWmefGPC3hDrdMw+2rrGcQwhs6uZOsaMQXMJnoXQ35tx9SkBV2ieRRU9N/jLOO6BZw==",
|
|
||||||
"path": "microsoft.entityframeworkcore.abstractions/10.0.9",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.abstractions.10.0.9.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Relational/10.0.9": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-dEoyYKgiaZHHgOFm1WMWm1sFEsEuhPWufX4L9PekKtqd/RaIcPjkCjvbrVvJtApErb5wPSJhYvnTlxhH+p9h2g==",
|
|
||||||
"path": "microsoft.entityframeworkcore.relational/10.0.9",
|
|
||||||
"hashPath": "microsoft.entityframeworkcore.relational.10.0.9.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Abstractions/8.19.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-gFA8THIk23uNF/vMdOHnjIdXD1LyA2g12cHzMJ+Xag6WpgWLw6E/6uCXxvA0gp9d2yAvkRt3xzFzMUiO/hofnQ==",
|
|
||||||
"path": "microsoft.identitymodel.abstractions/8.19.1",
|
|
||||||
"hashPath": "microsoft.identitymodel.abstractions.8.19.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.JsonWebTokens/8.19.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-6eeY+y2QFyjj3XnCz/8gJdoP5smYHTS9ow1bw2nsZzDIPjPhBZlackYTIduSMipVpxnoT/B62LkrXX2jPggOXg==",
|
|
||||||
"path": "microsoft.identitymodel.jsonwebtokens/8.19.1",
|
|
||||||
"hashPath": "microsoft.identitymodel.jsonwebtokens.8.19.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Logging/8.19.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-H+sMrMpdbWnwkQnpb/ESkQovtOgdefmj0ecGCcP40mDKzE5i4dUYkH6599M9mWYFNGNJnTp92l/9wLubYXWimw==",
|
|
||||||
"path": "microsoft.identitymodel.logging/8.19.1",
|
|
||||||
"hashPath": "microsoft.identitymodel.logging.8.19.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Tokens/8.19.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-KDiuSLXud2AFVNAOottd8ztVysfPeHyr4r8gofU3/VKUXlI7oytzGTnPsNJ/B3nui17rgz8wAdWNJOtzPjkUxw==",
|
|
||||||
"path": "microsoft.identitymodel.tokens/8.19.1",
|
|
||||||
"hashPath": "microsoft.identitymodel.tokens.8.19.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"MongoDB.Bson/3.9.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-J6vB61zKwMSfxbkN+/amGvj1qVMDKrKjV3kmoOWttMcv8JJScLDCFh89FyL3f2lDUyJrnfgZCR6/KX+07e99eg==",
|
|
||||||
"path": "mongodb.bson/3.9.0",
|
|
||||||
"hashPath": "mongodb.bson.3.9.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"MongoDB.Driver/3.9.0": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-XKUa+y5RtNH1iInfxj3Y7c1FN1BQ16/7hFxoqU6fzc3+BKM1D3mGa+pB/yBbAk8jNxf7+JWEnCfuQOyCo7dQLg==",
|
|
||||||
"path": "mongodb.driver/3.9.0",
|
|
||||||
"hashPath": "mongodb.driver.3.9.0.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"MongoDB.EntityFrameworkCore/10.0.2": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-GBN9+OAoIyBEgVPlYTXW3tyIsnpGPCA5AWZPy+X7ca3zJu+2Lli9FODZWl//w0yZHqwGwsLUL6XhcPPEaBYvBQ==",
|
|
||||||
"path": "mongodb.entityframeworkcore/10.0.2",
|
|
||||||
"hashPath": "mongodb.entityframeworkcore.10.0.2.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"SharpCompress/0.48.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-SqGaVniGG943Gph/gHhUQUiZPyC7y0tXZyMf0/B2oGsMav9dqs7JJOuUA+xOkwKYaWM2TM7aZQjNK91f4bX71A==",
|
|
||||||
"path": "sharpcompress/0.48.1",
|
|
||||||
"hashPath": "sharpcompress.0.48.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"Snappier/1.3.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-DOdDQiO8YZ5rBtVLY+6CmR1yp9WYoJRgEEktPBrR0tEj9QO2djA/zv0O3DX0OZpEAfosbY8pytQ9tQUogwQsEA==",
|
|
||||||
"path": "snappier/1.3.1",
|
|
||||||
"hashPath": "snappier.1.3.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"spworlds-api/1.0.3": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-rLQsC3MiXBfPKf3v45Qcrj0yypfZktTPuT/eGUJpcAlSZix1FBSRUbQVtfXbA8BHGIqn7nhZMVetaP3dmk+n1g==",
|
|
||||||
"path": "spworlds-api/1.0.3",
|
|
||||||
"hashPath": "spworlds-api.1.0.3.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"System.IdentityModel.Tokens.Jwt/8.19.1": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-2VHcRtT95GAcW1E3aVBLvL2rAAMxKHXKMXKXFyWzwgkdFXZPMMvP8tVOfnRydL4vTr1RirNuGC6T8VSEF2YsPQ==",
|
|
||||||
"path": "system.identitymodel.tokens.jwt/8.19.1",
|
|
||||||
"hashPath": "system.identitymodel.tokens.jwt.8.19.1.nupkg.sha512"
|
|
||||||
},
|
|
||||||
"ZstdSharp.Port/0.7.3": {
|
|
||||||
"type": "package",
|
|
||||||
"serviceable": true,
|
|
||||||
"sha512": "sha512-U9Ix4l4cl58Kzz1rJzj5hoVTjmbx1qGMwzAcbv1j/d3NzrFaESIurQyg+ow4mivCgkE3S413y+U9k4WdnEIkRA==",
|
|
||||||
"path": "zstdsharp.port/0.7.3",
|
|
||||||
"hashPath": "zstdsharp.port.0.7.3.nupkg.sha512"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"runtimeOptions": {
|
|
||||||
"tfm": "net10.0",
|
|
||||||
"frameworks": [
|
|
||||||
{
|
|
||||||
"name": "Microsoft.NETCore.App",
|
|
||||||
"version": "10.0.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Microsoft.AspNetCore.App",
|
|
||||||
"version": "10.0.0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configProperties": {
|
|
||||||
"System.GC.Server": true,
|
|
||||||
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
|
||||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"Version": 1,
|
|
||||||
"ManifestType": "Build",
|
|
||||||
"Endpoints": []
|
|
||||||
}
|
|
||||||
+1
-1
@@ -6,7 +6,7 @@ minecraft_version=1.21.11
|
|||||||
yarn_mappings=1.21.11+build.4
|
yarn_mappings=1.21.11+build.4
|
||||||
loader_version=0.18.1
|
loader_version=0.18.1
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version=0.4-pre-alpha
|
mod_version=0.5-beta
|
||||||
maven_group=git.yawaflua.tech
|
maven_group=git.yawaflua.tech
|
||||||
archives_base_name=SPMega
|
archives_base_name=SPMega
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public class ModMenuIntegration implements ModMenuApi {
|
|||||||
final boolean[] allowBackend = {current.allowBackend()};
|
final boolean[] allowBackend = {current.allowBackend()};
|
||||||
final boolean[] signQuickPayEnabled = {current.signQuickPayEnabled()};
|
final boolean[] signQuickPayEnabled = {current.signQuickPayEnabled()};
|
||||||
final GpsHudPosition[] gpsPosition = {current.gpsPosition()};
|
final GpsHudPosition[] gpsPosition = {current.gpsPosition()};
|
||||||
|
final GpsHudPosition[] notificationPosition = {current.notificationPosition()};
|
||||||
|
|
||||||
final boolean[] telemetryEnabled = {current.telemetryEnabled()};
|
final boolean[] telemetryEnabled = {current.telemetryEnabled()};
|
||||||
final int[] telemetryIntervalSeconds = {current.telemetryIntervalSeconds()};
|
final int[] telemetryIntervalSeconds = {current.telemetryIntervalSeconds()};
|
||||||
@@ -59,10 +60,12 @@ public class ModMenuIntegration implements ModMenuApi {
|
|||||||
.setTooltip(Text.literal("Запустить процесс авторизации через Mojang и ваш бэкенд"))
|
.setTooltip(Text.literal("Запустить процесс авторизации через Mojang и ваш бэкенд"))
|
||||||
.setSaveConsumer(newValue -> {
|
.setSaveConsumer(newValue -> {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
BackendAuthenticator.authenticate(MinecraftClient.getInstance());
|
BackendAuthenticator.authenticateAsync(MinecraftClient.getInstance())
|
||||||
if (SPMega.getConfig() != null) {
|
.thenAccept(authenticated -> {
|
||||||
apiToken[0] = SPMega.getConfig().apiToken();
|
if (authenticated && SPMega.getConfig() != null) {
|
||||||
}
|
apiToken[0] = SPMega.getConfig().apiToken();
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
apiToken[0] = ModConfig.DEFAULT_API_TOKEN;
|
apiToken[0] = ModConfig.DEFAULT_API_TOKEN;
|
||||||
}
|
}
|
||||||
@@ -84,6 +87,17 @@ public class ModMenuIntegration implements ModMenuApi {
|
|||||||
.setSaveConsumer(newValue -> gpsPosition[0] = newValue)
|
.setSaveConsumer(newValue -> gpsPosition[0] = newValue)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
|
general.addEntry(entryBuilder.startEnumSelector(
|
||||||
|
Text.translatable("option.spmega.notification_position"),
|
||||||
|
GpsHudPosition.class,
|
||||||
|
current.notificationPosition()
|
||||||
|
)
|
||||||
|
.setDefaultValue(ModConfig.DEFAULT_NOTIFICATION_POSITION)
|
||||||
|
.setEnumNameProvider(position -> Text.translatable(
|
||||||
|
"option.spmega.notification_position." + position.name().toLowerCase()))
|
||||||
|
.setSaveConsumer(newValue -> notificationPosition[0] = newValue)
|
||||||
|
.build());
|
||||||
|
|
||||||
// Telemetry settings
|
// Telemetry settings
|
||||||
ConfigCategory telemetry = builder.getOrCreateCategory(Text.translatable("category.spmega.telemetry"));
|
ConfigCategory telemetry = builder.getOrCreateCategory(Text.translatable("category.spmega.telemetry"));
|
||||||
|
|
||||||
@@ -119,6 +133,7 @@ public class ModMenuIntegration implements ModMenuApi {
|
|||||||
signQuickPayEnabled[0],
|
signQuickPayEnabled[0],
|
||||||
gpsEnabledVal,
|
gpsEnabledVal,
|
||||||
gpsPosition[0],
|
gpsPosition[0],
|
||||||
|
notificationPosition[0],
|
||||||
telemetryEnabled[0],
|
telemetryEnabled[0],
|
||||||
telemetryIntervalSeconds[0],
|
telemetryIntervalSeconds[0],
|
||||||
telemetryCollectSystemInfo[0]
|
telemetryCollectSystemInfo[0]
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import net.minecraft.text.Text;
|
|||||||
import net.minecraft.util.ActionResult;
|
import net.minecraft.util.ActionResult;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -107,7 +106,7 @@ public class SPMegaClient implements ClientModInitializer {
|
|||||||
UiOpeners.openMainMenu(client);
|
UiOpeners.openMainMenu(client);
|
||||||
}
|
}
|
||||||
while (scanQrKeyBinding.wasPressed()) {
|
while (scanQrKeyBinding.wasPressed()) {
|
||||||
QRCodeScanner.ScanQrCode(client);
|
QRCodeScanner.scanQrCode(client);
|
||||||
}
|
}
|
||||||
while (toggleGpsKeyBinding.wasPressed()) {
|
while (toggleGpsKeyBinding.wasPressed()) {
|
||||||
GpsHudRenderer.instance().toggle();
|
GpsHudRenderer.instance().toggle();
|
||||||
@@ -120,6 +119,7 @@ public class SPMegaClient implements ClientModInitializer {
|
|||||||
current.signQuickPayEnabled(),
|
current.signQuickPayEnabled(),
|
||||||
GpsHudRenderer.instance().isEnabled(),
|
GpsHudRenderer.instance().isEnabled(),
|
||||||
current.gpsPosition(),
|
current.gpsPosition(),
|
||||||
|
current.notificationPosition(),
|
||||||
current.telemetryEnabled(),
|
current.telemetryEnabled(),
|
||||||
current.telemetryIntervalSeconds(),
|
current.telemetryIntervalSeconds(),
|
||||||
current.telemetryCollectSystemInfo()
|
current.telemetryCollectSystemInfo()
|
||||||
@@ -232,7 +232,8 @@ public class SPMegaClient implements ClientModInitializer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
new ChatListener().register();
|
new ChatListener().register();
|
||||||
CompletableFuture.runAsync(() -> BackendAuthenticator.authenticate(MinecraftClient.getInstance()))
|
WebhookNotificationPoller.instance().start();
|
||||||
|
BackendAuthenticator.authenticateAsync(MinecraftClient.getInstance())
|
||||||
.exceptionally(throwable -> {
|
.exceptionally(throwable -> {
|
||||||
System.err.println("[SPMEGA] Error during async authenticate: " + throwable.getMessage());
|
System.err.println("[SPMEGA] Error during async authenticate: " + throwable.getMessage());
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package git.yawaflua.tech.spmega.client;
|
||||||
|
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.UiNotifications;
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator;
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public final class WebhookNotificationPoller {
|
||||||
|
private static final WebhookNotificationPoller INSTANCE = new WebhookNotificationPoller();
|
||||||
|
private static final long POLL_INTERVAL_SECONDS = 15;
|
||||||
|
|
||||||
|
private final AtomicBoolean started = new AtomicBoolean();
|
||||||
|
private final AtomicBoolean requestInFlight = new AtomicBoolean();
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "SPMega-Webhook-Poller");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
|
||||||
|
private WebhookNotificationPoller() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebhookNotificationPoller instance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(BackendAuthenticator.PaymentNotification notification) {
|
||||||
|
String sender = notification.senderName().isBlank()
|
||||||
|
? notification.senderNumber()
|
||||||
|
: notification.senderName();
|
||||||
|
String comment = notification.comment().isBlank() ? "" : " — " + notification.comment();
|
||||||
|
return "Получено " + notification.amount() + " АР от " + sender + comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (started.compareAndSet(false, true)) {
|
||||||
|
scheduler.scheduleAtFixedRate(this::poll, 0, POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void poll() {
|
||||||
|
try {
|
||||||
|
if (!BankUiService.instance().hasWebhookEnabledCards()
|
||||||
|
|| !requestInFlight.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
System.err.println("[SPMEGA] Failed to read local webhook state: " + exception.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendAuthenticator.readNotificationsAsync().whenComplete((notifications, exception) -> {
|
||||||
|
requestInFlight.set(false);
|
||||||
|
if (exception != null) {
|
||||||
|
System.err.println("[SPMEGA] Failed to poll webhook notifications: " + exception.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client == null || notifications.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.execute(() -> notifications.forEach(notification ->
|
||||||
|
UiNotifications.instance().showQueued(Text.literal(format(notification)))));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,12 @@ import git.yawaflua.tech.spmega.client.telemetry.InstrumentedHttpClient;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
|
||||||
public final class SPWorldsApiClient {
|
public final class SPWorldsApiClient {
|
||||||
private final InstrumentedHttpClient httpClient;
|
private final InstrumentedHttpClient httpClient;
|
||||||
@@ -48,88 +49,88 @@ public final class SPWorldsApiClient {
|
|||||||
return json.get(key).getAsString();
|
return json.get(key).getAsString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CardInfo getCardInfo(CardAuth auth) throws IOException, InterruptedException {
|
public CompletableFuture<CardInfo> getCardInfoAsync(CardAuth auth) {
|
||||||
HttpRequest request = requestBuilder("/api/public/card", auth).GET().build();
|
HttpRequest request = requestBuilder("/api/public/card", auth).GET().build();
|
||||||
String body = send(request);
|
return sendAsync(request).thenApply(body -> {
|
||||||
try {
|
try {
|
||||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
if (json.has("statusCode")) {
|
if (json.has("statusCode")) {
|
||||||
switch (json.get("statusCode").getAsInt()) {
|
switch (json.get("statusCode").getAsInt()) {
|
||||||
case 403:
|
case 403:
|
||||||
throw new IOException("Апи вернула ошибку: " + json.get("message").getAsString());
|
throw new IOException("Апи вернула ошибку: " + json.get("message").getAsString());
|
||||||
default:
|
default:
|
||||||
System.out.println("Unhandled status code in card info response: " + json.get("statusCode").getAsInt());
|
System.out.println("Unhandled status code in card info response: " + json.get("statusCode").getAsInt());
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
||||||
|
String webhook = json.has("webhook") && !json.get("webhook").isJsonNull()
|
||||||
|
? json.get("webhook").getAsString()
|
||||||
|
: "";
|
||||||
|
return new CardInfo(balance, webhook);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
System.out.println("Failed to parse card info response: " + exception.getMessage());
|
||||||
|
System.out.println(body);
|
||||||
|
throw new CompletionException(new IOException("Failed to parse card info response", exception));
|
||||||
}
|
}
|
||||||
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
});
|
||||||
String webhook = json.has("webhook") && !json.get("webhook").isJsonNull()
|
|
||||||
? json.get("webhook").getAsString()
|
|
||||||
: "";
|
|
||||||
return new CardInfo(balance, webhook);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println("Failed to parse card info response: " + e.getMessage());
|
|
||||||
System.out.println(body);
|
|
||||||
|
|
||||||
throw new IOException("Failed to parse card info response", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<PlayerCard> getPlayerCards(String username, CardAuth auth) throws IOException, InterruptedException {
|
public CompletableFuture<List<PlayerCard>> getPlayerCardsAsync(String username, CardAuth auth) {
|
||||||
HttpRequest request = requestBuilder("/api/public/accounts/" + username + "/cards", auth).GET().build();
|
HttpRequest request = requestBuilder("/api/public/accounts/" + username + "/cards", auth).GET().build();
|
||||||
String body = send(request);
|
return sendAsync(request).thenApply(body -> {
|
||||||
try {
|
try {
|
||||||
JsonArray json = JsonParser.parseString(body).getAsJsonArray();
|
JsonArray json = JsonParser.parseString(body).getAsJsonArray();
|
||||||
|
List<PlayerCard> cards = new ArrayList<>();
|
||||||
List<PlayerCard> cards = new ArrayList<>();
|
for (JsonElement element : json) {
|
||||||
for (JsonElement element : json) {
|
|
||||||
JsonObject card = element.getAsJsonObject();
|
|
||||||
String name = card.has("name") ? card.get("name").getAsString() : "";
|
|
||||||
String number = card.has("number") ? card.get("number").getAsString() : "";
|
|
||||||
cards.add(new PlayerCard(name, number));
|
|
||||||
}
|
|
||||||
return cards;
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println("Failed to parse player cards response: " + e.getMessage());
|
|
||||||
System.out.println(body);
|
|
||||||
throw new IOException("Failed to parse player cards response", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountMe getAccountMe(CardAuth auth) throws IOException, InterruptedException {
|
|
||||||
HttpRequest request = requestBuilder("/api/public/accounts/me", auth).GET().build();
|
|
||||||
String body = send(request);
|
|
||||||
try {
|
|
||||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
|
||||||
|
|
||||||
String id = json.has("id") ? json.get("id").getAsString() : "";
|
|
||||||
String username = json.has("username") ? json.get("username").getAsString() : "";
|
|
||||||
String minecraftUuid = json.has("minecraftUUID") ? json.get("minecraftUUID").getAsString() : "";
|
|
||||||
|
|
||||||
List<AccountCard> cards = new ArrayList<>();
|
|
||||||
if (json.has("cards") && json.get("cards").isJsonArray()) {
|
|
||||||
for (JsonElement element : json.getAsJsonArray("cards")) {
|
|
||||||
JsonObject card = element.getAsJsonObject();
|
JsonObject card = element.getAsJsonObject();
|
||||||
cards.add(new AccountCard(
|
String name = card.has("name") ? card.get("name").getAsString() : "";
|
||||||
getString(card, "id"),
|
String number = card.has("number") ? card.get("number").getAsString() : "";
|
||||||
getString(card, "name"),
|
cards.add(new PlayerCard(name, number));
|
||||||
getString(card, "number"),
|
|
||||||
card.has("color") && !card.get("color").isJsonNull() ? card.get("color").getAsInt() : 0
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
return cards;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
System.out.println("Failed to parse player cards response: " + exception.getMessage());
|
||||||
|
System.out.println(body);
|
||||||
|
throw new CompletionException(new IOException("Failed to parse player cards response", exception));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return new AccountMe(id, username, minecraftUuid, cards);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println("Failed to parse account info response: " + e.getMessage());
|
|
||||||
System.out.println(body);
|
|
||||||
|
|
||||||
throw new IOException("Failed to parse account info response", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TransactionResult createTransaction(CardAuth auth, String receiver, long amount, String comment)
|
public CompletableFuture<AccountMe> getAccountMeAsync(CardAuth auth) {
|
||||||
throws IOException, InterruptedException {
|
HttpRequest request = requestBuilder("/api/public/accounts/me", auth).GET().build();
|
||||||
|
return sendAsync(request).thenApply(body -> {
|
||||||
|
try {
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
|
||||||
|
String id = json.has("id") ? json.get("id").getAsString() : "";
|
||||||
|
String username = json.has("username") ? json.get("username").getAsString() : "";
|
||||||
|
String minecraftUuid = json.has("minecraftUUID") ? json.get("minecraftUUID").getAsString() : "";
|
||||||
|
|
||||||
|
List<AccountCard> cards = new ArrayList<>();
|
||||||
|
if (json.has("cards") && json.get("cards").isJsonArray()) {
|
||||||
|
for (JsonElement element : json.getAsJsonArray("cards")) {
|
||||||
|
JsonObject card = element.getAsJsonObject();
|
||||||
|
cards.add(new AccountCard(
|
||||||
|
getString(card, "id"),
|
||||||
|
getString(card, "name"),
|
||||||
|
getString(card, "number"),
|
||||||
|
card.has("color") && !card.get("color").isJsonNull() ? card.get("color").getAsInt() : 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AccountMe(id, username, minecraftUuid, cards);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
System.out.println("Failed to parse account info response: " + exception.getMessage());
|
||||||
|
System.out.println(body);
|
||||||
|
throw new CompletionException(new IOException("Failed to parse account info response", exception));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<TransactionResult> createTransactionAsync(
|
||||||
|
CardAuth auth, String receiver, long amount, String comment) {
|
||||||
JsonObject payload = new JsonObject();
|
JsonObject payload = new JsonObject();
|
||||||
payload.addProperty("receiver", receiver);
|
payload.addProperty("receiver", receiver);
|
||||||
payload.addProperty("amount", amount);
|
payload.addProperty("amount", amount);
|
||||||
@@ -139,18 +140,17 @@ public final class SPWorldsApiClient {
|
|||||||
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
|
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
String body = send(request);
|
return sendAsync(request).thenApply(body -> {
|
||||||
try {
|
try {
|
||||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
||||||
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
return new TransactionResult(balance);
|
||||||
return new TransactionResult(balance);
|
} catch (Exception exception) {
|
||||||
} catch (Exception exception) {
|
System.out.println("Failed to parse transaction response: " + exception.getMessage());
|
||||||
System.out.println("Failed to parse transaction response: " + exception.getMessage());
|
System.out.println(body);
|
||||||
System.out.println(body);
|
throw new CompletionException(new IOException("Failed to parse transaction response", exception));
|
||||||
|
}
|
||||||
throw new IOException("Failed to parse transaction response", exception);
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpRequest.Builder requestBuilder(String path, CardAuth auth) {
|
private HttpRequest.Builder requestBuilder(String path, CardAuth auth) {
|
||||||
@@ -160,22 +160,25 @@ public final class SPWorldsApiClient {
|
|||||||
.header("Accept", "application/json");
|
.header("Accept", "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String send(HttpRequest request) throws IOException, InterruptedException {
|
private CompletableFuture<String> sendAsync(HttpRequest request) {
|
||||||
HttpResponse<String> response = httpClient.send(request);
|
return httpClient.sendAsync(request).thenApply(response -> {
|
||||||
int statusCode = response.statusCode();
|
int statusCode = response.statusCode();
|
||||||
if (statusCode < 200 || statusCode >= 300) {
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
System.out.println(response.body());
|
System.out.println(response.body());
|
||||||
JsonElement parsed = JsonParser.parseString(response.body());
|
JsonElement parsed = JsonParser.parseString(response.body());
|
||||||
JsonObject object = parsed.isJsonArray() ? parsed.getAsJsonArray().get(0).getAsJsonObject() : parsed.getAsJsonObject();
|
JsonObject object = parsed.isJsonArray()
|
||||||
var message = "";
|
? parsed.getAsJsonArray().get(0).getAsJsonObject()
|
||||||
if (object.has("error") && !object.get("error").isJsonNull()) {
|
: parsed.getAsJsonObject();
|
||||||
message = "Ошибка в запросе: " + object.get("error").getAsString();
|
String message = "";
|
||||||
} else if (object.has("message") && !object.get("message").isJsonNull()) {
|
if (object.has("error") && !object.get("error").isJsonNull()) {
|
||||||
message = object.get("message").getAsString();
|
message = "Ошибка в запросе: " + object.get("error").getAsString();
|
||||||
|
} else if (object.has("message") && !object.get("message").isJsonNull()) {
|
||||||
|
message = object.get("message").getAsString();
|
||||||
|
}
|
||||||
|
throw new CompletionException(new IOException(statusCode + ": " + message));
|
||||||
}
|
}
|
||||||
throw new IOException(statusCode + ": " + message);
|
return response.body();
|
||||||
}
|
});
|
||||||
return response.body();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record CardAuth(String cardId, String cardToken) {
|
public record CardAuth(String cardId, String cardToken) {
|
||||||
|
|||||||
@@ -9,18 +9,27 @@ import git.yawaflua.tech.spmega.client.telemetry.TelemetryEvent;
|
|||||||
import git.yawaflua.tech.spmega.client.ui.QRcodeAcceptScreen;
|
import git.yawaflua.tech.spmega.client.ui.QRcodeAcceptScreen;
|
||||||
import git.yawaflua.tech.spmega.client.ui.UiNotifications;
|
import git.yawaflua.tech.spmega.client.ui.UiNotifications;
|
||||||
import net.minecraft.client.MinecraftClient;
|
import net.minecraft.client.MinecraftClient;
|
||||||
import net.minecraft.client.texture.NativeImage;
|
|
||||||
import net.minecraft.client.util.ScreenshotRecorder;
|
import net.minecraft.client.util.ScreenshotRecorder;
|
||||||
import net.minecraft.text.Text;
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public class QRCodeScanner {
|
public final class QRCodeScanner {
|
||||||
|
private static final ExecutorService DECODER = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "SPMega-QR-Decoder");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
|
||||||
public static void ScanQrCode(MinecraftClient client) {
|
private QRCodeScanner() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void scanQrCode(MinecraftClient client) {
|
||||||
if (client == null || client.player == null || client.getFramebuffer() == null) {
|
if (client == null || client.player == null || client.getFramebuffer() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -46,24 +55,24 @@ public class QRCodeScanner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String result = decodeQRCode(nativeImageToBufferedImage(nativeImage));
|
int width = nativeImage.getWidth();
|
||||||
long scanNs = System.nanoTime();
|
int height = nativeImage.getHeight();
|
||||||
boolean didLag = (scanNs - startNs) > 50_000_000L;
|
int[] pixels = nativeImage.copyPixelsArgb();
|
||||||
client.execute(() -> {
|
CompletableFuture.supplyAsync(() -> decodeQrCode(width, height, pixels), DECODER)
|
||||||
if (client.player == null) {
|
.whenComplete((result, throwable) -> {
|
||||||
return;
|
client.execute(() -> showResult(client, notifications, result, throwable));
|
||||||
}
|
recordQrEvent(
|
||||||
if (result == null) {
|
throwable == null && result != null,
|
||||||
notifications.show(Text.translatable("message.spmega.qr.not_found"));
|
System.nanoTime() - startNs > 50_000_000L,
|
||||||
return;
|
result,
|
||||||
}
|
throwable == null ? null : throwable.getClass().getSimpleName() + ": " + throwable.getMessage(),
|
||||||
|
startNs
|
||||||
Text clickableLink = Text.literal(result)
|
);
|
||||||
.styled(style -> style.withInsertion(result));
|
});
|
||||||
client.player.sendMessage(Text.translatable("message.spmega.qr.found_link", clickableLink), false);
|
} catch (Exception exception) {
|
||||||
client.setScreen(new QRcodeAcceptScreen(result, client.currentScreen));
|
client.execute(() -> notifications.show(Text.translatable("message.spmega.qr.error")));
|
||||||
});
|
recordQrEvent(false, false, null,
|
||||||
recordQrEvent(result != null, didLag, result, null, startNs);
|
exception.getClass().getSimpleName() + ": " + exception.getMessage(), startNs);
|
||||||
} finally {
|
} finally {
|
||||||
if (nativeImage != null) {
|
if (nativeImage != null) {
|
||||||
nativeImage.close();
|
nativeImage.close();
|
||||||
@@ -76,6 +85,29 @@ public class QRCodeScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void showResult(
|
||||||
|
MinecraftClient client,
|
||||||
|
UiNotifications notifications,
|
||||||
|
String result,
|
||||||
|
Throwable throwable
|
||||||
|
) {
|
||||||
|
if (client.player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (throwable != null) {
|
||||||
|
notifications.show(Text.translatable("message.spmega.qr.error"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
notifications.show(Text.translatable("message.spmega.qr.not_found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text clickableLink = Text.literal(result).styled(style -> style.withInsertion(result));
|
||||||
|
client.player.sendMessage(Text.translatable("message.spmega.qr.found_link", clickableLink), false);
|
||||||
|
client.setScreen(new QRcodeAcceptScreen(result, client.currentScreen));
|
||||||
|
}
|
||||||
|
|
||||||
private static void recordQrEvent(boolean success, boolean didLag, String decodedUrl, String error, long startNs) {
|
private static void recordQrEvent(boolean success, boolean didLag, String decodedUrl, String error, long startNs) {
|
||||||
com.google.gson.JsonObject payload = new com.google.gson.JsonObject();
|
com.google.gson.JsonObject payload = new com.google.gson.JsonObject();
|
||||||
payload.addProperty("success", success);
|
payload.addProperty("success", success);
|
||||||
@@ -90,31 +122,14 @@ public class QRCodeScanner {
|
|||||||
TelemetryCollector.instance().record(TelemetryEvent.now("qr_scan", payload));
|
TelemetryCollector.instance().record(TelemetryEvent.now("qr_scan", payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BufferedImage nativeImageToBufferedImage(NativeImage screenshot) {
|
private static String decodeQrCode(int width, int height, int[] pixels) {
|
||||||
BufferedImage bufferedImage = new BufferedImage(
|
|
||||||
screenshot.getWidth(),
|
|
||||||
screenshot.getHeight(),
|
|
||||||
BufferedImage.TYPE_INT_RGB
|
|
||||||
);
|
|
||||||
|
|
||||||
for (int y = 0; y < screenshot.getHeight(); y++) {
|
|
||||||
for (int x = 0; x < screenshot.getWidth(); x++) {
|
|
||||||
int color = screenshot.getColorArgb(x, y);
|
|
||||||
bufferedImage.setRGB(x, y, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bufferedImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String decodeQRCode(BufferedImage image) {
|
|
||||||
Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
|
Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
|
||||||
hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
|
hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
|
||||||
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
|
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
|
||||||
hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
|
hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
|
||||||
hints.put(DecodeHintType.ALSO_INVERTED, Boolean.TRUE);
|
hints.put(DecodeHintType.ALSO_INVERTED, Boolean.TRUE);
|
||||||
|
|
||||||
LuminanceSource source = new RGBLuminanceSource(image.getWidth(), image.getHeight(), image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth()));
|
LuminanceSource source = new RGBLuminanceSource(width, height, pixels);
|
||||||
|
|
||||||
String result = tryDecodeWithStrategies(source, hints);
|
String result = tryDecodeWithStrategies(source, hints);
|
||||||
|
|
||||||
|
|||||||
+2
-52
@@ -2,7 +2,6 @@ package git.yawaflua.tech.spmega.client.telemetry;
|
|||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
@@ -36,38 +35,8 @@ public record InstrumentedHttpClient(HttpClient delegate) {
|
|||||||
TelemetryCollector.instance().record(TelemetryEvent.now("http_request", payload));
|
TelemetryCollector.instance().record(TelemetryEvent.now("http_request", payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResponse<String> send(HttpRequest request) throws IOException, InterruptedException {
|
|
||||||
URI uri = request.uri();
|
|
||||||
String target = TelemetryUriSanitizer.classifyTarget(uri);
|
|
||||||
String sanitizedPath = TelemetryUriSanitizer.sanitize(uri);
|
|
||||||
String method = request.method();
|
|
||||||
|
|
||||||
int fpsBefore = PerformanceSampler.instance().currentFps();
|
|
||||||
long startNs = System.nanoTime();
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpResponse<String> response = delegate.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
|
||||||
int fpsAfter = PerformanceSampler.instance().currentFps();
|
|
||||||
|
|
||||||
recordEvent(target, method, sanitizedPath, response.statusCode(), durationMs,
|
|
||||||
true, null, fpsBefore, fpsAfter);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (IOException | InterruptedException e) {
|
|
||||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
|
||||||
int fpsAfter = PerformanceSampler.instance().currentFps();
|
|
||||||
|
|
||||||
recordEvent(target, method, sanitizedPath, -1, durationMs,
|
|
||||||
false, e.getClass().getSimpleName(), fpsBefore, fpsAfter);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<HttpResponse<String>> sendAsync(HttpRequest request) {
|
public CompletableFuture<HttpResponse<String>> sendAsync(HttpRequest request) {
|
||||||
URI uri = request.uri();
|
URI uri = request.uri();
|
||||||
String target = TelemetryUriSanitizer.classifyTarget(uri);
|
|
||||||
String sanitizedPath = TelemetryUriSanitizer.sanitize(uri);
|
|
||||||
String method = request.method();
|
String method = request.method();
|
||||||
|
|
||||||
int fpsBefore = PerformanceSampler.instance().currentFps();
|
int fpsBefore = PerformanceSampler.instance().currentFps();
|
||||||
@@ -79,31 +48,12 @@ public record InstrumentedHttpClient(HttpClient delegate) {
|
|||||||
int fpsAfter = PerformanceSampler.instance().currentFps();
|
int fpsAfter = PerformanceSampler.instance().currentFps();
|
||||||
|
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
recordEvent(target, method, sanitizedPath, -1, durationMs,
|
recordHttpEvent(uri, method, -1, durationMs,
|
||||||
false, throwable.getClass().getSimpleName(), fpsBefore, fpsAfter);
|
false, throwable.getClass().getSimpleName(), fpsBefore, fpsAfter);
|
||||||
} else {
|
} else {
|
||||||
recordEvent(target, method, sanitizedPath, response.statusCode(), durationMs,
|
recordHttpEvent(uri, method, response.statusCode(), durationMs,
|
||||||
true, null, fpsBefore, fpsAfter);
|
true, null, fpsBefore, fpsAfter);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recordEvent(String target, String method, String path, int statusCode,
|
|
||||||
long durationMs, boolean success, String errorType,
|
|
||||||
int fpsBefore, int fpsAfter) {
|
|
||||||
JsonObject payload = new JsonObject();
|
|
||||||
payload.addProperty("target", target);
|
|
||||||
payload.addProperty("method", method);
|
|
||||||
payload.addProperty("path", path);
|
|
||||||
payload.addProperty("statusCode", statusCode);
|
|
||||||
payload.addProperty("durationMs", durationMs);
|
|
||||||
payload.addProperty("success", success);
|
|
||||||
if (errorType != null) {
|
|
||||||
payload.addProperty("errorType", errorType);
|
|
||||||
}
|
|
||||||
payload.addProperty("fpsBefore", fpsBefore);
|
|
||||||
payload.addProperty("fpsAfter", fpsAfter);
|
|
||||||
|
|
||||||
TelemetryCollector.instance().record(TelemetryEvent.now("http_request", payload));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-19
@@ -8,12 +8,10 @@ import java.net.InetAddress;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
import java.nio.file.FileStore;
|
import java.nio.file.FileStore;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
public final class SystemInfoCollector {
|
public final class SystemInfoCollector {
|
||||||
@@ -38,23 +36,21 @@ public final class SystemInfoCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void fetchIpAsync() {
|
private void fetchIpAsync() {
|
||||||
CompletableFuture.runAsync(() -> {
|
HttpClient.newBuilder()
|
||||||
try {
|
.connectTimeout(Duration.ofSeconds(5))
|
||||||
HttpClient client = HttpClient.newBuilder()
|
.build()
|
||||||
.connectTimeout(Duration.ofSeconds(5))
|
.sendAsync(HttpRequest.newBuilder()
|
||||||
.build();
|
.uri(URI.create("https://api.ipify.org?format=text"))
|
||||||
HttpRequest req = HttpRequest.newBuilder()
|
.timeout(Duration.ofSeconds(5))
|
||||||
.uri(URI.create("https://api.ipify.org?format=text"))
|
.GET()
|
||||||
.timeout(Duration.ofSeconds(5))
|
.build(),
|
||||||
.GET()
|
java.net.http.HttpResponse.BodyHandlers.ofString())
|
||||||
.build();
|
.thenAccept(response -> {
|
||||||
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
|
if (response.statusCode() == 200) {
|
||||||
if (resp.statusCode() == 200) {
|
cachedIp.set(response.body().trim());
|
||||||
cachedIp.set(resp.body().trim());
|
}
|
||||||
}
|
})
|
||||||
} catch (Exception ignored) {
|
.exceptionally(ignored -> null);
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectGpuOnRenderThread() {
|
private void collectGpuOnRenderThread() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package git.yawaflua.tech.spmega.client.ui;
|
|||||||
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
||||||
import git.yawaflua.tech.spmega.client.ui.service.CardViewModel;
|
import git.yawaflua.tech.spmega.client.ui.service.CardViewModel;
|
||||||
import net.minecraft.client.gui.DrawContext;
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.ConfirmScreen;
|
||||||
import net.minecraft.client.gui.screen.Screen;
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
import net.minecraft.client.gui.widget.ButtonWidget;
|
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||||
import net.minecraft.text.Text;
|
import net.minecraft.text.Text;
|
||||||
@@ -16,6 +17,7 @@ public class CardScreen extends Screen {
|
|||||||
private final UiNotifications notifications = UiNotifications.instance();
|
private final UiNotifications notifications = UiNotifications.instance();
|
||||||
private final List<ButtonWidget> cardButtons = new ArrayList<>();
|
private final List<ButtonWidget> cardButtons = new ArrayList<>();
|
||||||
private ButtonWidget historyButton;
|
private ButtonWidget historyButton;
|
||||||
|
private ButtonWidget webhookButton;
|
||||||
|
|
||||||
public CardScreen(Screen parent) {
|
public CardScreen(Screen parent) {
|
||||||
super(Text.literal("Управление картами"));
|
super(Text.literal("Управление картами"));
|
||||||
@@ -93,8 +95,30 @@ public class CardScreen extends Screen {
|
|||||||
this.client.setScreen(new TransactionHistoryScreen(this, selected.id(), selected.title()));
|
this.client.setScreen(new TransactionHistoryScreen(this, selected.id(), selected.title()));
|
||||||
}).dimensions(rightX, startY + 72, columnWidth, 20).build());
|
}).dimensions(rightX, startY + 72, columnWidth, 20).build());
|
||||||
|
|
||||||
|
webhookButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Включить вебхуки"), button -> {
|
||||||
|
if (this.client == null || bankUiService.getSelectedCard() == null) {
|
||||||
|
notifications.show(Text.literal("Сначала выбери или добавь карту"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.client.setScreen(new ConfirmScreen(confirmed -> {
|
||||||
|
if (this.client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.client.setScreen(this);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bankUiService.registerSelectedWebhookAsync().thenAccept(message -> this.client.execute(() -> {
|
||||||
|
notifications.showMessage(message);
|
||||||
|
this.clearAndInit();
|
||||||
|
}));
|
||||||
|
}, Text.literal("Включить обработку вебхуков?"),
|
||||||
|
Text.literal("Если другой вебхук уже подключен к карте — он может затереться."),
|
||||||
|
Text.literal("Включить"), Text.literal("Отмена")));
|
||||||
|
}).dimensions(rightX, startY + 96, columnWidth, 20).build());
|
||||||
|
|
||||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
|
this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
|
||||||
.dimensions(rightX, startY + 120, columnWidth, 20)
|
.dimensions(rightX, startY + 144, columnWidth, 20)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
if (cards.isEmpty()) {
|
if (cards.isEmpty()) {
|
||||||
@@ -147,5 +171,11 @@ public class CardScreen extends Screen {
|
|||||||
if (historyButton != null) {
|
if (historyButton != null) {
|
||||||
historyButton.active = !cards.isEmpty();
|
historyButton.active = !cards.isEmpty();
|
||||||
}
|
}
|
||||||
|
if (webhookButton != null) {
|
||||||
|
CardViewModel selected = bankUiService.getSelectedCard();
|
||||||
|
boolean enabled = selected != null && selected.webhookEnabled();
|
||||||
|
webhookButton.active = selected != null && !enabled;
|
||||||
|
webhookButton.setMessage(Text.literal(enabled ? "Вебхуки включены" : "Включить вебхуки"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ import java.util.concurrent.TimeUnit;
|
|||||||
|
|
||||||
public final class GpsHudRenderer {
|
public final class GpsHudRenderer {
|
||||||
private static final GpsHudRenderer INSTANCE = new GpsHudRenderer();
|
private static final GpsHudRenderer INSTANCE = new GpsHudRenderer();
|
||||||
private final Object citiesLock = new Object();
|
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
private List<City> cities = new ArrayList<>();
|
private volatile List<City> cities = List.of();
|
||||||
|
|
||||||
private GpsHudRenderer() {
|
private GpsHudRenderer() {
|
||||||
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
@@ -50,33 +49,35 @@ public final class GpsHudRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void fetchCities() {
|
private void fetchCities() {
|
||||||
try {
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
InstrumentedHttpClient client = new InstrumentedHttpClient();
|
.uri(URI.create("https://map.sp-mini.ru/api/map/territories"))
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
.GET()
|
||||||
.uri(URI.create("https://map.sp-mini.ru/api/map/territories"))
|
.build();
|
||||||
.GET()
|
|
||||||
.build();
|
new InstrumentedHttpClient().sendAsync(request)
|
||||||
var response = client.send(request);
|
.thenAccept(response -> {
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() != 200) {
|
||||||
TerritoryWrapper[] parsed = new Gson().fromJson(response.body(), TerritoryWrapper[].class);
|
return;
|
||||||
if (parsed != null) {
|
}
|
||||||
List<City> list = new ArrayList<>();
|
TerritoryWrapper[] parsed = new Gson().fromJson(response.body(), TerritoryWrapper[].class);
|
||||||
for (TerritoryWrapper w : parsed) {
|
if (parsed == null) {
|
||||||
if (w != null && w.territory != null && w.territory.nether_portal != null && w.territory.nether_portal.length >= 2) {
|
return;
|
||||||
City c = new City();
|
}
|
||||||
c.name = w.territory.name;
|
List<City> updatedCities = new ArrayList<>();
|
||||||
c.netherX = w.territory.nether_portal[0];
|
for (TerritoryWrapper wrapper : parsed) {
|
||||||
c.netherZ = w.territory.nether_portal[1];
|
if (wrapper != null && wrapper.territory != null
|
||||||
list.add(c);
|
&& wrapper.territory.nether_portal != null
|
||||||
|
&& wrapper.territory.nether_portal.length >= 2) {
|
||||||
|
City city = new City();
|
||||||
|
city.name = wrapper.territory.name;
|
||||||
|
city.netherX = wrapper.territory.nether_portal[0];
|
||||||
|
city.netherZ = wrapper.territory.nether_portal[1];
|
||||||
|
updatedCities.add(city);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
synchronized (citiesLock) {
|
cities = List.copyOf(updatedCities);
|
||||||
cities = list;
|
})
|
||||||
}
|
.exceptionally(ignored -> null);
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void render(DrawContext context, TextRenderer textRenderer, int width, int height) {
|
public void render(DrawContext context, TextRenderer textRenderer, int width, int height) {
|
||||||
@@ -102,13 +103,13 @@ public final class GpsHudRenderer {
|
|||||||
String offsetText = "§7Смещение: §f" + (playerLane.offset == 0 ? "0" : (playerLane.offset > 0 ? "+" + playerLane.offset : playerLane.offset));
|
String offsetText = "§7Смещение: §f" + (playerLane.offset == 0 ? "0" : (playerLane.offset > 0 ? "+" + playerLane.offset : playerLane.offset));
|
||||||
|
|
||||||
City nearestCity = null;
|
City nearestCity = null;
|
||||||
double minDistanceSq = Double.MAX_VALUE;
|
double minDistanceSquared = Double.MAX_VALUE;
|
||||||
for (City city : cities) {
|
for (City city : cities) {
|
||||||
double dx = playerX - (city.netherX / 8);
|
double dx = playerX - (city.netherX / 8);
|
||||||
double dz = playerZ - (city.netherZ / 8);
|
double dz = playerZ - (city.netherZ / 8);
|
||||||
double distSq = Math.sqrt(dx * dx + dz * dz);
|
double distanceSquared = dx * dx + dz * dz;
|
||||||
if (distSq < minDistanceSq) {
|
if (distanceSquared < minDistanceSquared) {
|
||||||
minDistanceSq = distSq;
|
minDistanceSquared = distanceSquared;
|
||||||
nearestCity = city;
|
nearestCity = city;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class MainBankScreen extends Screen {
|
|||||||
|
|
||||||
this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.scan_qr"), button -> {
|
this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.scan_qr"), button -> {
|
||||||
this.client.setScreen(null);
|
this.client.setScreen(null);
|
||||||
QRCodeScanner.ScanQrCode(this.client);
|
QRCodeScanner.scanQrCode(this.client);
|
||||||
}).dimensions(centerX - 60, y + 28, 120, 20).build());
|
}).dimensions(centerX - 60, y + 28, 120, 20).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,50 +107,47 @@ public class TransactionHistoryScreen extends Screen {
|
|||||||
|
|
||||||
UiNotifications.instance().show(Text.literal("Загрузка..."));
|
UiNotifications.instance().show(Text.literal("Загрузка..."));
|
||||||
|
|
||||||
CompletableFuture.runAsync(() -> {
|
int page = currentPage;
|
||||||
try {
|
System.out.println("[SPMEGA] Transaction history loading started for card: " + cardId + ", page: " + page);
|
||||||
System.out.println("[SPMEGA] Transaction history loading started for card: " + cardId + ", page: " + currentPage);
|
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
|
||||||
List<LocalTransaction> list = null;
|
CompletableFuture<List<LocalTransaction>> remote = config != null && config.allowBackend()
|
||||||
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
|
? BackendAuthenticator.fetchTransactionsFromBackendAsync(cardId, page)
|
||||||
if (config != null && config.allowBackend()) {
|
.whenComplete((list, exception) -> {
|
||||||
try {
|
if (exception == null) {
|
||||||
list = BackendAuthenticator.fetchTransactionsFromBackend(cardId, currentPage);
|
System.out.println("[SPMEGA] Fetched " + list.size() + " transactions from backend.");
|
||||||
System.out.println("[SPMEGA] Fetched " + (list != null ? list.size() : "null") + " transactions from backend.");
|
} else {
|
||||||
} catch (Exception e) {
|
System.err.println("[SPMEGA] Failed to fetch transactions from server, falling back to DB: "
|
||||||
System.err.println("[SPMEGA] Failed to fetch transactions from server, falling back to DB: " + e.getMessage());
|
+ exception.getMessage());
|
||||||
e.printStackTrace();
|
}
|
||||||
|
})
|
||||||
|
.exceptionally(exception -> null)
|
||||||
|
: CompletableFuture.completedFuture(null);
|
||||||
|
|
||||||
|
remote.thenApply(list -> {
|
||||||
|
if (list != null) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
List<LocalTransaction> local = BankUiService.instance().getDatabase().loadTransferHistory(cardId);
|
||||||
|
System.out.println("[SPMEGA] Loaded " + local.size() + " transactions from database.");
|
||||||
|
return local;
|
||||||
|
})
|
||||||
|
.whenComplete((list, exception) -> {
|
||||||
|
MinecraftClient minecraftClient = MinecraftClient.getInstance();
|
||||||
|
if (minecraftClient == null) {
|
||||||
|
System.out.println("[SPMEGA] MinecraftClient.getInstance() is null in loadTransactions callback!");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (list == null) {
|
|
||||||
list = BankUiService.instance().getDatabase().loadTransferHistory(cardId);
|
|
||||||
System.out.println("[SPMEGA] Loaded " + (list != null ? list.size() : "null") + " transactions from database.");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<LocalTransaction> finalList = list != null ? list : new ArrayList<>();
|
|
||||||
MinecraftClient minecraftClient = MinecraftClient.getInstance();
|
|
||||||
if (minecraftClient != null) {
|
|
||||||
minecraftClient.execute(() -> {
|
minecraftClient.execute(() -> {
|
||||||
System.out.println("[SPMEGA] Setting transactions list on main thread. Count: " + finalList.size());
|
if (exception != null) {
|
||||||
this.transactions.addAll(finalList);
|
errorMessage = exception.getMessage();
|
||||||
this.scrollOffset = 0;
|
} else {
|
||||||
this.loading = false;
|
System.out.println("[SPMEGA] Setting transactions list on main thread. Count: " + list.size());
|
||||||
|
transactions.addAll(list);
|
||||||
|
scrollOffset = 0;
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
System.out.println("[SPMEGA] MinecraftClient.getInstance() is null in loadTransactions callback!");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("[SPMEGA] Outer exception in loadTransactions: " + e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
MinecraftClient minecraftClient = MinecraftClient.getInstance();
|
|
||||||
if (minecraftClient != null) {
|
|
||||||
minecraftClient.execute(() -> {
|
|
||||||
this.errorMessage = e.getMessage();
|
|
||||||
this.loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -5,17 +5,22 @@ import com.google.gson.JsonObject;
|
|||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
import com.google.gson.Strictness;
|
import com.google.gson.Strictness;
|
||||||
import com.google.gson.stream.JsonReader;
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import git.yawaflua.tech.spmega.GpsHudPosition;
|
||||||
|
import git.yawaflua.tech.spmega.SPMega;
|
||||||
import net.minecraft.client.font.TextRenderer;
|
import net.minecraft.client.font.TextRenderer;
|
||||||
import net.minecraft.client.gui.DrawContext;
|
import net.minecraft.client.gui.DrawContext;
|
||||||
import net.minecraft.text.Text;
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
public final class UiNotifications {
|
public final class UiNotifications {
|
||||||
private static final int DEFAULT_DURATION_TICKS = 70;
|
private static final int DEFAULT_DURATION_TICKS = 70;
|
||||||
private static final UiNotifications INSTANCE = new UiNotifications();
|
private static final UiNotifications INSTANCE = new UiNotifications();
|
||||||
|
|
||||||
private Text currentText = Text.empty();
|
private Text currentText = Text.empty();
|
||||||
|
private final Queue<Text> queuedTexts = new ArrayDeque<>();
|
||||||
private int remainingTicks;
|
private int remainingTicks;
|
||||||
|
|
||||||
private UiNotifications() {
|
private UiNotifications() {
|
||||||
@@ -62,6 +67,17 @@ public final class UiNotifications {
|
|||||||
show(Text.literal(extractMessage(message)));
|
show(Text.literal(extractMessage(message)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void showQueued(Text text) {
|
||||||
|
if (text == null || text.getString().isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isVisible()) {
|
||||||
|
show(text);
|
||||||
|
} else {
|
||||||
|
queuedTexts.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized void render(DrawContext context, TextRenderer textRenderer, int width, int height) {
|
public synchronized void render(DrawContext context, TextRenderer textRenderer, int width, int height) {
|
||||||
if (!isVisible()) {
|
if (!isVisible()) {
|
||||||
return;
|
return;
|
||||||
@@ -72,8 +88,18 @@ public final class UiNotifications {
|
|||||||
int textWidth = textRenderer.getWidth(message);
|
int textWidth = textRenderer.getWidth(message);
|
||||||
int boxWidth = textWidth + padding * 2;
|
int boxWidth = textWidth + padding * 2;
|
||||||
int boxHeight = textRenderer.fontHeight + padding * 2;
|
int boxHeight = textRenderer.fontHeight + padding * 2;
|
||||||
int x = width - boxWidth - 10;
|
GpsHudPosition position = SPMega.getConfig() == null
|
||||||
int y = height - boxHeight - 10;
|
? GpsHudPosition.BOTTOM_RIGHT
|
||||||
|
: SPMega.getConfig().notificationPosition();
|
||||||
|
int x = switch (position) {
|
||||||
|
case TOP_LEFT, BOTTOM_LEFT -> 10;
|
||||||
|
case TOP_CENTER, BOTTOM_CENTER -> (width - boxWidth) / 2;
|
||||||
|
case TOP_RIGHT, BOTTOM_RIGHT -> width - boxWidth - 10;
|
||||||
|
};
|
||||||
|
int y = switch (position) {
|
||||||
|
case TOP_LEFT, TOP_CENTER, TOP_RIGHT -> 10;
|
||||||
|
case BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT -> height - boxHeight - 10;
|
||||||
|
};
|
||||||
|
|
||||||
context.fill(x, y, x + boxWidth, y + boxHeight, Color.DARK_GRAY.getRGB());
|
context.fill(x, y, x + boxWidth, y + boxHeight, Color.DARK_GRAY.getRGB());
|
||||||
context.drawTextWithShadow(textRenderer, currentText, x + padding, y + padding, Color.WHITE.getRGB());
|
context.drawTextWithShadow(textRenderer, currentText, x + padding, y + padding, Color.WHITE.getRGB());
|
||||||
@@ -85,8 +111,8 @@ public final class UiNotifications {
|
|||||||
}
|
}
|
||||||
remainingTicks--;
|
remainingTicks--;
|
||||||
if (remainingTicks <= 0) {
|
if (remainingTicks <= 0) {
|
||||||
currentText = Text.empty();
|
currentText = queuedTexts.isEmpty() ? Text.empty() : queuedTexts.remove();
|
||||||
remainingTicks = 0;
|
remainingTicks = currentText.getString().isBlank() ? 0 : DEFAULT_DURATION_TICKS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+194
-104
@@ -18,43 +18,48 @@ import java.net.http.HttpResponse;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
|
||||||
public class BackendAuthenticator {
|
public final class BackendAuthenticator {
|
||||||
|
|
||||||
public static boolean authenticate(MinecraftClient client) {
|
private BackendAuthenticator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Boolean> authenticateAsync(MinecraftClient client) {
|
||||||
Session session = client.getSession();
|
Session session = client.getSession();
|
||||||
if (session == null || session.getUuidOrNull() == null) {
|
if (session == null || session.getUuidOrNull() == null) {
|
||||||
System.err.println("Cannot authenticate: Client has no valid session.");
|
System.err.println("Cannot authenticate: Client has no valid session.");
|
||||||
return false;
|
return CompletableFuture.completedFuture(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
MinecraftSessionService sessionService = client.getApiServices().sessionService();
|
MinecraftSessionService sessionService = client.getApiServices().sessionService();
|
||||||
|
return sendStartSessionRequestToBackendAsync(session.getUsername(), session.getUuidOrNull())
|
||||||
try {
|
.thenCompose(serverId -> CompletableFuture.runAsync(() -> {
|
||||||
var serverId = sendStartSessionRequestToBackend(session.getUsername(), session.getUuidOrNull());
|
System.out.println("[SPMEGA] Trying to auth in mojang with serverId: " + serverId);
|
||||||
System.out.println("[SPMEGA] Trying to auth in mojang with serverId: " + serverId);
|
try {
|
||||||
sessionService.joinServer(
|
sessionService.joinServer(session.getUuidOrNull(), session.getAccessToken(), serverId);
|
||||||
session.getUuidOrNull(),
|
} catch (AuthenticationException exception) {
|
||||||
session.getAccessToken(),
|
throw new CompletionException(exception);
|
||||||
serverId
|
}
|
||||||
);
|
})
|
||||||
System.out.println("[SPMEGA] Sending session submitter to backend");
|
.thenCompose(ignored -> {
|
||||||
|
System.out.println("[SPMEGA] Sending session submitter to backend");
|
||||||
return sendAuthRequestToBackend(session.getUuidOrNull(), serverId);
|
return sendAuthRequestToBackendAsync(session.getUuidOrNull(), serverId);
|
||||||
|
}))
|
||||||
} catch (AuthenticationException e) {
|
.exceptionally(throwable -> {
|
||||||
System.err.println("I cant auth by Mojang: " + e.getMessage());
|
Throwable cause = throwable.getCause() == null ? throwable : throwable.getCause();
|
||||||
System.err.println("Please check your credentials and try again.");
|
if (cause instanceof AuthenticationException) {
|
||||||
return false;
|
System.err.println("I cant auth by Mojang: " + cause.getMessage());
|
||||||
|
System.err.println("Please check your credentials and try again.");
|
||||||
} catch (Exception e) {
|
} else {
|
||||||
System.err.println("Failed to authenticate with backend: " + e.getMessage());
|
System.err.println("Failed to authenticate with backend: " + cause.getMessage());
|
||||||
return false;
|
}
|
||||||
|
return false;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String sendStartSessionRequestToBackend(String username, UUID uuid) throws IOException, InterruptedException {
|
private static CompletableFuture<String> sendStartSessionRequestToBackendAsync(String username, UUID uuid) {
|
||||||
ModConfig config = SPMega.getConfig();
|
ModConfig config = SPMega.getConfig();
|
||||||
String apiDomain = (config != null) ? config.apiDomain() : ModConfig.DEFAULT_API_DOMAIN;
|
String apiDomain = (config != null) ? config.apiDomain() : ModConfig.DEFAULT_API_DOMAIN;
|
||||||
if (apiDomain == null || apiDomain.isBlank()) {
|
if (apiDomain == null || apiDomain.isBlank()) {
|
||||||
@@ -78,21 +83,22 @@ public class BackendAuthenticator {
|
|||||||
.method("POST", HttpRequest.BodyPublishers.ofString(requestBody))
|
.method("POST", HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request);
|
return httpClient.sendAsync(request).thenApply(response -> {
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on start: " + response.body());
|
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on start: " + response.body());
|
||||||
throw new IOException("Server returned status code " + response.statusCode() + " on start: " + response.body());
|
throw new CompletionException(new IOException(
|
||||||
}
|
"Server returned status code " + response.statusCode() + " on start: " + response.body()));
|
||||||
|
}
|
||||||
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
|
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
|
||||||
if (!json.has("sessionId")) {
|
if (!json.has("sessionId")) {
|
||||||
System.err.println("[SPMEGA] Invalid response from start endpoint: " + response.body());
|
System.err.println("[SPMEGA] Invalid response from start endpoint: " + response.body());
|
||||||
throw new IOException("Invalid response from start endpoint: " + response.body());
|
throw new CompletionException(new IOException("Invalid response from start endpoint: " + response.body()));
|
||||||
}
|
}
|
||||||
return json.get("sessionId").getAsString();
|
return json.get("sessionId").getAsString();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean sendAuthRequestToBackend(UUID uuid, String serverId) throws IOException, InterruptedException {
|
private static CompletableFuture<Boolean> sendAuthRequestToBackendAsync(UUID uuid, String serverId) {
|
||||||
ModConfig config = SPMega.getConfig();
|
ModConfig config = SPMega.getConfig();
|
||||||
String apiDomain = (config != null) ? config.apiDomain() : ModConfig.DEFAULT_API_DOMAIN;
|
String apiDomain = (config != null) ? config.apiDomain() : ModConfig.DEFAULT_API_DOMAIN;
|
||||||
if (apiDomain == null || apiDomain.isBlank()) {
|
if (apiDomain == null || apiDomain.isBlank()) {
|
||||||
@@ -116,37 +122,30 @@ public class BackendAuthenticator {
|
|||||||
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request);
|
return httpClient.sendAsync(request).thenApply(response -> {
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on validate: " + response.body());
|
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on validate: " + response.body());
|
||||||
throw new IOException("Server returned status code " + response.statusCode() + " on validate: " + response.body());
|
throw new CompletionException(new IOException(
|
||||||
}
|
"Server returned status code " + response.statusCode() + " on validate: " + response.body()));
|
||||||
|
}
|
||||||
|
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
|
||||||
|
if (!json.has("token")) {
|
||||||
|
System.err.println("[SPMEGA] Invalid response from validate endpoint: " + response.body());
|
||||||
|
throw new CompletionException(new IOException("Invalid response from validate endpoint: " + response.body()));
|
||||||
|
}
|
||||||
|
if (config == null) {
|
||||||
|
System.err.println("[SPMEGA] Config is null, cannot save token.");
|
||||||
|
throw new CompletionException(new IOException("Config is null, cannot save token."));
|
||||||
|
}
|
||||||
|
|
||||||
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
|
String token = json.get("token").getAsString();
|
||||||
if (!json.has("token")) {
|
SPMega.setConfig(new ModConfig(
|
||||||
System.err.println("[SPMEGA] Invalid response from validate endpoint: " + response.body());
|
config.apiDomain(), token, config.allowBackend(), config.signQuickPayEnabled(),
|
||||||
throw new IOException("Invalid response from validate endpoint: " + response.body());
|
config.gpsEnabled(), config.gpsPosition(), config.notificationPosition(), config.telemetryEnabled(),
|
||||||
}
|
config.telemetryIntervalSeconds(), config.telemetryCollectSystemInfo()));
|
||||||
if (config == null) {
|
System.out.println("[SPMEGA] Backend auth successful, saved token.");
|
||||||
System.err.println("[SPMEGA] Config is null, cannot save token.");
|
return true;
|
||||||
throw new IOException("Config is null, cannot save token.");
|
});
|
||||||
}
|
|
||||||
|
|
||||||
String token = json.get("token").getAsString();
|
|
||||||
ModConfig updated = new ModConfig(
|
|
||||||
config.apiDomain(),
|
|
||||||
token,
|
|
||||||
config.allowBackend(),
|
|
||||||
config.signQuickPayEnabled(),
|
|
||||||
config.gpsEnabled(),
|
|
||||||
config.gpsPosition(),
|
|
||||||
config.telemetryEnabled(),
|
|
||||||
config.telemetryIntervalSeconds(),
|
|
||||||
config.telemetryCollectSystemInfo()
|
|
||||||
);
|
|
||||||
SPMega.setConfig(updated);
|
|
||||||
System.out.println("[SPMEGA] Backend auth successful, saved token.");
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void sendCardToBackend(String cardId, String cardToken) {
|
public static void sendCardToBackend(String cardId, String cardToken) {
|
||||||
@@ -193,10 +192,10 @@ public class BackendAuthenticator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<CardCredentials> fetchCardsFromBackend() throws IOException, InterruptedException {
|
public static CompletableFuture<List<CardCredentials>> fetchCardsFromBackendAsync() {
|
||||||
ModConfig config = SPMega.getConfig();
|
ModConfig config = SPMega.getConfig();
|
||||||
if (config == null || !config.allowBackend()) {
|
if (config == null || !config.allowBackend()) {
|
||||||
return List.of();
|
return CompletableFuture.completedFuture(List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
String apiDomain = config.apiDomain();
|
String apiDomain = config.apiDomain();
|
||||||
@@ -216,28 +215,29 @@ public class BackendAuthenticator {
|
|||||||
.GET()
|
.GET()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request);
|
return httpClient.sendAsync(request).thenApply(response -> {
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
throw new IOException("Server returned status code " + response.statusCode() + " on fetch cards.");
|
throw new CompletionException(new IOException(
|
||||||
}
|
"Server returned status code " + response.statusCode() + " on fetch cards."));
|
||||||
|
|
||||||
com.google.gson.JsonArray cardsArray = JsonParser.parseString(response.body()).getAsJsonArray();
|
|
||||||
List<CardCredentials> cards = new ArrayList<>();
|
|
||||||
for (com.google.gson.JsonElement el : cardsArray) {
|
|
||||||
JsonObject cardJson = el.getAsJsonObject();
|
|
||||||
String cardId = cardJson.has("cardId") && !cardJson.get("cardId").isJsonNull()
|
|
||||||
? cardJson.get("cardId").getAsString()
|
|
||||||
: (cardJson.has("id") && !cardJson.get("id").isJsonNull() ? cardJson.get("id").getAsString() : "");
|
|
||||||
|
|
||||||
String cardToken = cardJson.has("cardToken") && !cardJson.get("cardToken").isJsonNull()
|
|
||||||
? cardJson.get("cardToken").getAsString()
|
|
||||||
: (cardJson.has("token") && !cardJson.get("token").isJsonNull() ? cardJson.get("token").getAsString() : "");
|
|
||||||
|
|
||||||
if (!cardId.isEmpty() && !cardToken.isEmpty()) {
|
|
||||||
cards.add(new CardCredentials(cardId, cardToken));
|
|
||||||
}
|
}
|
||||||
}
|
com.google.gson.JsonArray cardsArray = JsonParser.parseString(response.body()).getAsJsonArray();
|
||||||
return cards;
|
List<CardCredentials> cards = new ArrayList<>();
|
||||||
|
for (com.google.gson.JsonElement element : cardsArray) {
|
||||||
|
JsonObject cardJson = element.getAsJsonObject();
|
||||||
|
String cardId = cardJson.has("cardId") && !cardJson.get("cardId").isJsonNull()
|
||||||
|
? cardJson.get("cardId").getAsString()
|
||||||
|
: (cardJson.has("id") && !cardJson.get("id").isJsonNull() ? cardJson.get("id").getAsString() : "");
|
||||||
|
String cardToken = cardJson.has("cardToken") && !cardJson.get("cardToken").isJsonNull()
|
||||||
|
? cardJson.get("cardToken").getAsString()
|
||||||
|
: (cardJson.has("token") && !cardJson.get("token").isJsonNull() ? cardJson.get("token").getAsString() : "");
|
||||||
|
boolean webhookEnabled = cardJson.has("webhookConnected")
|
||||||
|
&& cardJson.get("webhookConnected").getAsBoolean();
|
||||||
|
if (!cardId.isEmpty() && !cardToken.isEmpty()) {
|
||||||
|
cards.add(new CardCredentials(cardId, cardToken, webhookEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void deleteCardOnBackend(String cardId) {
|
public static void deleteCardOnBackend(String cardId) {
|
||||||
@@ -278,10 +278,87 @@ public class BackendAuthenticator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean createTransactionOnBackend(String senderCardId, String receiver, long amount, String comment) throws IOException, InterruptedException {
|
public static CompletableFuture<Void> registerWebhookForCardAsync(String cardId) {
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
if (config == null || !config.allowBackend()) {
|
||||||
|
return CompletableFuture.failedFuture(new IOException("Backend is disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(backendUrl(config, "/api/v1/webhook/" + cardId)))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + config.apiToken())
|
||||||
|
.PUT(HttpRequest.BodyPublishers.noBody())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new InstrumentedHttpClient().sendAsync(request).thenApply(response -> {
|
||||||
|
if (response.statusCode() != 200 && response.statusCode() != 204) {
|
||||||
|
throw new CompletionException(new IOException(
|
||||||
|
"Server returned status code " + response.statusCode() + ": " + response.body()));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<List<PaymentNotification>> readNotificationsAsync() {
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
if (config == null || !config.allowBackend()
|
||||||
|
|| config.apiToken() == null || config.apiToken().isBlank()
|
||||||
|
|| ModConfig.DEFAULT_API_TOKEN.equals(config.apiToken())) {
|
||||||
|
return CompletableFuture.completedFuture(List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(backendUrl(config, "/api/v1/webhook/read")))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + config.apiToken())
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new InstrumentedHttpClient().sendAsync(request).thenApply(response -> {
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw new CompletionException(new IOException(
|
||||||
|
"Server returned status code " + response.statusCode() + " on notification read."));
|
||||||
|
}
|
||||||
|
JsonElement parsed = JsonParser.parseString(response.body());
|
||||||
|
com.google.gson.JsonArray array = parsed.isJsonArray()
|
||||||
|
? parsed.getAsJsonArray()
|
||||||
|
: parsed.getAsJsonObject().getAsJsonArray("$values");
|
||||||
|
if (array == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PaymentNotification> notifications = new ArrayList<>();
|
||||||
|
for (JsonElement element : array) {
|
||||||
|
JsonObject json = element.getAsJsonObject();
|
||||||
|
notifications.add(new PaymentNotification(
|
||||||
|
stringValue(json, "id"),
|
||||||
|
stringValue(json, "senderName"),
|
||||||
|
stringValue(json, "senderNumber"),
|
||||||
|
stringValue(json, "comment"),
|
||||||
|
json.has("amount") ? json.get("amount").getAsInt() : 0,
|
||||||
|
stringValue(json, "type")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return notifications;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String backendUrl(ModConfig config, String path) {
|
||||||
|
String domain = config.apiDomain();
|
||||||
|
if (domain == null || domain.isBlank()) {
|
||||||
|
domain = ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
}
|
||||||
|
return (domain.endsWith("/") ? domain.substring(0, domain.length() - 1) : domain) + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stringValue(JsonObject json, String name) {
|
||||||
|
return json.has(name) && !json.get(name).isJsonNull() ? json.get(name).getAsString() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Boolean> createTransactionOnBackendAsync(
|
||||||
|
String senderCardId, String receiver, long amount, String comment) {
|
||||||
ModConfig config = SPMega.getConfig();
|
ModConfig config = SPMega.getConfig();
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
throw new IOException("ModConfig is null");
|
return CompletableFuture.failedFuture(new IOException("ModConfig is null"));
|
||||||
}
|
}
|
||||||
|
|
||||||
String apiDomain = config.apiDomain();
|
String apiDomain = config.apiDomain();
|
||||||
@@ -311,19 +388,22 @@ public class BackendAuthenticator {
|
|||||||
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
|
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request);
|
return httpClient.sendAsync(request).thenApply(response -> {
|
||||||
if (response.statusCode() != 200 && response.statusCode() != 201 && response.statusCode() != 204) {
|
if (response.statusCode() != 200 && response.statusCode() != 201 && response.statusCode() != 204) {
|
||||||
throw new IOException("Server returned status code " + response.statusCode() + ": " + response.body());
|
throw new CompletionException(new IOException(
|
||||||
}
|
"Server returned status code " + response.statusCode() + ": " + response.body()));
|
||||||
return true;
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<BankDatabase.LocalTransaction> fetchTransactionsFromBackend(String cardId, int page) throws IOException, InterruptedException {
|
public static CompletableFuture<List<BankDatabase.LocalTransaction>> fetchTransactionsFromBackendAsync(
|
||||||
|
String cardId, int page) {
|
||||||
System.out.println("[SPMEGA] fetchTransactionsFromBackend called for cardId: " + cardId + ", page: " + page);
|
System.out.println("[SPMEGA] fetchTransactionsFromBackend called for cardId: " + cardId + ", page: " + page);
|
||||||
ModConfig config = SPMega.getConfig();
|
ModConfig config = SPMega.getConfig();
|
||||||
if (config == null || !config.allowBackend()) {
|
if (config == null || !config.allowBackend()) {
|
||||||
System.out.println("[SPMEGA] fetchTransactionsFromBackend aborted: config is null or backend not allowed.");
|
System.out.println("[SPMEGA] fetchTransactionsFromBackend aborted: config is null or backend not allowed.");
|
||||||
return List.of();
|
return CompletableFuture.completedFuture(List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
String apiDomain = config.apiDomain();
|
String apiDomain = config.apiDomain();
|
||||||
@@ -344,11 +424,17 @@ public class BackendAuthenticator {
|
|||||||
.GET()
|
.GET()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request);
|
return httpClient.sendAsync(request)
|
||||||
|
.thenApply(response -> parseTransactionsResponse(response, cardId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<BankDatabase.LocalTransaction> parseTransactionsResponse(
|
||||||
|
HttpResponse<String> response, String cardId) {
|
||||||
System.out.println("[SPMEGA] Response status code: " + response.statusCode());
|
System.out.println("[SPMEGA] Response status code: " + response.statusCode());
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
System.err.println("[SPMEGA] Failed response body: " + response.body());
|
System.err.println("[SPMEGA] Failed response body: " + response.body());
|
||||||
throw new IOException("Server returned status code " + response.statusCode() + " on fetch transactions.");
|
throw new CompletionException(new IOException(
|
||||||
|
"Server returned status code " + response.statusCode() + " on fetch transactions."));
|
||||||
}
|
}
|
||||||
|
|
||||||
String body = response.body();
|
String body = response.body();
|
||||||
@@ -426,4 +512,8 @@ public class BackendAuthenticator {
|
|||||||
System.out.println("[SPMEGA] Returning " + list.size() + " filtered transactions for card " + cardId);
|
System.out.println("[SPMEGA] Returning " + list.size() + " filtered transactions for card " + cardId);
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record PaymentNotification(
|
||||||
|
String id, String senderName, String senderNumber, String comment, int amount, String type) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,23 @@ public final class BankDatabase {
|
|||||||
card_number TEXT,
|
card_number TEXT,
|
||||||
balance INTEGER NOT NULL DEFAULT 0,
|
balance INTEGER NOT NULL DEFAULT 0,
|
||||||
owner_uuid TEXT,
|
owner_uuid TEXT,
|
||||||
|
webhook_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
""");
|
""");
|
||||||
|
boolean hasWebhookColumn = false;
|
||||||
|
try (ResultSet columns = statement.executeQuery("PRAGMA table_info(cards)")) {
|
||||||
|
while (columns.next()) {
|
||||||
|
if ("webhook_enabled".equals(columns.getString("name"))) {
|
||||||
|
hasWebhookColumn = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasWebhookColumn) {
|
||||||
|
statement.executeUpdate(
|
||||||
|
"ALTER TABLE cards ADD COLUMN webhook_enabled INTEGER NOT NULL DEFAULT 0");
|
||||||
|
}
|
||||||
statement.executeUpdate("""
|
statement.executeUpdate("""
|
||||||
CREATE TABLE IF NOT EXISTS transfer_history (
|
CREATE TABLE IF NOT EXISTS transfer_history (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -100,8 +114,31 @@ public final class BankDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void setWebhookEnabled(String cardId, boolean enabled) {
|
||||||
|
String sql = "UPDATE cards SET webhook_enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE card_id = ?";
|
||||||
|
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
|
statement.setBoolean(1, enabled);
|
||||||
|
statement.setString(2, cardId);
|
||||||
|
statement.executeUpdate();
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
throw new RuntimeException("Failed to update webhook state", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean hasWebhookEnabledCards() {
|
||||||
|
try (Connection connection = open();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"SELECT 1 FROM cards WHERE webhook_enabled = 1 LIMIT 1");
|
||||||
|
ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next();
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
throw new RuntimeException("Failed to read webhook state", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized List<StoredCard> loadCards() {
|
public synchronized List<StoredCard> loadCards() {
|
||||||
String sql = "SELECT card_id, card_token, card_name, card_number, balance, owner_uuid FROM cards ORDER BY updated_at DESC";
|
String sql = "SELECT card_id, card_token, card_name, card_number, balance, owner_uuid, webhook_enabled "
|
||||||
|
+ "FROM cards ORDER BY updated_at DESC";
|
||||||
List<StoredCard> result = new ArrayList<>();
|
List<StoredCard> result = new ArrayList<>();
|
||||||
try (Connection connection = open();
|
try (Connection connection = open();
|
||||||
PreparedStatement statement = connection.prepareStatement(sql);
|
PreparedStatement statement = connection.prepareStatement(sql);
|
||||||
@@ -113,7 +150,8 @@ public final class BankDatabase {
|
|||||||
rs.getString("card_name"),
|
rs.getString("card_name"),
|
||||||
rs.getString("card_number"),
|
rs.getString("card_number"),
|
||||||
rs.getLong("balance"),
|
rs.getLong("balance"),
|
||||||
rs.getString("owner_uuid")
|
rs.getString("owner_uuid"),
|
||||||
|
rs.getBoolean("webhook_enabled")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -107,47 +107,37 @@ public final class BankUiService {
|
|||||||
return lastMessage;
|
return lastMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String rootMessage(Throwable throwable) {
|
||||||
|
Throwable cause = throwable;
|
||||||
|
while (cause.getCause() != null) {
|
||||||
|
cause = cause.getCause();
|
||||||
|
}
|
||||||
|
return cause.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> syncCardsWithBackendAsync(String playerUuid) {
|
public CompletableFuture<Void> syncCardsWithBackendAsync(String playerUuid) {
|
||||||
return CompletableFuture.runAsync(() -> {
|
return BackendAuthenticator.fetchCardsFromBackendAsync()
|
||||||
try {
|
.thenCompose(cards -> syncMissingCardsAsync(cards, playerUuid))
|
||||||
List<CardCredentials> backendCards = BackendAuthenticator.fetchCardsFromBackend();
|
.thenAccept(changed -> {
|
||||||
boolean changed = false;
|
if (changed) {
|
||||||
for (CardCredentials creds : backendCards) {
|
reloadCardsFromDb();
|
||||||
if (database.getCredentials(creds.cardId()) == null) {
|
|
||||||
database.upsertCardCredentials(creds.cardId(), creds.cardToken());
|
|
||||||
refreshCardSync(creds.cardId(), creds.cardToken(), playerUuid, false, false);
|
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
if (changed) {
|
.exceptionally(exception -> {
|
||||||
reloadCardsFromDb();
|
System.err.println("[SPMEGA] Failed to sync cards with backend: " + exception.getMessage());
|
||||||
}
|
return null;
|
||||||
} catch (Exception e) {
|
});
|
||||||
System.err.println("[SPMEGA] Failed to sync cards with backend: " + e.getMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> refreshOnServerJoinAsync(String playerUuid) {
|
public CompletableFuture<Void> refreshOnServerJoinAsync(String playerUuid) {
|
||||||
return CompletableFuture.runAsync(() -> {
|
return BackendAuthenticator.fetchCardsFromBackendAsync()
|
||||||
try {
|
.exceptionally(exception -> {
|
||||||
List<CardCredentials> backendCards = BackendAuthenticator.fetchCardsFromBackend();
|
System.err.println("[SPMEGA] Failed to sync cards with backend: " + exception.getMessage());
|
||||||
for (CardCredentials creds : backendCards) {
|
return List.of();
|
||||||
if (database.getCredentials(creds.cardId()) == null) {
|
})
|
||||||
database.upsertCardCredentials(creds.cardId(), creds.cardToken());
|
.thenCompose(cards -> syncMissingCardsAsync(cards, playerUuid))
|
||||||
refreshCardSync(creds.cardId(), creds.cardToken(), playerUuid, false, false);
|
.thenCompose(ignored -> refreshStoredCardsAsync(playerUuid))
|
||||||
}
|
.thenRun(this::reloadCardsFromDb);
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("[SPMEGA] Failed to sync cards with backend: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<StoredCard> storedCards = database.loadCards();
|
|
||||||
for (StoredCard card : storedCards) {
|
|
||||||
refreshCardSync(card.cardId(), card.cardToken(), playerUuid, false, false);
|
|
||||||
}
|
|
||||||
reloadCardsFromDb();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<List<String>> loadRecipientCardsAsync(String username) {
|
public CompletableFuture<List<String>> loadRecipientCardsAsync(String username) {
|
||||||
@@ -156,9 +146,8 @@ public final class BankUiService {
|
|||||||
return CompletableFuture.completedFuture(List.of());
|
return CompletableFuture.completedFuture(List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return apiClient.getPlayerCardsAsync(username, toApiAuth(credentials))
|
||||||
try {
|
.thenApply(apiCards -> {
|
||||||
List<SPWorldsApiClient.PlayerCard> apiCards = apiClient.getPlayerCards(username, toApiAuth(credentials));
|
|
||||||
List<String> numbers = new ArrayList<>();
|
List<String> numbers = new ArrayList<>();
|
||||||
for (SPWorldsApiClient.PlayerCard apiCard : apiCards) {
|
for (SPWorldsApiClient.PlayerCard apiCard : apiCards) {
|
||||||
if (apiCard.number() != null && !apiCard.number().isBlank()) {
|
if (apiCard.number() != null && !apiCard.number().isBlank()) {
|
||||||
@@ -167,40 +156,11 @@ public final class BankUiService {
|
|||||||
}
|
}
|
||||||
lastMessage = "";
|
lastMessage = "";
|
||||||
return numbers;
|
return numbers;
|
||||||
} catch (Exception exception) {
|
})
|
||||||
|
.exceptionally(exception -> {
|
||||||
lastMessage = "Не удалось получить карты игрока: " + exception.getMessage();
|
lastMessage = "Не удалось получить карты игрока: " + exception.getMessage();
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<String> addCardAsync(String cardIdRaw, String cardTokenRaw, String playerUuid) {
|
|
||||||
String cardId = cardIdRaw == null ? "" : cardIdRaw.trim();
|
|
||||||
String cardToken = cardTokenRaw == null ? "" : cardTokenRaw.trim();
|
|
||||||
|
|
||||||
if (cardId.isEmpty() || cardToken.isEmpty()) {
|
|
||||||
return CompletableFuture.completedFuture("Укажи cardId и cardToken");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
UUID.fromString(cardId);
|
|
||||||
} catch (IllegalArgumentException exception) {
|
|
||||||
return CompletableFuture.completedFuture("cardId должен быть UUID");
|
|
||||||
}
|
|
||||||
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
|
||||||
database.upsertCardCredentials(cardId, cardToken);
|
|
||||||
boolean refreshed = refreshCardSync(cardId, cardToken, playerUuid, true, true);
|
|
||||||
if (!refreshed) {
|
|
||||||
database.deleteCard(cardId);
|
|
||||||
}
|
|
||||||
reloadCardsFromDb();
|
|
||||||
|
|
||||||
if (refreshed && lastMessage.isBlank()) {
|
|
||||||
return "Карта добавлена";
|
|
||||||
}
|
|
||||||
return lastMessage;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<String> removeSelectedCardAsync() {
|
public CompletableFuture<String> removeSelectedCardAsync() {
|
||||||
@@ -227,101 +187,48 @@ public final class BankUiService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<String> addCardAsync(String cardIdRaw, String cardTokenRaw, String playerUuid) {
|
||||||
|
String cardId = cardIdRaw == null ? "" : cardIdRaw.trim();
|
||||||
|
String cardToken = cardTokenRaw == null ? "" : cardTokenRaw.trim();
|
||||||
|
|
||||||
|
if (cardId.isEmpty() || cardToken.isEmpty()) {
|
||||||
|
return CompletableFuture.completedFuture("Укажи cardId и cardToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
UUID.fromString(cardId);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
return CompletableFuture.completedFuture("cardId должен быть UUID");
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.runAsync(() -> database.upsertCardCredentials(cardId, cardToken))
|
||||||
|
.thenCompose(ignored -> refreshCardAsync(cardId, cardToken, playerUuid, true, true))
|
||||||
|
.thenApply(refreshed -> {
|
||||||
|
if (!refreshed) {
|
||||||
|
database.deleteCard(cardId);
|
||||||
|
}
|
||||||
|
reloadCardsFromDb();
|
||||||
|
return refreshed && lastMessage.isBlank() ? "Карта добавлена" : lastMessage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public CompletableFuture<String> refreshSelectedCardAsync(String playerUuid) {
|
public CompletableFuture<String> refreshSelectedCardAsync(String playerUuid) {
|
||||||
CardViewModel selected = getSelectedCard();
|
CardViewModel selected = getSelectedCard();
|
||||||
if (selected == null) {
|
if (selected == null) {
|
||||||
return CompletableFuture.completedFuture("Нет карты для обновления");
|
return CompletableFuture.completedFuture("Нет карты для обновления");
|
||||||
}
|
}
|
||||||
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> database.getCredentials(selected.id()))
|
||||||
CardCredentials credentials = database.getCredentials(selected.id());
|
.thenCompose(credentials -> {
|
||||||
if (credentials == null) {
|
if (credentials == null) {
|
||||||
return "Не найдены креды карты";
|
return CompletableFuture.completedFuture("Не найдены креды карты");
|
||||||
}
|
}
|
||||||
|
return refreshCardAsync(credentials.cardId(), credentials.cardToken(), playerUuid, false, false)
|
||||||
refreshCardSync(credentials.cardId(), credentials.cardToken(), playerUuid, false, false);
|
.thenApply(ignored -> {
|
||||||
reloadCardsFromDb();
|
reloadCardsFromDb();
|
||||||
return lastMessage.isBlank() ? "Карта обновлена" : lastMessage;
|
return lastMessage.isBlank() ? "Карта обновлена" : lastMessage;
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
public CompletableFuture<Boolean> submitPaymentAsync(PaymentDraft draft) {
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
|
||||||
CardCredentials credentials = database.getCredentials(draft.senderCardId());
|
|
||||||
if (credentials == null) {
|
|
||||||
lastMessage = "Не найдены креды карты отправителя";
|
|
||||||
recordPaymentEvent(draft, false, "local", "no_credentials", 0, null);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
|
|
||||||
boolean useBackend = config != null && config.allowBackend();
|
|
||||||
String mode = useBackend ? "backend" : "spworlds-direct";
|
|
||||||
String destination = useBackend ? "backend" : "local-db";
|
|
||||||
String senderLast4 = extractLastDigits(credentials.cardId());
|
|
||||||
|
|
||||||
long startNs = System.nanoTime();
|
|
||||||
try {
|
|
||||||
long newBalance = 0;
|
|
||||||
if (useBackend) {
|
|
||||||
BackendAuthenticator.createTransactionOnBackend(
|
|
||||||
draft.senderCardId(),
|
|
||||||
draft.recipient(),
|
|
||||||
draft.amount(),
|
|
||||||
draft.comment()
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
refreshCardSync(credentials.cardId(), credentials.cardToken(), "", false, false);
|
|
||||||
StoredCard updatedCard = database.loadCards().stream()
|
|
||||||
.filter(c -> c.cardId().equals(credentials.cardId()))
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
if (updatedCard != null) {
|
|
||||||
newBalance = updatedCard.balance();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("[SPMEGA] Failed to refresh card balance after transaction: " + e.getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SPWorldsApiClient.TransactionResult result = apiClient.createTransaction(
|
|
||||||
toApiAuth(credentials),
|
|
||||||
draft.recipient(),
|
|
||||||
draft.amount(),
|
|
||||||
draft.comment()
|
|
||||||
);
|
|
||||||
newBalance = result.balance();
|
|
||||||
database.updateCardBalance(credentials.cardId(), newBalance);
|
|
||||||
}
|
|
||||||
|
|
||||||
database.insertTransferHistory(
|
|
||||||
credentials.cardId(),
|
|
||||||
draft.recipient(),
|
|
||||||
draft.amount(),
|
|
||||||
draft.comment(),
|
|
||||||
newBalance,
|
|
||||||
"SUCCESS"
|
|
||||||
);
|
|
||||||
|
|
||||||
reloadCardsFromDb();
|
|
||||||
lastMessage = "Перевод выполнен";
|
|
||||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
|
||||||
recordPaymentEvent(draft, true, mode, destination, durationMs, senderLast4);
|
|
||||||
return true;
|
|
||||||
} catch (Exception exception) {
|
|
||||||
database.insertTransferHistory(
|
|
||||||
credentials.cardId(),
|
|
||||||
draft.recipient(),
|
|
||||||
draft.amount(),
|
|
||||||
draft.comment(),
|
|
||||||
null,
|
|
||||||
"FAILED: " + trimMessage(exception.getMessage())
|
|
||||||
);
|
|
||||||
lastMessage = "Ошибка перевода: " + exception.getMessage();
|
|
||||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
|
||||||
recordPaymentEvent(draft, false, mode, destination, durationMs, senderLast4, exception.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recordPaymentEvent(PaymentDraft draft, boolean success, String mode, String destination, long durationMs, String senderLast4) {
|
private void recordPaymentEvent(PaymentDraft draft, boolean success, String mode, String destination, long durationMs, String senderLast4) {
|
||||||
@@ -343,17 +250,73 @@ public final class BankUiService {
|
|||||||
TelemetryCollector.instance().record(TelemetryEvent.now("payment", payload));
|
TelemetryCollector.instance().record(TelemetryEvent.now("payment", payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean refreshCardSync(
|
public CompletableFuture<Boolean> submitPaymentAsync(PaymentDraft draft) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> database.getCredentials(draft.senderCardId()))
|
||||||
|
.thenCompose(credentials -> {
|
||||||
|
if (credentials == null) {
|
||||||
|
lastMessage = "Не найдены креды карты отправителя";
|
||||||
|
recordPaymentEvent(draft, false, "local", "no_credentials", 0, null);
|
||||||
|
return CompletableFuture.completedFuture(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
boolean useBackend = config != null && config.allowBackend();
|
||||||
|
String mode = useBackend ? "backend" : "spworlds-direct";
|
||||||
|
String destination = useBackend ? "backend" : "local-db";
|
||||||
|
String senderLast4 = extractLastDigits(credentials.cardId());
|
||||||
|
long startNs = System.nanoTime();
|
||||||
|
|
||||||
|
CompletableFuture<Long> transaction = useBackend
|
||||||
|
? BackendAuthenticator.createTransactionOnBackendAsync(
|
||||||
|
draft.senderCardId(), draft.recipient(), draft.amount(), draft.comment())
|
||||||
|
.thenCompose(ignored -> refreshCardAsync(
|
||||||
|
credentials.cardId(), credentials.cardToken(), "", false, false))
|
||||||
|
.thenApply(ignored -> database.loadCards().stream()
|
||||||
|
.filter(card -> card.cardId().equals(credentials.cardId()))
|
||||||
|
.findFirst()
|
||||||
|
.map(StoredCard::balance)
|
||||||
|
.orElse(0L))
|
||||||
|
: apiClient.createTransactionAsync(
|
||||||
|
toApiAuth(credentials), draft.recipient(), draft.amount(), draft.comment())
|
||||||
|
.thenApply(result -> {
|
||||||
|
database.updateCardBalance(credentials.cardId(), result.balance());
|
||||||
|
return result.balance();
|
||||||
|
});
|
||||||
|
|
||||||
|
return transaction
|
||||||
|
.thenApply(newBalance -> {
|
||||||
|
database.insertTransferHistory(
|
||||||
|
credentials.cardId(), draft.recipient(), draft.amount(), draft.comment(),
|
||||||
|
newBalance, "SUCCESS");
|
||||||
|
reloadCardsFromDb();
|
||||||
|
lastMessage = "Перевод выполнен";
|
||||||
|
recordPaymentEvent(draft, true, mode, destination,
|
||||||
|
(System.nanoTime() - startNs) / 1_000_000L, senderLast4);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.exceptionally(exception -> {
|
||||||
|
String error = rootMessage(exception);
|
||||||
|
database.insertTransferHistory(
|
||||||
|
credentials.cardId(), draft.recipient(), draft.amount(), draft.comment(),
|
||||||
|
null, "FAILED: " + trimMessage(error));
|
||||||
|
lastMessage = "Ошибка перевода: " + error;
|
||||||
|
recordPaymentEvent(draft, false, mode, destination,
|
||||||
|
(System.nanoTime() - startNs) / 1_000_000L, senderLast4, error);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Boolean> refreshCardAsync(
|
||||||
String cardId,
|
String cardId,
|
||||||
String cardToken,
|
String cardToken,
|
||||||
String playerUuid,
|
String playerUuid,
|
||||||
boolean reportOwnerWarning,
|
boolean reportOwnerWarning,
|
||||||
boolean requireCardInAccount
|
boolean requireCardInAccount
|
||||||
) {
|
) {
|
||||||
try {
|
SPWorldsApiClient.CardAuth auth = new SPWorldsApiClient.CardAuth(cardId, cardToken);
|
||||||
SPWorldsApiClient.CardAuth auth = new SPWorldsApiClient.CardAuth(cardId, cardToken);
|
return apiClient.getAccountMeAsync(auth)
|
||||||
SPWorldsApiClient.AccountMe me = apiClient.getAccountMe(auth);
|
.thenCompose(me -> apiClient.getCardInfoAsync(auth).thenApply(cardInfo -> {
|
||||||
SPWorldsApiClient.CardInfo cardInfo = apiClient.getCardInfo(auth);
|
|
||||||
|
|
||||||
SPWorldsApiClient.AccountCard currentCard = me.cards().stream()
|
SPWorldsApiClient.AccountCard currentCard = me.cards().stream()
|
||||||
.filter(card -> Objects.equals(card.id(), cardId))
|
.filter(card -> Objects.equals(card.id(), cardId))
|
||||||
@@ -378,17 +341,67 @@ public final class BankUiService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
|
ModConfig config = SPMega.getConfig();
|
||||||
if (config != null && config.allowBackend()) {
|
if (config != null && config.allowBackend()) {
|
||||||
BackendAuthenticator.sendCardToBackend(cardId, cardToken);
|
BackendAuthenticator.sendCardToBackend(cardId, cardToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMessage = "";
|
lastMessage = "";
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception exception) {
|
}))
|
||||||
lastMessage = "Не удалось обновить карту: " + exception.getMessage();
|
.exceptionally(exception -> {
|
||||||
return false;
|
lastMessage = "Не удалось обновить карту: " + rootMessage(exception);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<String> registerSelectedWebhookAsync() {
|
||||||
|
CardViewModel selected = getSelectedCard();
|
||||||
|
if (selected == null) {
|
||||||
|
return CompletableFuture.completedFuture("Сначала выбери или добавь карту");
|
||||||
}
|
}
|
||||||
|
if (selected.webhookEnabled()) {
|
||||||
|
return CompletableFuture.completedFuture("Вебхук для этой карты уже включён");
|
||||||
|
}
|
||||||
|
|
||||||
|
return BackendAuthenticator.registerWebhookForCardAsync(selected.id())
|
||||||
|
.thenApply(ignored -> {
|
||||||
|
database.setWebhookEnabled(selected.id(), true);
|
||||||
|
reloadCardsFromDb();
|
||||||
|
return "Обработка вебхуков включена";
|
||||||
|
})
|
||||||
|
.exceptionally(exception -> "Не удалось включить вебхук: " + rootMessage(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasWebhookEnabledCards() {
|
||||||
|
return database.hasWebhookEnabledCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Boolean> syncMissingCardsAsync(List<CardCredentials> backendCards, String playerUuid) {
|
||||||
|
CompletableFuture<Boolean> result = CompletableFuture.completedFuture(false);
|
||||||
|
for (CardCredentials credentials : backendCards) {
|
||||||
|
result = result.thenCompose(changed -> {
|
||||||
|
if (database.getCredentials(credentials.cardId()) != null) {
|
||||||
|
database.setWebhookEnabled(credentials.cardId(), credentials.webhookEnabled());
|
||||||
|
return CompletableFuture.completedFuture(changed);
|
||||||
|
}
|
||||||
|
database.upsertCardCredentials(credentials.cardId(), credentials.cardToken());
|
||||||
|
database.setWebhookEnabled(credentials.cardId(), credentials.webhookEnabled());
|
||||||
|
return refreshCardAsync(
|
||||||
|
credentials.cardId(), credentials.cardToken(), playerUuid, false, false)
|
||||||
|
.thenApply(ignored -> true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> refreshStoredCardsAsync(String playerUuid) {
|
||||||
|
CompletableFuture<Void> result = CompletableFuture.completedFuture(null);
|
||||||
|
for (StoredCard card : database.loadCards()) {
|
||||||
|
result = result.thenCompose(ignored -> refreshCardAsync(
|
||||||
|
card.cardId(), card.cardToken(), playerUuid, false, false).thenApply(refreshed -> null));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reloadCardsFromDb() {
|
private void reloadCardsFromDb() {
|
||||||
@@ -401,7 +414,8 @@ public final class BankUiService {
|
|||||||
: stored.cardNumber();
|
: stored.cardNumber();
|
||||||
String cardName = stored.cardName() == null || stored.cardName().isBlank() ? "Карта" : stored.cardName();
|
String cardName = stored.cardName() == null || stored.cardName().isBlank() ? "Карта" : stored.cardName();
|
||||||
String title = cardNumber + ": " + cardName;
|
String title = cardNumber + ": " + cardName;
|
||||||
cards.add(new CardViewModel(stored.cardId(), title, stored.balance()));
|
cards.add(new CardViewModel(
|
||||||
|
stored.cardId(), title, stored.balance(), stored.webhookEnabled()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cards.isEmpty()) {
|
if (cards.isEmpty()) {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package git.yawaflua.tech.spmega.client.ui.service;
|
package git.yawaflua.tech.spmega.client.ui.service;
|
||||||
|
|
||||||
public record CardCredentials(String cardId, String cardToken) {
|
public record CardCredentials(String cardId, String cardToken, boolean webhookEnabled) {
|
||||||
|
public CardCredentials(String cardId, String cardToken) {
|
||||||
|
this(cardId, cardToken, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package git.yawaflua.tech.spmega.client.ui.service;
|
package git.yawaflua.tech.spmega.client.ui.service;
|
||||||
|
|
||||||
public record CardViewModel(String id, String title, long balance) {
|
public record CardViewModel(String id, String title, long balance, boolean webhookEnabled) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package git.yawaflua.tech.spmega.client.ui.service;
|
package git.yawaflua.tech.spmega.client.ui.service;
|
||||||
|
|
||||||
public record StoredCard(String cardId, String cardToken, String cardName, String cardNumber, long balance,
|
public record StoredCard(String cardId, String cardToken, String cardName, String cardNumber, long balance,
|
||||||
String ownerUuid) {
|
String ownerUuid, boolean webhookEnabled) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,13 @@
|
|||||||
"option.spmega.gps_position.bottom_right": "Bottom-Right",
|
"option.spmega.gps_position.bottom_right": "Bottom-Right",
|
||||||
"option.spmega.gps_position.bottom_center": "Bottom-center",
|
"option.spmega.gps_position.bottom_center": "Bottom-center",
|
||||||
"option.spmega.gps_position.top_center": "Top-center",
|
"option.spmega.gps_position.top_center": "Top-center",
|
||||||
|
"option.spmega.notification_position": "Notification position",
|
||||||
|
"option.spmega.notification_position.top_left": "Top-left",
|
||||||
|
"option.spmega.notification_position.top_right": "Top-right",
|
||||||
|
"option.spmega.notification_position.bottom_left": "Bottom-left",
|
||||||
|
"option.spmega.notification_position.bottom_right": "Bottom-right",
|
||||||
|
"option.spmega.notification_position.bottom_center": "Bottom-center",
|
||||||
|
"option.spmega.notification_position.top_center": "Top-center",
|
||||||
"category.spmega.telemetry": "Telemetry",
|
"category.spmega.telemetry": "Telemetry",
|
||||||
"option.spmega.telemetry_enabled": "Collect system information",
|
"option.spmega.telemetry_enabled": "Collect system information",
|
||||||
"tooltip.spmega.telemetry_enabled": "Disabling hides hardware data (CPU, GPU, RAM, IP). Performance and request stats keep sending.",
|
"tooltip.spmega.telemetry_enabled": "Disabling hides hardware data (CPU, GPU, RAM, IP). Performance and request stats keep sending.",
|
||||||
|
|||||||
@@ -29,6 +29,13 @@
|
|||||||
"option.spmega.gps_position.bottom_center": "Снизу по-центру",
|
"option.spmega.gps_position.bottom_center": "Снизу по-центру",
|
||||||
"option.spmega.gps_position.top_center": "Сверху по-центру",
|
"option.spmega.gps_position.top_center": "Сверху по-центру",
|
||||||
"option.spmega.gps_position.bottom_right": "Снизу справа",
|
"option.spmega.gps_position.bottom_right": "Снизу справа",
|
||||||
|
"option.spmega.notification_position": "Положение уведомлений",
|
||||||
|
"option.spmega.notification_position.top_left": "Слева вверху",
|
||||||
|
"option.spmega.notification_position.top_right": "Справа вверху",
|
||||||
|
"option.spmega.notification_position.bottom_left": "Слева внизу",
|
||||||
|
"option.spmega.notification_position.bottom_center": "Снизу по центру",
|
||||||
|
"option.spmega.notification_position.top_center": "Сверху по центру",
|
||||||
|
"option.spmega.notification_position.bottom_right": "Снизу справа",
|
||||||
"category.spmega.telemetry": "Телеметрия",
|
"category.spmega.telemetry": "Телеметрия",
|
||||||
"option.spmega.telemetry_enabled": "Сбор системной информации",
|
"option.spmega.telemetry_enabled": "Сбор системной информации",
|
||||||
"tooltip.spmega.telemetry_enabled": "Отключение скрывает данные компьютера (CPU, GPU, RAM, IP). Статистика производительности и запросов продолжает отправляться.",
|
"tooltip.spmega.telemetry_enabled": "Отключение скрывает данные компьютера (CPU, GPU, RAM, IP). Статистика производительности и запросов продолжает отправляться.",
|
||||||
@@ -37,4 +44,3 @@
|
|||||||
"option.spmega.telemetry_system_info": "Отправлять информацию о системе",
|
"option.spmega.telemetry_system_info": "Отправлять информацию о системе",
|
||||||
"tooltip.spmega.telemetry_system_info": "CPU, GPU, RAM, диск, IP, имя устройства, версия Java"
|
"tooltip.spmega.telemetry_system_info": "CPU, GPU, RAM, диск, IP, имя устройства, версия Java"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public final class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean allowAccess = readBoolean(properties, "allow.backend", defaults.allowBackend());
|
boolean allowAccess = readBoolean(properties, "allow.backend", defaults.allowBackend());
|
||||||
String rawAllowAccess = properties.getProperty("sign.quickPay.enabled");
|
String rawAllowAccess = properties.getProperty("allow.backend");
|
||||||
if (rawAllowAccess == null || !Boolean.toString(allowAccess).equalsIgnoreCase(rawAllowAccess.trim())) {
|
if (rawAllowAccess == null || !Boolean.toString(allowAccess).equalsIgnoreCase(rawAllowAccess.trim())) {
|
||||||
shouldSave = true;
|
shouldSave = true;
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,14 @@ public final class ConfigManager {
|
|||||||
shouldSave = true;
|
shouldSave = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GpsHudPosition notificationPosition = readEnum(
|
||||||
|
properties, "notifications.position", GpsHudPosition.class, defaults.notificationPosition());
|
||||||
|
String rawNotificationPosition = properties.getProperty("notifications.position");
|
||||||
|
if (rawNotificationPosition == null
|
||||||
|
|| !notificationPosition.name().equalsIgnoreCase(rawNotificationPosition.trim())) {
|
||||||
|
shouldSave = true;
|
||||||
|
}
|
||||||
|
|
||||||
boolean telemetryEnabled = readBoolean(properties, "telemetry.enabled", defaults.telemetryEnabled());
|
boolean telemetryEnabled = readBoolean(properties, "telemetry.enabled", defaults.telemetryEnabled());
|
||||||
String rawTelemetry = properties.getProperty("telemetry.enabled");
|
String rawTelemetry = properties.getProperty("telemetry.enabled");
|
||||||
if (rawTelemetry == null || !Boolean.toString(telemetryEnabled).equalsIgnoreCase(rawTelemetry.trim())) {
|
if (rawTelemetry == null || !Boolean.toString(telemetryEnabled).equalsIgnoreCase(rawTelemetry.trim())) {
|
||||||
@@ -85,6 +93,7 @@ public final class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ModConfig config = new ModConfig(apiDomain, apiToken, allowAccess, signQuickPayEnabled, gpsEnabled, gpsPosition,
|
ModConfig config = new ModConfig(apiDomain, apiToken, allowAccess, signQuickPayEnabled, gpsEnabled, gpsPosition,
|
||||||
|
notificationPosition,
|
||||||
telemetryEnabled, telemetryIntervalSeconds, telemetryCollectSystemInfo);
|
telemetryEnabled, telemetryIntervalSeconds, telemetryCollectSystemInfo);
|
||||||
|
|
||||||
|
|
||||||
@@ -145,9 +154,11 @@ public final class ConfigManager {
|
|||||||
Properties properties = new Properties();
|
Properties properties = new Properties();
|
||||||
properties.setProperty("api.domain", config.apiDomain());
|
properties.setProperty("api.domain", config.apiDomain());
|
||||||
properties.setProperty("api.token", config.apiToken());
|
properties.setProperty("api.token", config.apiToken());
|
||||||
|
properties.setProperty("allow.backend", Boolean.toString(config.allowBackend()));
|
||||||
properties.setProperty("sign.quickPay.enabled", Boolean.toString(config.signQuickPayEnabled()));
|
properties.setProperty("sign.quickPay.enabled", Boolean.toString(config.signQuickPayEnabled()));
|
||||||
properties.setProperty("gps.enabled", Boolean.toString(config.gpsEnabled()));
|
properties.setProperty("gps.enabled", Boolean.toString(config.gpsEnabled()));
|
||||||
properties.setProperty("gps.position", config.gpsPosition().name());
|
properties.setProperty("gps.position", config.gpsPosition().name());
|
||||||
|
properties.setProperty("notifications.position", config.notificationPosition().name());
|
||||||
properties.setProperty("telemetry.enabled", Boolean.toString(config.telemetryEnabled()));
|
properties.setProperty("telemetry.enabled", Boolean.toString(config.telemetryEnabled()));
|
||||||
properties.setProperty("telemetry.intervalSeconds", Integer.toString(config.telemetryIntervalSeconds()));
|
properties.setProperty("telemetry.intervalSeconds", Integer.toString(config.telemetryIntervalSeconds()));
|
||||||
properties.setProperty("telemetry.collectSystemInfo", Boolean.toString(config.telemetryCollectSystemInfo()));
|
properties.setProperty("telemetry.collectSystemInfo", Boolean.toString(config.telemetryCollectSystemInfo()));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package git.yawaflua.tech.spmega;
|
|||||||
|
|
||||||
public record ModConfig(String apiDomain, String apiToken, boolean allowBackend, boolean signQuickPayEnabled,
|
public record ModConfig(String apiDomain, String apiToken, boolean allowBackend, boolean signQuickPayEnabled,
|
||||||
boolean gpsEnabled, GpsHudPosition gpsPosition,
|
boolean gpsEnabled, GpsHudPosition gpsPosition,
|
||||||
|
GpsHudPosition notificationPosition,
|
||||||
boolean telemetryEnabled, int telemetryIntervalSeconds, boolean telemetryCollectSystemInfo) {
|
boolean telemetryEnabled, int telemetryIntervalSeconds, boolean telemetryCollectSystemInfo) {
|
||||||
public static final String DEFAULT_API_DOMAIN = "https://spmega.yawaflua.tech";
|
public static final String DEFAULT_API_DOMAIN = "https://spmega.yawaflua.tech";
|
||||||
public static final boolean ALLOW_BACKEND = true;
|
public static final boolean ALLOW_BACKEND = true;
|
||||||
@@ -9,6 +10,7 @@ public record ModConfig(String apiDomain, String apiToken, boolean allowBackend,
|
|||||||
public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true;
|
public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true;
|
||||||
public static final boolean DEFAULT_GPS_ENABLED = true;
|
public static final boolean DEFAULT_GPS_ENABLED = true;
|
||||||
public static final GpsHudPosition DEFAULT_GPS_POSITION = GpsHudPosition.TOP_CENTER;
|
public static final GpsHudPosition DEFAULT_GPS_POSITION = GpsHudPosition.TOP_CENTER;
|
||||||
|
public static final GpsHudPosition DEFAULT_NOTIFICATION_POSITION = GpsHudPosition.BOTTOM_RIGHT;
|
||||||
public static final boolean DEFAULT_TELEMETRY_ENABLED = true;
|
public static final boolean DEFAULT_TELEMETRY_ENABLED = true;
|
||||||
public static final int DEFAULT_TELEMETRY_INTERVAL_SECONDS = 60;
|
public static final int DEFAULT_TELEMETRY_INTERVAL_SECONDS = 60;
|
||||||
public static final boolean DEFAULT_TELEMETRY_COLLECT_SYSTEM_INFO = true;
|
public static final boolean DEFAULT_TELEMETRY_COLLECT_SYSTEM_INFO = true;
|
||||||
@@ -21,6 +23,7 @@ public record ModConfig(String apiDomain, String apiToken, boolean allowBackend,
|
|||||||
DEFAULT_SIGN_QUICK_PAY_ENABLED,
|
DEFAULT_SIGN_QUICK_PAY_ENABLED,
|
||||||
DEFAULT_GPS_ENABLED,
|
DEFAULT_GPS_ENABLED,
|
||||||
DEFAULT_GPS_POSITION,
|
DEFAULT_GPS_POSITION,
|
||||||
|
DEFAULT_NOTIFICATION_POSITION,
|
||||||
DEFAULT_TELEMETRY_ENABLED,
|
DEFAULT_TELEMETRY_ENABLED,
|
||||||
DEFAULT_TELEMETRY_INTERVAL_SECONDS,
|
DEFAULT_TELEMETRY_INTERVAL_SECONDS,
|
||||||
DEFAULT_TELEMETRY_COLLECT_SYSTEM_INFO
|
DEFAULT_TELEMETRY_COLLECT_SYSTEM_INFO
|
||||||
|
|||||||
Reference in New Issue
Block a user