Merge branch 'merge-hectic-lab'

This commit is contained in:
2026-04-26 13:52:17 +00:00
16 changed files with 1130 additions and 12 deletions

View File

@@ -11,7 +11,7 @@ let
hectic.imports = attrValues (
readModulesRecursive' ./hectic { inherit flake self inputs; }
);
# Read generic modules seperately
# Read generic modules separately
generic = readModulesRecursive'
./generic
{ inherit flake self inputs; };

View File

@@ -0,0 +1,169 @@
# INFO(nrv): This is standalone shadowsocks module. Instance-specific is at ./shadowsocks.nix
{
...
}:
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.shadowsocks-rust;
opts = {
server = cfg.localAddress;
server_port = cfg.port;
method = cfg.encryptionMethod;
mode = cfg.mode;
user = "nobody";
fast_open = cfg.fastOpen;
} // optionalAttrs (cfg.plugin != null) {
plugin = cfg.plugin;
plugin_opts = cfg.pluginOpts;
} // optionalAttrs (cfg.password != null) {
password = cfg.password;
} // cfg.extraConfig;
configFile = pkgs.writeText "shadowsocks.json" (builtins.toJSON opts);
in
{
###### interface
options = {
services.shadowsocks-rust = {
enable = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc ''
Whether to run shadowsocks-rust shadowsocks server.
'';
};
localAddress = mkOption {
type = types.str;
default = "0.0.0.0";
description = lib.mdDoc ''
Local addresses to which the server binds.
'';
};
port = mkOption {
type = types.port;
default = 8388;
description = lib.mdDoc ''
Port which the server uses.
'';
};
password = mkOption {
type = types.nullOr types.str;
default = null;
description = lib.mdDoc ''
Password for connecting clients.
'';
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
description = lib.mdDoc ''
Password file with a password for connecting clients.
'';
};
mode = mkOption {
type = types.enum [ "tcp_only" "tcp_and_udp" "udp_only" ];
default = "tcp_and_udp";
description = lib.mdDoc ''
Relay protocols.
'';
};
fastOpen = mkOption {
type = types.bool;
default = true;
description = lib.mdDoc ''
use TCP fast-open
'';
};
encryptionMethod = mkOption {
type = types.str;
default = "chacha20-ietf-poly1305";
description = lib.mdDoc ''
Encryption method. See <https://github.com/shadowsocks/shadowsocks-org/wiki/AEAD-Ciphers>.
'';
};
plugin = mkOption {
type = types.nullOr types.str;
default = null;
example = literalExpression ''"''${pkgs.shadowsocks-v2ray-plugin}/bin/v2ray-plugin"'';
description = lib.mdDoc ''
SIP003 plugin for shadowsocks
'';
};
pluginOpts = mkOption {
type = types.str;
default = "";
example = "server;host=example.com";
description = lib.mdDoc ''
Options to pass to the plugin if one was specified
'';
};
extraConfig = mkOption {
type = types.attrs;
default = {};
example = {
nameserver = "8.8.8.8";
};
description = lib.mdDoc ''
Additional configuration for shadowsocks that is not covered by the
provided options. The provided attrset will be serialized to JSON and
has to contain valid shadowsocks options. Unfortunately most
additional options are undocumented but it's easy to find out what is
available by looking into the source code of
<https://github.com/shadowsocks/shadowsocks-rust/blob/master/src/jconf.c>
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
assertions = singleton
{ assertion = cfg.password == null || cfg.passwordFile == null;
message = "Cannot use both password and passwordFile for shadowsocks-rust";
};
systemd.services.shadowsocks-rust = {
description = "shadowsocks-rust Daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.shadowsocks-rust ]
++ optional (cfg.plugin != null) cfg.plugin
++ optional (cfg.passwordFile != null) pkgs.jq;
serviceConfig.PrivateTmp = true;
script = ''
${optionalString (cfg.passwordFile != null) ''
cat ${configFile} | jq --arg password "$(cat "${cfg.passwordFile}")" '. + { password: $password }' > /run/shadowsocks.json
''}
exec ssserver --config ${if cfg.passwordFile != null then "/run/shadowsocks.json" else configFile}
'';
};
};
}

View File

@@ -0,0 +1,28 @@
{
...
}:
{
pkgs,
config,
...
}:
{
sops.secrets."ss-bfs/password" = {};
services.shadowsocks-rust = {
enable = true;
plugin = "${pkgs.shadowsocks-v2ray-plugin}/bin/v2ray-plugin";
# TODO: setup dnscrypt or a private DNS server for this
# extraConfig = {
# nameserver = "185.12.64.1"; # FIXME: this can vary across instances.
# };
port = 55228;
pluginOpts = "server";
# TODO: setup a TLS certs for this (look: (README.md) https://github.com/shadowsocks/v2ray-plugin/)
#pluginOpts = "server;tls;host=ss.bfs.band";
passwordFile = config.sops.secrets."ss-bfs/password".path;
mode = "tcp_and_udp"; # default
localAddress = "0.0.0.0";
fastOpen = true; # default
encryptionMethod = "chacha20-ietf-poly1305"; # default
};
}

View File

@@ -12,6 +12,7 @@
in {
imports = [
inputs.disko.nixosModules.default
inputs.nixos-mailserver.nixosModules.mailserver
];
options.hectic.archetype.base.enable = lib.mkEnableOption "Enable archetupe.dev";

View File

@@ -0,0 +1,65 @@
{
inputs,
flake,
self,
}:
{
lib,
config,
...
}: let
cfg = config.services.mailserver;
transformLoginAccounts = domain: input:
builtins.listToAttrs (map (key: {
name = key + "@" + domain;
value = input.${key};
}) (builtins.attrNames input));
in {
options = {
services.mailserver.enable = lib.mkEnableOption "Mail server";
services.mailserver.domain = lib.mkOption {
type = lib.types.str;
description = "The domain name of the mail server";
};
services.mailserver.loginAccounts = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
hashedPassword = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
hashedPasswordFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Full path to a file containing the hashed password suitable
for use with `chpasswd -e`.
'';
};
};
});
default = {};
description = "Login accounts for the mail server";
};
};
config = lib.mkIf cfg.enable {
mailserver = {
enable = true;
fqdn = "mail." + cfg.domain;
domains = [ cfg.domain ];
loginAccounts = transformLoginAccounts cfg.domain cfg.loginAccounts;
certificateScheme = "acme-nginx";
};
# NOTE(yukkop): avoid Gmail rejection due to missing IPv6 PTR records
services.postfix.settings.main.inet_protocols = lib.mkDefault "ipv4";
security.acme.acceptTerms = true;
security.acme.defaults.email = "security@" + cfg.domain;
};
}

View File

@@ -0,0 +1,108 @@
{
inputs ? null,
flake ? null,
self ? null,
...
}:
{
config ? null,
pkgs ? null,
lib ? null,
modulesPath ? null,
...
}:
with builtins;
with lib;
# with inputs.dream.lib;
let
in {
# networking.nat = {
# enable = true;
# internalInterfaces = [ "ve-+" ];
# externalInterface = "lo";
# # Lazy IPv6 connectivity for the container
# enableIPv6 = true;
# };
# containers.webserver = {
# autoStart = true;
# privateNetwork = true;
# hostAddress = "192.168.115.10";
# localAddress = "192.168.115.11";
# hostAddress6 = "fc00::1";
# localAddress6 = "fc00::2";
# config = import "${inputs.quteproxy}/nixos/system/quteproxy-staging/quteproxy-staging.nix" {
# self = inputs.quteproxy;
# inputs = inputs.quteproxy.inputs;
# flake = inputs.quteproxy;
# };
# };
# environment.etc.nixos.source = self;
# boot.kernelModules = [ "kvm" ];
# microvm.autostart = [
# "myvm1"
# ];
# microvm.vms = {
# myvm1 = {
# flake = self;
# updateFlake = "git+file:///etc/nixos";
# };
# };
# microvm = {
# mem = 1024*3;
# vcpu = 4;
# storeOnDisk = false;
# shares = [
# {
# proto = "9p";
# # securityModel = "mapped";
# tag = "ro-store";
# source = "/nix/store";
# mountPoint = "/nix/.ro-store";
# }
# {
# proto = "9p";
# securityModel = "mapped";
# tag = "fsRoot";
# source = "/media/pool/mythos/vm/work/vproxy/pr";
# mountPoint = "/home/devbox-user/pr";
# }
# ];
# interfaces = [
# {
# type = "user";
#
# # interface name on the host
# id = "vm-seht";
#
# # Ethernet address of the MicroVM's interface, not the host's
# # Locally administered have one of 2/6/A/E in the second nibble.
# mac = "02:00:00:00:00:01";
# }
# ];
# forwardPorts = [
# { from = "host"; host.port = 40500; guest.port = 22; }
# ];
#
# writableStoreOverlay = "/nix/.rw-store";
# volumes = [
# {
# autoCreate = true;
# size = 1024*32;
#
# image = "/media/pool/mythos/vm/work/vproxy/nix-store-overlay.img";
# mountPoint = config.microvm.writableStoreOverlay;
# }
# {
# autoCreate = true;
# size = 1024*32;
#
# image = "/media/pool/mythos/vm/work/vproxy/root.img";
# mountPoint = "/";
# }
# ];
# };
}

View File

@@ -0,0 +1,20 @@
{
flake,
self,
inputs,
system ? "x86_64-linux",
...
}: let
# Use folder name as name of this system
name = builtins.baseNameOf ./.;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
};
modules = [
{ networking.hostName = name; }
(import ./${name}.nix { inherit flake self inputs; })
];
}

View File

@@ -0,0 +1,257 @@
{
inputs,
flake,
self,
...
}:
{
config,
pkgs,
lib,
modulesPath,
...
}:
with builtins;
with lib;
let
domain = "hectic-lab.com";
sslOpts = {
sslCertificate = config.sops.secrets."ssl/porkbun/${domain}/domain.cert.pem".path;
sslCertificateKey = config.sops.secrets."ssl/porkbun/${domain}/private.key.pem".path;
};
in {
imports = [
self.nixosModules.hectic
inputs.sops-nix.nixosModules.sops
self.nixosModules."shadowsocks-rust" # NOTE(nrv): impl
self.nixosModules."shadowsocks" # NOTE(nrv): usage/instance
(import ./containers.nix { inherit flake self inputs; })
(import (./. + "/sentinèlla.nix") { inherit flake self inputs domain sslOpts; })
];
hectic = {
archetype.dev.enable = true;
hardware.hetzner-cloud = {
enable = true;
networkMatchConfigName = "enp1s0";
ipv4 = "128.140.75.58";
ipv6 = "2a01:4f8:c2c:d54a";
};
};
# NOTE(yukkop): disk was provisioned by Hetzner rescue image, disko was never
# run, so partition labels don't exist. Override fileSystems with actual UUIDs.
fileSystems."/" = lib.mkForce {
device = "/dev/disk/by-uuid/48ba7286-d019-4cdc-9784-459767979b07";
fsType = "ext4";
};
fileSystems."/boot" = lib.mkForce {
device = "/dev/disk/by-uuid/71F2-4E98";
fsType = "vfat";
options = [ "umask=0077" ];
};
programs.zsh.enable = true;
programs.zsh.interactiveShellInit = ''
setopt vi
'';
environment.systemPackages = with pkgs; [
git
rsync
python311
kitty
];
# Secrets config
sops = {
gnupg.sshKeyPaths = [ ];
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile = "${flake}/sus/hectic-lab.yaml";
};
users.users.root.openssh.authorizedKeys.keys = [
# yukkop
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMuP5NSfEQmO6m77xBWZvZ3hk7cw1q2k2vbsFd37rybU u0_a327@localhost"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBLxMo5icX2Xyng7mcWGnIi+c4ZbVygjPhuU8noCkfZ"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxgLlX/15Fk7PgIc9FSrA7oRtA8qK4GXfOhj7ZlNUaJ nix-on-droid@localhost"
# snuff
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFouceNUxI3bGC24/hfA8J3VuBpvTcZh3KhixgrMiLte"
# nrv
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE/EhBI6sJb2yHbTkqhZiCzUrsLE6t+CZe7RhS22z7w5 nrv@adamantia"
# github workflow
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKPEUArBxu7NUULT7Pi8ArtVxY1uVbIBSaeRKtqz1sz1"
];
users.users.ds4d = { # NOTE(nrv): artishoque
isNormalUser = true;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINcjBc57N6MxtMYAHEB/nwZ+OGsG3P1KWO1ZXvzQyhKn ds4d@ds4d"
];
};
users.users.sshuttle = {
isNormalUser = true;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKd4iU2E5fiwPwBbeo1ZPo0YBFEj9qBPew/KitaO+OHU"
];
};
sops.secrets."mailserver/security/hashedPassword" = {};
sops.secrets."mailserver/yukkop/hashedPassword" = {};
sops.secrets."mailserver/snuff/hashedPassword" = {};
sops.secrets."mailserver/antoshka/hashedPassword" = {};
sops.secrets."mailserver/founders/hashedPassword" = {};
services.mailserver = {
enable = true;
domain = domain;
loginAccounts = {
"security" = {
hashedPasswordFile = config.sops.secrets."mailserver/security/hashedPassword".path;
};
"founders" = {
hashedPasswordFile = config.sops.secrets."mailserver/founders/hashedPassword".path;
};
"yukkop" = {
hashedPasswordFile = config.sops.secrets."mailserver/yukkop/hashedPassword".path;
};
"snuff" = {
hashedPasswordFile = config.sops.secrets."mailserver/snuff/hashedPassword".path;
};
"antoshka" = {
hashedPasswordFile = config.sops.secrets."mailserver/antoshka/hashedPassword".path;
};
};
};
mailserver.stateVersion = 3;
services.redis.servers."vproxy-bot-test-state" = {
enable = true;
port = 6379;
};
services.mysql = {
enable = true;
package = pkgs.mariadb;
};
networking.firewall = {
allowedTCPPorts = [
443
3306 # mysql
25565
55228 # ss-bfs
];
allowedUDPPorts = [
51820 # wg-bfs
55228 # ss-bfs
];
};
virtualisation.docker.enable = true;
systemd.tmpfiles.rules = [
"d /var/www/store 0755 nginx nginx -"
];
sops.secrets."ssl/porkbun/${domain}/domain.cert.pem" = { group = "nginx"; mode = "0440"; };
sops.secrets."ssl/porkbun/${domain}/private.key.pem" = { group = "nginx"; mode = "0440"; };
sops.secrets."ssl/porkbun/${domain}/public.key.pem" = { group = "nginx"; mode = "0440"; };
services.nginx = {
enable = true;
virtualHosts.${domain} = sslOpts // {
forceSSL = true;
locations."/" = {
extraConfig = ''
root ${"${flake}/nixos/system/hectic-lab/static"};
try_files $uri $uri/ /index.html;
'';
};
};
virtualHosts."umbriel.${domain}" = sslOpts // {
forceSSL = true;
locations."/" = {
extraConfig = ''
root ${"${flake}/nixos/system/hectic-lab/static"};
try_files $uri $uri/ /index.html;
'';
};
};
virtualHosts."store.${domain}" = sslOpts // {
forceSSL = true;
root = "/var/www/store";
locations."/" = {
extraConfig = ''
autoindex on;
'';
};
};
virtualHosts."snuff.${domain}" = sslOpts // {
forceSSL = true;
locations."/" = {
extraConfig = ''
proxy_pass http://188.32.215.29:3993/;
proxy_redirect off;
'';
};
};
virtualHosts."nrv.${domain}" = sslOpts // {
forceSSL = true;
locations."/" = {
extraConfig = ''
proxy_pass http://127.0.0.1:22842/;
proxy_redirect off;
'';
};
};
virtualHosts."yukkop.${domain}" = sslOpts // {
forceSSL = true;
locations."/" = {
extraConfig = ''
proxy_pass http://127.0.0.1:9855/;
proxy_redirect off;
'';
};
};
};
# === WireGuard (disabled) ===
sops.secrets."wg-bfs/private-key" = {};
# networking.wireguard.interfaces = let
# subnet = "10.13.37";
# externalInterface = "eth0";
# in {
# wg-bfs = {
# ips = [ "${subnet}.1/24" ];
# listenPort = 51820;
# postSetup = ''
# ${pkgs.iptables}/bin/iptables -t 'nat' -A 'POSTROUTING' -s '${subnet}.0/24' -o '${externalInterface}' -j 'MASQUERADE'
# '';
# postShutdown = ''
# ${pkgs.iptables}/bin/iptables -t 'nat' -D 'POSTROUTING' -s '${subnet}.0/24' -o '${externalInterface}' -j 'MASQUERADE'
# '';
# privateKeyFile = config.sops.secrets."wg-bfs/private-key".path;
# generatePrivateKeyFile = false;
# peers = with lib; with builtins; let
# pubkeys = [
# "3dVzf1jxnVVTkLAyxedW+kRQBexZDzYDwpaLIcTrLjc=" # nrv (host: 2)
# "Kk2d0ncj24rO0qbuKh4V4t1OLnmVYbeaYvuEnL2OPFM=" # lysmi (host: 3)
# "BkM/NEDbR/XQ6WYQ0Yt+nJrc2HFCVsoW4QxBmkqxHn8=" # yukkop (host: 4)
# ];
# hosts = lists.range 2 254;
# zipped = zipLists pubkeys hosts;
# in flip map zipped ({ fst, snd }: {
# publicKey = "${fst}";
# allowedIPs = [ "${subnet}.${toString snd}/32" ];
# });
# };
# };
}

View File

@@ -0,0 +1,26 @@
{
inputs,
flake,
self,
domain,
sslOpts,
...
}: let
port = 5869;
in {
hectic = {
services."sentinèlla".probe = {
enable = true;
inherit port;
};
};
services.nginx = {
virtualHosts."probe.${domain}" = sslOpts // {
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:${builtins.toString port}";
};
};
};
}

View File

@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Counter App</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.tab-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
border: none;
background: #e5e7eb;
cursor: pointer;
border-radius: 4px;
}
.tab-button.active {
background: #3b82f6;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.counter-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.counter-button {
padding: 15px;
border: 1px solid #e5e7eb;
background: white;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
.counter-button:hover {
background: #f3f4f6;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.control-button {
padding: 10px 20px;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.control-button:hover {
background: #2563eb;
}
</style>
</head>
<body>
<div class="controls">
<button class="control-button" onclick="resetCounters()">Reset</button>
<button class="control-button" onclick="saveCounters()">Save</button>
<input type="file" id="loadFile" style="display: none" onchange="loadCounters(event)">
<button class="control-button" onclick="document.getElementById('loadFile').click()">Load</button>
</div>
<div class="tab-buttons">
<button class="tab-button active" onclick="showTab('counters')">Counters</button>
<button class="tab-button" onclick="showTab('chart')">Chart</button>
</div>
<div id="counters" class="tab-content active">
<div class="counter-grid" id="counterGrid"></div>
</div>
<div id="chart" class="tab-content">
<canvas id="counterChart"></canvas>
</div>
<script>
let counters = Array(20).fill(0);
let chart = null;
function initializeCounters() {
const grid = document.getElementById('counterGrid');
grid.innerHTML = '';
for (let i = 0; i < 20; i++) {
const button = document.createElement('button');
button.className = 'counter-button';
button.innerHTML = `Button ${i + 1}<br>Count: ${counters[i]}`;
button.onclick = () => incrementCounter(i);
grid.appendChild(button);
}
updateChart();
}
function incrementCounter(index) {
counters[index]++;
updateCounterDisplay();
updateChart();
}
function updateCounterDisplay() {
const buttons = document.querySelectorAll('.counter-button');
buttons.forEach((button, i) => {
button.innerHTML = `Button ${i + 1}<br>Count: ${counters[i]}`;
});
}
function updateChart() {
if (chart) {
chart.destroy();
}
const ctx = document.getElementById('counterChart').getContext('2d');
chart = new Chart(ctx, {
type: 'bar',
data: {
labels: Array.from({length: 20}, (_, i) => `Button ${i + 1}`),
datasets: [{
label: 'Click Count',
data: counters,
backgroundColor: '#3b82f6'
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
}
function showTab(tabId) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active');
});
document.getElementById(tabId).classList.add('active');
document.querySelector(`[onclick="showTab('${tabId}')"]`).classList.add('active');
if (tabId === 'chart') {
updateChart();
}
}
function resetCounters() {
counters = Array(20).fill(0);
updateCounterDisplay();
updateChart();
}
function saveCounters() {
const data = JSON.stringify(counters);
const blob = new Blob([data], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'counters.json';
a.click();
URL.revokeObjectURL(url);
}
function loadCounters(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
counters = JSON.parse(e.target.result);
updateCounterDisplay();
updateChart();
} catch (error) {
alert('Invalid file format');
}
};
reader.readAsText(file);
}
}
initializeCounters();
</script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>tg test</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://telegram.org/js/telegram-web-app.js?56"></script>
<script src="test.js"></script>
</head>
<body>
<div class="container">
<div class="content">
<p>TEST (again)</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,41 @@
function webappInit() {
console.log("Init start");
window.Telegram.WebApp.BackButton.isVisible = true;
window.Telegram.WebApp.backgroundColor = "#E60C0C";
let initData = window.Telegram.WebApp.initData;
if (initData) {
console.log("InitData", initData);
validate(initData);
}
console.log("Init end");
}
function validate(initData) {
const urlencodedData = initData;
const decodedData = decodeURIComponent(urlencodedData);
fetch(
"http://localhost:52022/rpc/webapp_auth",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Profile": "qutegate",
},
body: JSON.stringify({ raw_init_data: btoa(decodedData) }),
}
)
}
function waitForWebApp() {
if (window.Telegram && window.Telegram.WebApp) {
console.log("Telegram WebApp is available");
webappInit();
} else {
console.log("Telegram WebApp is not available yet");
setTimeout(waitForWebApp, 100);
}
}
waitForWebApp();