GroundControl/src/controller/GroundController.ts
Overtorment 5d3f630a01
FIX: FCM pushes fixed due to deprecation of legacy api (#292)
* FIX: FCM pushes fixed due to deprecation of legacy  api
2024-12-15 16:35:40 +00:00

434 lines
16 KiB
TypeScript

import { DataSource } from "typeorm";
import { NextFunction, Request, Response } from "express";
import { TokenToAddress } from "../entity/TokenToAddress";
import { TokenToHash } from "../entity/TokenToHash";
import { TokenToTxid } from "../entity/TokenToTxid";
import { TokenConfiguration } from "../entity/TokenConfiguration";
import { SendQueue } from "../entity/SendQueue";
import { PushLog } from "../entity/PushLog";
import { KeyValue } from "../entity/KeyValue";
import dataSource from "../data-source";
import { paths, components } from "../openapi/api";
require("dotenv").config();
const pck = require("../../package.json");
if (!process.env.JAWSDB_MARIA_URL || !process.env.GOOGLE_KEY_FILE || !process.env.APNS_P8 || !process.env.APNS_TOPIC || !process.env.APPLE_TEAM_ID || !process.env.APNS_P8_KID || !process.env.GOOGLE_PROJECT_ID) {
console.error("not all env variables set");
process.exit();
}
const LAST_PROCESSED_BLOCK = "LAST_PROCESSED_BLOCK";
const ADDRESS_IGNORE_LIST = [
"1NXNHZr6Pbzi3VStcgaxwEhspTWNXQ3Q4G",
"bc1qee7hk4a3k3km7j8hwclm0pkl76dhhgxay5nevu",
"bc1qclyfsxuu8vcwq38yygs5zrskwacq8sjlyvk9mx",
"bc1qltxjty7xfrnkzlrhmxpcekknr3uncne8sht7rn",
"bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej",
"bc1qw8wrek2m7nlqldll66ajnwr9mh64syvkt67zlu",
"1111111111111111111114oLvT2",
"1BrasiLb2KMbdtuhb1chAVnS2FvcNGfV9J",
"1GQdrgqAbkeEPUef1UpiTc4X1mUHMcyuGW",
"1KHwtS5mn7NMUm7Ls7Y1XwxLqMriLdaGbX",
"bc1q7cyrfmck2ffu2ud3rn5l5a8yv6f0chkp0zpemf",
"bc1qwfgdjyy95aay2686fn74h6a4nu9eev6np7q4fn204dkj3274frlqrskvx0",
"bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h",
"bc1qrnn4wfhgz2e0etek66sh3n9l6k99alxk044mhr",
"13nCMaHDNRGM29UfMMkhUQjHkVYY1ZyrpU",
"bc1qel7tps3wu6zqztaanczvt76hffwh7k06jd8r9xh2v3ztpa5ty5dsz358ys",
"bc1qyzxdu4px4jy8gwhcj82zpv7qzhvc0fvumgnh0r",
"bc1qyemk24czaa6a2nr89nrz775ewvptxg7yfe750u",
"bc1qq904ynep5mvwpjxdlyecgeupg22dm8am6cfvgq",
"37biYvTEcBVMoR1NGkPTGvHUuLTrzcLpiv",
"bc1qq9tk3uhcx58y5qvmzs50nhs49m0pdkmrfpkzs4",
"1Kr6QSydW9bFQG1mXiPNNu6WpJGmUa9i1g",
"3DGxAYYUA61WrrdbBac8Ra9eA9peAQwTJF",
"bc1qmgj3w0aw5455y9s4zfhts2kxm4qstwyjx5f907",
"bc1qgrxsrmrhsapvh9addyx6sh8j4rw0sn9xtur9uq",
"bc1qp3f7vnmuj4pjxpfvkvf7yznac9h9r5arlv4fpv",
"bc1qnsupj8eqya02nm8v6tmk93zslu2e2z8chlmcej",
"bc1qt5m8xeclsja4lkfvl2nvmrt6z9vg60sd8w2kc6",
"bc1qsatlphjcgvzlt9xhsgn0dnjus5jgwg83dr05c6",
"bc1quq29mutxkgxmjfdr7ayj3zd9ad0ld5mrhh89l2",
"bc1qe9nagya0tvfhvymt8sejwedlukwq4a094h6ht9",
"1GrwDkr33gT6LuumniYjKEGjTLhsL5kmqC",
"33WSGLeVoEpuZDjB54HKZ1y5YsERELoVNq",
"3A8n8rwMnHnt2BqnjW4R73eZCMcUDTpYvv",
"bc1qns9f7yfx3ry9lj6yz7c9er0vwa0ye2eklpzqfw",
"38XnPvu9PmonFU9WouPXUjYbW91wa5MerL",
"1Bo8hs81QwnR6A3oFBXcWNZXgtwpfgByb3",
"37Z6neB2wDC3hsPDHLy2n2kFahNNU3eos8",
"1CK6KHY6MHgYvmRQ4PAafKYDrg1ejbH1cE",
"36XWTfSYJJz3WSNPZVZ3q3aa5eFuJHR9nu",
"bc1qc8ee9860cdnkyej0ag5hf49pcx7uvz89lkwpr9",
"bc1q7ug4w4as2sefar89q057hnmxkakp58a25535ttlmurn6cncs8tms4e7gp2",
"3JodN7GmkHdPgKj9G7HCkn9NDLhrcWCjVN",
"bc1qujepl0k5n0ga2e86yskvxa6auehpf6dlf84dx0",
"bc1qg9lgqyukp2rup5x4frrz7hhw7988k5q26luakm",
"3JSdUu1ivm3rqMvuCTAdAj6Dc2hdVhHiEe",
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
"bc1qr35hws365juz5rtlsjtvmulu97957kqvr3zpw3",
"bc1qzlcvh2k3xs2jyzvya7xmx8l2ylpt259txy7wnd",
"bc1qprdf80adfz7aekh5nejjfrp3jksc8r929svpxk",
"35iMHbUZeTssxBodiHwEEkb32jpBfVueEL",
"3QiETomgUhPu573ZvhXbdofq7y5ocNS1ie",
"3HcEUguUZ4vyyMAPWDPUDjLqz882jXwMfV",
"3J7cUjBZxvGRCwFBz3q23zAsnhFfZrDSSU",
"bc1qamgjuxaywqls56h7rg7afga3m6rgqwfkew688k",
"1G47mSr3oANXMafVrR8UC4pzV7FEAzo3r9",
"17StnGroPUsNXBq4AVJQ1fqGftoFZh3zva",
"1FWQiwK27EnGXb6BiBMRLJvunJQZZPMcGd",
"16r7U7GqbVPeKukgfd3mUN9LCkuoKbfpXM",
"bc1q22hp7n28whk5h94z93vm05hfx2zxs8ca9gglk7",
"bc1q0wu0tqp2u3rtunjl0h0rsl9pvf86acy6sep63st0lp7lgg67ykzqeq89pn",
"bc1qryhgpmfv03qjhhp2dj8nw8g4ewg08jzmgy3cyx",
"bc1quhruqrghgcca950rvhtrg7cpd7u8k6svpzgzmrjy8xyukacl5lkq0r8l2d",
"bc1q0s6rca52mjumq8fjuz4j6ka5h9jhq57esyd40r",
"bc1qjguxrqjx9qnpmklzymdr0y6mqxrth9d3swzdpr",
"bc1qr4dl5wa7kl8yu792dceg9z5knl2gkn220lk7a9",
"1Dy8gSw3Zbpatr4bGn7zR4phNX18XuTD56",
"bc1qcdqj2smprre85c78d942wx5tauw5n7uw92r7wr",
"bc1qmhq4sgtchfgh6ul75x3rsuegt55mef0zx3ehm2",
"bc1qws8yeyfxzykuq7tevwwxezyv3ad99emlyy9uls",
"bc1q42kvqt0e3f27qhd2ucnprarl5ywpuj7tu0h9v2",
"bc1q5k9tyr30xhmvmnj2z2cx0psprz44ksnmpuw7q8",
];
let connection: DataSource;
const pushLogPurge = () => {
console.log("purging PushLog...");
let today = new Date();
connection
.createQueryBuilder()
.delete()
.from(PushLog)
.where("created <= :currentDate", { currentDate: new Date(today.getTime() - 3 * 24 * 60 * 60 * 1000) })
.execute()
.then(() => console.log("PushLog purged ok"))
.catch((error) => console.log("error purging PushLog:", error));
};
const purgeOldTxidSubscriptions = () => {
console.log("purging TokenToTxid...");
let today = new Date();
connection
.createQueryBuilder()
.delete()
.from(TokenToTxid)
.where("created <= :currentDate", { currentDate: new Date(today.getTime() - 3 * 30 * 24 * 60 * 60 * 1000) }) // 3 mo
.execute()
.then(() => console.log("TokenToTxid purged ok"))
.catch((error) => console.log("error purging TokenToTxid:", error));
};
const purgeIgnoredAddressesSubscriptions = () => {
console.log("Purging addresses subscriptions...");
connection
.createQueryBuilder()
.delete()
.from(TokenToAddress)
.where("address IN (:...id)", { id: ADDRESS_IGNORE_LIST })
.execute()
.then(() => console.log("Addresses subscriptions purged ok"))
.catch((error) => console.log("error purging addresses subscriptions:", error));
};
dataSource.initialize().then((c) => {
console.log("db connected");
connection = c;
purgeIgnoredAddressesSubscriptions();
pushLogPurge();
purgeOldTxidSubscriptions();
setInterval(pushLogPurge, 3600 * 1000);
});
export class GroundController {
private _tokenToAddressRepository;
private _tokenToHashRepository;
private _tokenToTxidRepository;
private _tokenConfigurationRepository;
private _sendQueueRepository;
get tokenToAddressRepository() {
if (this._tokenToAddressRepository) {
return this._tokenToAddressRepository;
}
this._tokenToAddressRepository = connection.getRepository(TokenToAddress);
return this._tokenToAddressRepository;
}
get tokenToHashRepository() {
if (this._tokenToHashRepository) {
return this._tokenToHashRepository;
}
this._tokenToHashRepository = connection.getRepository(TokenToHash);
return this._tokenToHashRepository;
}
get tokenToTxidRepository() {
if (this._tokenToTxidRepository) {
return this._tokenToTxidRepository;
}
this._tokenToTxidRepository = connection.getRepository(TokenToTxid);
return this._tokenToTxidRepository;
}
get tokenConfigurationRepository() {
if (this._tokenConfigurationRepository) {
return this._tokenConfigurationRepository;
}
this._tokenConfigurationRepository = connection.getRepository(TokenConfiguration);
return this._tokenConfigurationRepository;
}
get sendQueueRepository() {
if (this._sendQueueRepository) {
return this._sendQueueRepository;
}
this._sendQueueRepository = connection.getRepository(SendQueue);
return this._sendQueueRepository;
}
/**
* Submit bitcoin addressess that you wish to be notified about to specific push token. Token serves as unique identifier of a device/user. Also, OS of the token
*
* @param request
* @param response
* @param next
*/
async majorTomToGroundControl(request: Request, response: Response, next: NextFunction) {
const body: paths["/majorTomToGroundControl"]["post"]["requestBody"]["content"]["application/json"] = request.body;
// todo: checks that we are receiving data and that there are not too much records in it (probably 1000 addresses for a start is enough)
if (!body.addresses || !Array.isArray(body.addresses)) {
body.addresses = [];
}
if (!body.hashes || !Array.isArray(body.hashes)) {
body.hashes = [];
}
if (!body.txids || !Array.isArray(body.txids)) {
body.txids = [];
}
if (!body.token || !body.os) {
response.status(500).send("token not provided");
return;
}
// todo: refactor into single batch save
for (const address of body.addresses) {
if (ADDRESS_IGNORE_LIST.includes(address)) {
continue;
}
// todo: validate bitcoin address
console.log(body.token, "->", address);
try {
await this.tokenToAddressRepository.save({
address,
token: body.token,
os: body.os,
});
} catch (_) {}
}
// todo: refactor into single batch save
for (const hash of body.hashes) {
// todo: validate hash
console.log(body.token, "->", hash);
try {
await this.tokenToHashRepository.save({
hash,
token: body.token,
os: body.os,
});
} catch (_) {}
}
// todo: refactor into single batch save
for (const txid of body.txids) {
// todo: validate txid
console.log(body.token, "->", txid);
try {
await this.tokenToTxidRepository.save({
txid,
token: body.token,
os: body.os,
});
} catch (_) {}
}
response.status(201).send("");
}
async unsubscribe(request: Request, response: Response, next: NextFunction) {
const body: paths["/unsubscribe"]["post"]["requestBody"]["content"]["application/json"] = request.body;
// todo: checks that we are receiving data and that there are not too much records in it (probably 1000 addresses for a start is enough)
if (!body.addresses || !Array.isArray(body.addresses)) {
body.addresses = [];
}
if (!body.hashes || !Array.isArray(body.hashes)) {
body.hashes = [];
}
if (!body.txids || !Array.isArray(body.txids)) {
body.txids = [];
}
if (!body.token || !body.os) {
response.status(500).send("token not provided");
return;
}
for (const address of body.addresses) {
try {
const addressRecord = await this.tokenToAddressRepository.findOneBy({ os: body.os, token: body.token, address });
await this.tokenToAddressRepository.remove(addressRecord);
} catch (_) {}
}
for (const hash of body.hashes) {
try {
const hashRecord = await this.tokenToHashRepository.findOneBy({ os: body.os, token: body.token, hash });
await this.tokenToHashRepository.remove(hashRecord);
} catch (_) {}
}
for (const txid of body.txids) {
try {
const txidRecord = await this.tokenToTxidRepository.findOneBy({ os: body.os, token: body.token, txid });
await this.tokenToTxidRepository.remove(txidRecord);
} catch (_) {}
}
response.status(201).send("");
}
/**
* POST request handler that notifies us that specific ln invoice was paid
*
* @param request
* @param response
* @param next
*/
async lightningInvoiceGotSettled(request: Request, response: Response, next: NextFunction) {
const body: paths["/lightningInvoiceGotSettled"]["post"]["requestBody"]["content"]["application/json"] = request.body;
const hashShouldBe = require("crypto").createHash("sha256").update(Buffer.from(body.preimage, "hex")).digest("hex");
if (hashShouldBe !== body.hash) {
response.status(500).send("preimage doesnt match hash");
return;
}
const tokenToHashAll = await this.tokenToHashRepository.find({
where: {
hash: hashShouldBe,
},
});
for (const tokenToHash of tokenToHashAll) {
process.env.VERBOSE && console.log("enqueueing to token", tokenToHash.token, tokenToHash.os);
const pushNotification: components["schemas"]["PushNotificationLightningInvoicePaid"] = {
sat: body.amt_paid_sat,
badge: 1,
type: 1,
level: "transactions",
os: tokenToHash.os === "android" ? "android" : "ios", //hacky
token: tokenToHash.token,
hash: hashShouldBe,
memo: body.memo,
};
await this.sendQueueRepository.save({
data: JSON.stringify(pushNotification),
});
}
response.status(200).send("");
}
async ping(request: Request, response: Response, next: NextFunction) {
const keyValueRepository = connection.getRepository(KeyValue);
const sendQueueRepository = connection.getRepository(SendQueue);
const keyVal = await keyValueRepository.findOneBy({ key: LAST_PROCESSED_BLOCK });
const send_queue_size = await sendQueueRepository.count();
const ts = new Date(+new Date() - 1000 * 3600 * 24).toISOString();
const sent_24h = await connection.createQueryBuilder(PushLog, "PushLog").where("PushLog.created >= :ts", { ts }).getCount();
const serverInfo: paths["/ping"]["get"]["responses"]["200"]["content"]["application/json"] = {
name: pck.name,
description: pck.description,
version: pck.version,
uptime: Math.floor(process.uptime()),
last_processed_block: +keyVal.value,
send_queue_size,
sent_24h,
};
return serverInfo;
}
async setTokenConfiguration(request: Request, response: Response, next: NextFunction) {
const body: paths["/setTokenConfiguration"]["post"]["requestBody"]["content"]["application/json"] = request.body;
let tokenConfig = await this.tokenConfigurationRepository.findOneBy({ token: body.token, os: body.os });
if (!tokenConfig) {
tokenConfig = new TokenConfiguration();
tokenConfig.token = body.token;
tokenConfig.os = body.os;
} else {
if (typeof body.level_all !== "undefined") tokenConfig.level_all = !!body.level_all;
if (typeof body.level_transactions !== "undefined") tokenConfig.level_transactions = !!body.level_transactions;
if (typeof body.level_price !== "undefined") tokenConfig.level_price = !!body.level_price;
if (typeof body.level_news !== "undefined") tokenConfig.level_news = !!body.level_news;
if (typeof body.level_tips !== "undefined") tokenConfig.level_tips = !!body.level_tips;
if (typeof body.lang !== "undefined") tokenConfig.lang = String(body.lang);
if (typeof body.app_version !== "undefined") tokenConfig.app_version = String(body.app_version);
tokenConfig.last_online = new Date();
}
try {
await this.tokenConfigurationRepository.save(tokenConfig);
} catch (error) {
console.warn(error.message);
}
response.status(200).send("");
}
async enqueue(request: Request, response: Response, next: NextFunction) {
const body: paths["/enqueue"]["post"]["requestBody"]["content"]["application/json"] = request.body;
process.env.VERBOSE && console.log("enqueueing", body);
await this.sendQueueRepository.save({
data: JSON.stringify(body),
});
response.status(200).send("");
}
async getTokenConfiguration(request: Request, response: Response, next: NextFunction) {
const body: paths["/getTokenConfiguration"]["post"]["requestBody"]["content"]["application/json"] = request.body;
let tokenConfig = await this.tokenConfigurationRepository.findOneBy({ token: body.token, os: body.os });
if (!tokenConfig) {
tokenConfig = new TokenConfiguration();
tokenConfig.token = body.token;
tokenConfig.os = body.os;
await this.tokenConfigurationRepository.save(tokenConfig);
}
const config: paths["/getTokenConfiguration"]["post"]["responses"]["200"]["content"]["application/json"] = {
level_all: tokenConfig.level_all,
level_news: tokenConfig.level_news,
level_price: tokenConfig.level_price,
level_transactions: tokenConfig.level_transactions,
level_tips: tokenConfig.level_tips,
lang: tokenConfig.lang,
app_version: tokenConfig.app_version,
};
return config;
}
}