diff --git a/src/backend/.dockerignore b/src/backend/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/src/backend/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/backend/.gitignore b/src/backend/.gitignore new file mode 100644 index 0000000..74e533e --- /dev/null +++ b/src/backend/.gitignore @@ -0,0 +1,4 @@ +SpMega.Backend/src/* +SpMega.Backend/obj/* +SpMega.Backend/appsettings.json +SpMega.Backend/SpMega.Backend.http \ No newline at end of file diff --git a/src/backend/SpMega.Backend/Dockerfile b/src/backend/SpMega.Backend/Dockerfile new file mode 100644 index 0000000..b1a0681 --- /dev/null +++ b/src/backend/SpMega.Backend/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["SpMega.Backend/SpMega.Backend.csproj", "SpMega.Backend/"] +RUN dotnet restore "SpMega.Backend/SpMega.Backend.csproj" +COPY . . +WORKDIR "/src/SpMega.Backend" +RUN dotnet build "./SpMega.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./SpMega.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "SpMega.Backend.dll"] diff --git a/src/backend/SpMega.Backend/Program.cs b/src/backend/SpMega.Backend/Program.cs new file mode 100644 index 0000000..4f8b706 --- /dev/null +++ b/src/backend/SpMega.Backend/Program.cs @@ -0,0 +1,39 @@ +namespace SpMega.Backend; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.Services.AddAuthorization(); + + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + + app.UseAuthorization(); + + var summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + app.MapGet("/weatherforecast", (HttpContext httpContext) => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }) + .ToArray(); + return forecast; + }); + + app.Run(); + } +} \ No newline at end of file diff --git a/src/backend/SpMega.Backend/Properties/launchSettings.json b/src/backend/SpMega.Backend/Properties/launchSettings.json new file mode 100644 index 0000000..1946cb0 --- /dev/null +++ b/src/backend/SpMega.Backend/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5129", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/backend/SpMega.Backend/SpMega.Backend.csproj b/src/backend/SpMega.Backend/SpMega.Backend.csproj new file mode 100644 index 0000000..42e686b --- /dev/null +++ b/src/backend/SpMega.Backend/SpMega.Backend.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + Linux + + + + + .dockerignore + + + + diff --git a/src/backend/SpMega.Backend/SpMega.Backend.http b/src/backend/SpMega.Backend/SpMega.Backend.http new file mode 100644 index 0000000..fc6226b --- /dev/null +++ b/src/backend/SpMega.Backend/SpMega.Backend.http @@ -0,0 +1,6 @@ +@SpMega.Backend_HostAddress = http://localhost:5129 + +GET {{SpMega.Backend_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/backend/SpMega.Backend/WeatherForecast.cs b/src/backend/SpMega.Backend/WeatherForecast.cs new file mode 100644 index 0000000..e9d246d --- /dev/null +++ b/src/backend/SpMega.Backend/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace SpMega.Backend; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} \ No newline at end of file diff --git a/src/backend/SpMega.Backend/appsettings.Development.json b/src/backend/SpMega.Backend/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/backend/SpMega.Backend/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/backend/SpMega.Backend/appsettings.json b/src/backend/SpMega.Backend/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/backend/SpMega.Backend/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/backend/backend.sln b/src/backend/backend.sln new file mode 100644 index 0000000..d5efded --- /dev/null +++ b/src/backend/backend.sln @@ -0,0 +1,21 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpMega.Backend", "SpMega.Backend\SpMega.Backend.csproj", "{FF126D84-9381-4F0C-9925-FE08AC1FD07E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{64C061F2-C216-4C79-9AC2-CC43F1C0A34D}" + ProjectSection(SolutionItems) = preProject + compose.yaml = compose.yaml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/backend/compose.yaml b/src/backend/compose.yaml new file mode 100644 index 0000000..103676e --- /dev/null +++ b/src/backend/compose.yaml @@ -0,0 +1,7 @@ +services: + spmega.backend: + image: spmega.backend + build: + context: . + dockerfile: SpMega.Backend/Dockerfile + diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java b/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java new file mode 100644 index 0000000..f1d6cec --- /dev/null +++ b/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java @@ -0,0 +1,124 @@ +package git.yawaflua.tech.spmega.client; + +import git.yawaflua.tech.spmega.client.ui.UiNotifications; +import git.yawaflua.tech.spmega.client.ui.service.BankUiService; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +public class ChatListener { + private final UiNotifications notifications = UiNotifications.instance(); + private final Pattern TRANSACTION_PATTERN = Pattern.compile( + ".*(Управление картой).*", + Pattern.CASE_INSENSITIVE + ); + + public void register() { + ClientReceiveMessageEvents.CHAT.register((message, signedMessage, sender, params, timestamp) -> { + try { + handleMessage(message); + } catch (Throwable t) { + System.err.println("[SPMega] Ошибка обработки сообщения в CHAT листенере:"); + t.printStackTrace(); + } + }); + + ClientReceiveMessageEvents.GAME.register((message, overlay) -> { + try { + handleMessage(message); + } catch (Throwable t) { + System.err.println("[SPMega] Ошибка обработки сообщения в GAME листенере:"); + t.printStackTrace(); + } + }); + } + + private void handleMessage(Text message) { + if (message == null) { + return; + } + + String text = message.getString(); + if (TRANSACTION_PATTERN.matcher(text).matches()) { + List clickValues = new ArrayList<>(); + collectClickEventValues(message, clickValues); + + if (clickValues.size() >= 2) { + String tokenId = clickValues.get(0); + String cardId = clickValues.get(1); + + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player != null) { + String playerUuid = client.player.getUuidAsString(); + + CompletableFuture + .supplyAsync(() -> BankUiService.instance().addCard(cardId, tokenId, playerUuid)) + .thenAccept(msg -> { + if (client == null) { + return; + } + client.execute(() -> { + notifications.showMessage(msg); + }); + }) + .exceptionally(exception -> { + if (client != null) { + client.execute(() -> { + notifications.show(Text.literal("Ошибка добавления карты: " + exception.getMessage())); + }); + } + return null; + }); + } + } else { + System.out.println("[SPMega] Недостаточно кликабельных данных в сообщении (найдено: " + clickValues.size() + ")"); + } + } + } + + private void collectClickEventValues(Text text, List values) { + if (text == null) { + return; + } + + ClickEvent clickEvent = text.getStyle().getClickEvent(); + if (clickEvent != null) { + try { + String value = getEventValue(clickEvent); + if (value != null && !value.isEmpty()) { + values.add(value); + } + } catch (Throwable t) { + System.err.println("[SPMega] Ошибка извлечения значения клик-события:"); + t.printStackTrace(); + } + } + + for (Text sibling : text.getSiblings()) { + collectClickEventValues(sibling, values); + } + } + + private String getEventValue(ClickEvent clickEvent) { + if (clickEvent instanceof ClickEvent.CopyToClipboard(String value)) { + return value; + } else if (clickEvent instanceof ClickEvent.RunCommand(String command1)) { + return command1; + } else if (clickEvent instanceof ClickEvent.SuggestCommand(String command)) { + return command; + } else if (clickEvent instanceof ClickEvent.OpenUrl(java.net.URI uri)) { + return uri.toString(); + } else if (clickEvent instanceof ClickEvent.OpenFile(String path)) { + return path; + } else if (clickEvent instanceof ClickEvent.ChangePage(int page)) { + return String.valueOf(page); + } + return ""; + } +} diff --git a/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java b/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java index ee06332..a023d27 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java @@ -167,5 +167,7 @@ public class SPMegaClient implements ClientModInitializer { } }); + + new ChatListener().register(); } } diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java index 668b13f..78129eb 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java @@ -41,6 +41,7 @@ public class PaymentScreen extends Screen { private Text senderCardText = Text.literal("Карта отправителя: не выбрана"); private ButtonWidget transferButton; private ButtonWidget backButton; + private boolean transferInProgress; public PaymentScreen(Screen parent) { this(parent, ""); @@ -192,6 +193,11 @@ public class PaymentScreen extends Screen { } private void submit() { + if (transferInProgress) { + notifications.show(Text.literal("Перевод уже выполняется")); + return; + } + CardViewModel selectedCard = bankUiService.getSelectedCard(); if (selectedCard == null) { notifications.show(Text.literal("Нет выбранной карты отправителя")); @@ -205,7 +211,8 @@ public class PaymentScreen extends Screen { notifications.show(Text.literal("Сумма должна быть больше 0")); return; } - } catch (NumberFormatException exception) { + } catch (Exception exception) { + System.out.println(exception.getMessage()); notifications.show(Text.literal("Некорректная сумма")); return; } @@ -232,18 +239,46 @@ public class PaymentScreen extends Screen { commentField.getText().trim() ); - boolean accepted = bankUiService.submitPayment(draft); - String serviceMessage = bankUiService.getLastMessage(); - if (!serviceMessage.isBlank()) { + setTransferInProgress(true); + notifications.show(Text.literal("Отправка перевода...")); - notifications.showMessage(serviceMessage); - } else { - notifications.show(accepted - ? Text.literal("Перевод выполнен") - : Text.literal("Перевод отклонен")); + bankUiService.submitPaymentAsync(draft) + .thenAccept(accepted -> { + if (this.client == null) { + return; + } + this.client.execute(() -> { + setTransferInProgress(false); + + String serviceMessage = bankUiService.getLastMessage(); + if (!serviceMessage.isBlank()) { + notifications.showMessage(serviceMessage); + } else { + notifications.show(accepted + ? Text.literal("Перевод выполнен") + : Text.literal("Перевод отклонен")); + } + + updateSenderCardSelector(); + updateSenderCardText(); + }); + }) + .exceptionally(exception -> { + if (this.client != null) { + this.client.execute(() -> { + setTransferInProgress(false); + notifications.show(Text.literal("Ошибка перевода: " + exception.getMessage())); + }); + } + return null; + }); + } + + private void setTransferInProgress(boolean inProgress) { + transferInProgress = inProgress; + if (transferButton != null) { + transferButton.active = !inProgress; } - updateSenderCardSelector(); - updateSenderCardText(); } private boolean isValidRecipient(String recipient) { @@ -277,7 +312,7 @@ public class PaymentScreen extends Screen { private void updateSenderCardSelector() { CardViewModel selectedCard = bankUiService.getSelectedCard(); if (selectedCard == null) { - senderCardLabelButton.setMessage(Text.literal("\"00000\": \"0\" АР")); + senderCardLabelButton.setMessage(Text.literal("TEST 00000: 0 АР")); senderLeftButton.active = false; senderRightButton.active = false; return; @@ -285,9 +320,8 @@ public class PaymentScreen extends Screen { senderLeftButton.active = true; senderRightButton.active = true; - String cardId = extractCardId(selectedCard.title()); String balance = Long.toString(selectedCard.balance()); - senderCardLabelButton.setMessage(Text.literal("\"" + cardId + "\": \"" + balance + "\" АР")); + senderCardLabelButton.setMessage(Text.literal(selectedCard.title() + ": " + balance + " АР")); senderCardLabelButton.active = false; } diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java index df15417..72d7229 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java @@ -6,6 +6,7 @@ import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.text.Text; import net.minecraft.util.Util; +import java.awt.*; import java.net.URI; public class QRcodeAcceptScreen extends Screen { @@ -20,6 +21,8 @@ public class QRcodeAcceptScreen extends Screen { @Override protected void init() { + + this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.qr.cancel"), button -> { if (this.client != null) { this.client.setScreen(parent); @@ -53,17 +56,10 @@ public class QRcodeAcceptScreen extends Screen { context.drawCenteredTextWithShadow( this.textRenderer, - Text.translatable("screen.spmega.qr.accept_link"), + Text.literal("Найдена ссылка: " + url), this.width / 2, - this.height / 2 - 30, - 0xFFFFFF - ); - context.drawCenteredTextWithShadow( - this.textRenderer, - Text.literal(url), - this.width / 2, - this.height / 2 - 10, - 0xFFD27F + this.height / 2, + Color.WHITE.getRGB() ); } } diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankUiService.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankUiService.java index a458a09..1b060e5 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankUiService.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankUiService.java @@ -4,6 +4,7 @@ import git.yawaflua.tech.spmega.api.SPWorldsApiClient; import net.fabricmc.loader.api.FabricLoader; import java.util.*; +import java.util.concurrent.CompletableFuture; public final class BankUiService { private static final BankUiService INSTANCE = new BankUiService(); @@ -110,7 +111,7 @@ public final class BankUiService { List numbers = new ArrayList<>(); for (SPWorldsApiClient.PlayerCard apiCard : apiCards) { if (apiCard.number() != null && !apiCard.number().isBlank()) { - numbers.add(apiCard.number()); + numbers.add(apiCard.name() + " : " + apiCard.number()); } } lastMessage = ""; @@ -224,6 +225,10 @@ public final class BankUiService { } } + public CompletableFuture submitPaymentAsync(PaymentDraft draft) { + return CompletableFuture.supplyAsync(() -> submitPayment(draft)); + } + private boolean refreshCard( String cardId, String cardToken, diff --git a/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java b/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java index af6a208..61f95be 100644 --- a/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java +++ b/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java @@ -53,6 +53,15 @@ public final class SPWorldsApiClient { String body = send(request); try { JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + if (json.has("statusCode")) { + switch (json.get("statusCode").getAsInt()) { + case 403: + throw new IOException("Апи вернула ошибку: " + json.get("message").getAsString()); + default: + System.out.println("Unhandled status code in card info response: " + json.get("statusCode").getAsInt()); + break; + } + } long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L; String webhook = json.has("webhook") && !json.get("webhook").isJsonNull() ? json.get("webhook").getAsString() diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index d5e984e..2beddca 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -4,8 +4,11 @@ "version": "${version}", "name": "SPMega", "description": "yawaflua`s SPRadar+SPMHelper mod, thats make a lot!", - "authors": [], - "contact": {}, + "authors": [ + "Dmitri 'yawaflua' Shimanski aka DOLBAYEB" + ], + "contact": { + }, "license": "CC BY-NC-ND 4.0", "icon": "icon.png", "environment": "client",