aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-01-31 14:35:52 -0800
committerAdam Soltys <[email protected]>2024-01-31 14:35:52 -0800
commit25d57b726f158f7eb142b109c3a51e2c84bad210 (patch)
tree5a1e18122f5322f2cd17475b2ba564a91735fe75
parent0c223a4835711a287f17835af959b47f72ea68c9 (diff)
downloadcoinos-server-25d57b726f158f7eb142b109c3a51e2c84bad210.tar.gz
coinos-server-25d57b726f158f7eb142b109c3a51e2c84bad210.zip
withdrawal emails
-rw-r--r--lib/locales/de.json7
-rw-r--r--lib/locales/el.json7
-rw-r--r--lib/locales/en.json7
-rw-r--r--lib/locales/es.json7
-rw-r--r--lib/locales/fa.json7
-rw-r--r--lib/locales/fr.json7
-rw-r--r--lib/locales/guided_translation.py169
-rw-r--r--lib/locales/index.js5
-rw-r--r--lib/locales/ja.json7
-rw-r--r--lib/locales/pt.json7
-rw-r--r--lib/locales/ru.json7
-rw-r--r--lib/locales/zh.json7
-rw-r--r--lib/mail.js3
-rw-r--r--lib/payments.js78
-rw-r--r--routes/payments.js8
-rw-r--r--templates/payments/received.html27
16 files changed, 320 insertions, 40 deletions
diff --git a/lib/locales/de.json b/lib/locales/de.json
new file mode 100644
index 00000000..24a8c895
--- /dev/null
+++ b/lib/locales/de.json
@@ -0,0 +1,7 @@
+{
+ "hello": "Hallo",
+ "youReceived": "Sie haben eine Zahlung von erhalten",
+ "withdrawalTriggered": "Dies löste einen Rückzug von aus",
+ "problemWithdrawing": "Beim Senden an Ihre automatische Auszahlungsadresse ist ein Problem aufgetreten",
+ "paymentReceived": "Zahlung erhalten"
+}
diff --git a/lib/locales/el.json b/lib/locales/el.json
new file mode 100644
index 00000000..4cef5bad
--- /dev/null
+++ b/lib/locales/el.json
@@ -0,0 +1,7 @@
+{
+ "hello": "Γειά σου",
+ "youReceived": "Λάβατε μια πληρωμή του",
+ "withdrawalTriggered": "Αυτό προκάλεσε την απόσυρση του",
+ "problemWithdrawing": "Παρουσιάστηκε πρόβλημα κατά την αποστολή στη διεύθυνση αυτόματης απόσυρσής σας",
+ "paymentReceived": "Η πληρωμή ελήφθη"
+}
diff --git a/lib/locales/en.json b/lib/locales/en.json
new file mode 100644
index 00000000..73cfbd3f
--- /dev/null
+++ b/lib/locales/en.json
@@ -0,0 +1,7 @@
+{
+ "paymentReceived": "Payment received",
+ "hello": "Hello",
+ "youReceived": "You received a payment of",
+ "withdrawalTriggered": "This triggered a withdrawal of",
+ "problemWithdrawing": "There was a problem sending to your autowithdrawal address"
+}
diff --git a/lib/locales/es.json b/lib/locales/es.json
new file mode 100644
index 00000000..635d04b3
--- /dev/null
+++ b/lib/locales/es.json
@@ -0,0 +1,7 @@
+{
+ "hello": "Hola",
+ "youReceived": "Recibiste un pago de",
+ "withdrawalTriggered": "Esto provocó la retirada de",
+ "problemWithdrawing": "Hubo un problema al enviar a su dirección de retiro automático",
+ "paymentReceived": "Pago recibido"
+}
diff --git a/lib/locales/fa.json b/lib/locales/fa.json
new file mode 100644
index 00000000..0c7d87b2
--- /dev/null
+++ b/lib/locales/fa.json
@@ -0,0 +1,7 @@
+{
+ "hello": "سلام",
+ "youReceived": "شما پرداختی دریافت کردید",
+ "withdrawalTriggered": "این باعث خروج از",
+ "problemWithdrawing": "مشکلی در ارسال به آدرس برداشت خودکار شما وجود داشت",
+ "paymentReceived": "پرداخت دریافت شد"
+}
diff --git a/lib/locales/fr.json b/lib/locales/fr.json
new file mode 100644
index 00000000..72c27f92
--- /dev/null
+++ b/lib/locales/fr.json
@@ -0,0 +1,7 @@
+{
+ "hello": "Bonjour",
+ "youReceived": "Vous avez reçu un paiement de",
+ "withdrawalTriggered": "Cela a déclenché un retrait de",
+ "problemWithdrawing": "Un problème est survenu lors de l'envoi à votre adresse de retrait automatique",
+ "paymentReceived": "Paiement reçu"
+}
diff --git a/lib/locales/guided_translation.py b/lib/locales/guided_translation.py
new file mode 100644
index 00000000..27342314
--- /dev/null
+++ b/lib/locales/guided_translation.py
@@ -0,0 +1,169 @@
+#!/usr/bin/python
+
+import argparse, json, logging, sys, requests, os
+from pathlib import Path
+
+DEFAULT_FROM_LOCALE = 'en'
+LOCALE_DATA_FILEPATH = '../lib/translations/lang.json'
+
+def nested_dict_keys(d, _keyname_prefix=''):
+ entry_list = []
+ for key in d:
+ value = d[key]
+ if isinstance(value, dict):
+ entry_list += nested_dict_keys(value, _keyname_prefix + key + '.')
+ else:
+ entry_list += [_keyname_prefix + key]
+ return entry_list
+
+def create_locale(locale_code):
+ logging.debug("Creating locale %s...", locale_code)
+ with open(LOCALE_DATA_FILEPATH, 'r', encoding='utf-8') as lang_file:
+ lang_data = json.load(lang_file)
+
+ lang_data[locale_code] = input("What is the name of the language " + locale_code + "? ")
+
+ with open(LOCALE_DATA_FILEPATH, 'w') as lang_file:
+ json.dump(lang_data, lang_file, ensure_ascii=False, indent='\t')
+ lang_file.write('\n')
+ logging.debug("Created locale %s.", locale_code)
+
+def localize_string(string_id, from_data, to_data, to_locale):
+ api_key = os.environ.get('GOOGLE_API_KEY')
+ logging.debug("Translating string with id %s", string_id)
+ id_parts = string_id.split('.')
+
+ # get original string
+ to_translate = from_data
+ for id_part in id_parts:
+ to_translate = to_translate[id_part]
+
+ # Google Translate API endpoint
+ url = "https://translation.googleapis.com/language/translate/v2"
+
+ # Prepare data for POST request
+ data = {
+ 'q': to_translate,
+ 'source': 'en', # assuming the source language is English
+ 'target': to_locale,
+ 'format': 'text'
+ }
+
+ # Headers for the request
+ headers = {"Content-Type": "application/json"}
+
+ # Make the request
+ response = requests.post(url, headers=headers, params={'key': api_key}, json=data)
+
+ # Check if the request was successful
+ if response.status_code == 200:
+ # Parse the response and extract translation
+ translated_text = response.json()['data']['translations'][0]['translatedText']
+ print("Translated text:", translated_text)
+ else:
+ print("Error while translating:", response.json())
+ translated_text = None
+
+ # Proceed only if translation was successful
+ if translated_text:
+ # find/create place to put translated string
+ translation_location = to_data # the dictionary or list that should contain the translated string
+ for id_part in id_parts[:-1]:
+ if id_part not in translation_location:
+ translation_location[id_part] = {}
+ translation_location = translation_location[id_part]
+
+ translation_location[id_parts[-1]] = translated_text
+
+if __name__ == "__main__":
+ # setup and parse commandline arguments
+ args_parser = argparse.ArgumentParser(description="guided translation of coinos")
+ args_parser.add_argument('from_locale', nargs='?', type=str,
+ help="which locale to translate from")
+ args_parser.add_argument('to_locale', nargs='?', type=str,
+ help="which locale to translate to")
+ args_parser.add_argument('-v', '--verbose', action='count', default=0,
+ help="show more information (use twice for even more)")
+ args_parser.add_argument('-V', '--version', action='store_true',
+ help="show version info and exit")
+ args = args_parser.parse_args()
+
+ if args.version:
+ print("Guided Translation Script version 0.3.0")
+ sys.exit(0)
+
+ from_locale = args.from_locale or input("Which locale do you want to translate from (default: %s)? "
+ % DEFAULT_FROM_LOCALE) or DEFAULT_FROM_LOCALE
+ to_locale = args.to_locale or input("Which locale do you want to translate to? ")
+ if not to_locale:
+ raise ValueError("You need to specify a language to translate to.")
+
+ if args.verbose >= 2:
+ logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
+ elif args.verbose >= 1:
+ logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO)
+ else:
+ logging.basicConfig(format='%(levelname)s: %(message)s')
+
+ # load locale files
+ logging.debug("Reading file to convert from (%s.json)", from_locale)
+ with open(from_locale + ".json", 'r', encoding='utf-8') as from_file:
+ from_json_text = from_file.read()
+ logging.debug("Converting from data from JSON to object")
+ from_data = json.loads(from_json_text)
+ from_id_list = nested_dict_keys(from_data)
+ logging.info("Successfully obtained %d strings from locale %s",
+ len(from_id_list), from_locale)
+ logging.debug("List of strings: %s", from_id_list)
+
+ logging.debug("Reading file to convert to (%s.json)", to_locale)
+ to_filepath = "./" + to_locale + ".json"
+ if Path(to_filepath).exists():
+ with open(to_locale + ".json", 'r', encoding='utf-8') as to_file:
+ to_json_text = to_file.read()
+ logging.debug("Converting to data from JSON to object")
+ to_data = json.loads(to_json_text)
+ else:
+ logging.info("No file found. Creating new locale.")
+ create_locale(to_locale)
+ to_data = {}
+ to_id_list = nested_dict_keys(to_data)
+ logging.info("Successfully obtained %d strings from locale %s",
+ len(to_id_list), to_locale)
+ logging.debug("List of strings: %s", to_id_list)
+
+ # offer to remove strings in to list that were removed from from list
+ removed_strings = []
+ for string_id in to_id_list:
+ if string_id not in from_id_list:
+ removed_strings.append(string_id)
+
+ if removed_strings:
+ print("""There are %d pieces of text that are in the translated locale (%s) but not the original locale (%s).
+The IDs of these pieces of text are: %s
+This could mean that this text was translated first, or it could mean this text was removed from the original locale but has yet to be removed from the translated locale. In the latter case, you may want to remove them from the translated locale file.
+""" % (len(removed_strings), to_locale, from_locale, removed_strings), file=sys.stderr)
+
+ untranslated_strings = []
+ for string_id in from_id_list:
+ if string_id not in to_id_list:
+ untranslated_strings.append(string_id)
+
+ if untranslated_strings:
+ print("There are %d pieces of text to translate: %s"
+ % (len(untranslated_strings), untranslated_strings))
+ # localize strings!
+ try:
+ for string_id in untranslated_strings:
+ localize_string(string_id, from_data, to_data, to_locale)
+ to_id_list.append(string_id)
+ except KeyboardInterrupt:
+ print("\nProgram interrupted. Saving strings...")
+ else:
+ print("Translations already completed.")
+
+ # save localized data
+ with open(to_locale + ".json", 'w', encoding='utf-8') as to_file:
+ json.dump(to_data, to_file, ensure_ascii=False, indent='\t')
+ to_file.write('\n')
+ logging.info("Saved %d localized strings to %s.json", len(to_id_list), to_locale)
diff --git a/lib/locales/index.js b/lib/locales/index.js
new file mode 100644
index 00000000..3416a31b
--- /dev/null
+++ b/lib/locales/index.js
@@ -0,0 +1,5 @@
+import fs from "fs";
+export default "de,en,fa,ja,ru,el,es,fr,pt,zh".split(",").reduce((a, l) => {
+ a[l] = JSON.parse(fs.readFileSync(`lib/locales/${l}.json`));
+ return a;
+}, {});
diff --git a/lib/locales/ja.json b/lib/locales/ja.json
new file mode 100644
index 00000000..544ae31a
--- /dev/null
+++ b/lib/locales/ja.json
@@ -0,0 +1,7 @@
+{
+ "hello": "こんにちは",
+ "youReceived": "の支払いを受けました",
+ "withdrawalTriggered": "これが撤退のきっかけとなった",
+ "problemWithdrawing": "自動引き出しアドレスへの送信中に問題が発生しました",
+ "paymentReceived": "お支払い頂いた"
+}
diff --git a/lib/locales/pt.json b/lib/locales/pt.json
new file mode 100644
index 00000000..2e96b146
--- /dev/null
+++ b/lib/locales/pt.json
@@ -0,0 +1,7 @@
+{
+ "hello": "Olá",
+ "youReceived": "Você recebeu um pagamento de",
+ "withdrawalTriggered": "Isto desencadeou uma retirada de",
+ "problemWithdrawing": "Ocorreu um problema ao enviar para seu endereço de retirada automática",
+ "paymentReceived": "Pagamento recebido"
+}
diff --git a/lib/locales/ru.json b/lib/locales/ru.json
new file mode 100644
index 00000000..f61680ce
--- /dev/null
+++ b/lib/locales/ru.json
@@ -0,0 +1,7 @@
+{
+ "hello": "Привет",
+ "youReceived": "Вы получили выплату в размере",
+ "withdrawalTriggered": "Это послужило толчком к выводу",
+ "problemWithdrawing": "Возникла проблема с отправкой на ваш адрес автоматического вывода средств.",
+ "paymentReceived": "Платеж получен"
+}
diff --git a/lib/locales/zh.json b/lib/locales/zh.json
new file mode 100644
index 00000000..dbc579e3
--- /dev/null
+++ b/lib/locales/zh.json
@@ -0,0 +1,7 @@
+{
+ "hello": "你好",
+ "youReceived": "您收到了一笔付款",
+ "withdrawalTriggered": "这引发了撤回",
+ "problemWithdrawing": "发送至您的自动提款地址时出现问题",
+ "paymentReceived": "已收到付款"
+}
diff --git a/lib/mail.js b/lib/mail.js
index f71806ec..368ad417 100644
--- a/lib/mail.js
+++ b/lib/mail.js
@@ -23,6 +23,7 @@ export let mail = async (user, subject, template, params) => {
let html = handlebars.compile(source)(params);
let client = new SESClient({ region: "us-east-2" });
+ params.URL = process.env.URL;
await client.send(
new SendEmailCommand({
@@ -35,7 +36,7 @@ export let mail = async (user, subject, template, params) => {
},
Subject: { Charset, Data: subject },
},
- Source: config.support,
+ Source: `"Coinos " <${config.support}>`,
}),
);
} catch (e) {
diff --git a/lib/payments.js b/lib/payments.js
index ee030a5e..7f83f09a 100644
--- a/lib/payments.js
+++ b/lib/payments.js
@@ -1,3 +1,4 @@
+import locales from "$lib/locales/index";
import config from "$config";
import store from "$lib/store";
import { emit } from "$lib/sockets";
@@ -15,6 +16,8 @@ import lq from "$lib/liquid";
import bc from "$lib/bitcoin";
import ln from "$lib/ln";
+let { URL } = process.env;
+
let api = {
bitcoin: "https://mempool.space/api/v1",
liquid: "https://liquid.network/api/v1",
@@ -158,11 +161,11 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => {
if ([types.bitcoin, types.liquid].includes(type)) inv.pending += amount;
else inv.received += amount;
- let balance = "balance";
+ let balanceKey = "balance";
if ([types.bitcoin, types.liquid].includes(type)) {
let [txid, vout] = ref.split(":").slice(-2);
p.confirmed = false;
- balance = "pending";
+ balanceKey = "pending";
await s(`payment:${txid}:${vout}`, id);
} else {
await s(`payment:${hash}`, id);
@@ -176,7 +179,7 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => {
m.set(`invoice:${inv.iid}`, JSON.stringify(inv))
.set(`payment:${p.id}`, JSON.stringify(p))
.lPush(`${uid}:payments`, p.id)
- .incrBy(`${balance}:${uid}`, amount)
+ .incrBy(`${balanceKey}:${uid}`, amount)
.exec();
emit(uid, "payment", p);
@@ -184,18 +187,6 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => {
callWebhook(inv, p);
l(username, "received", type, amount);
- if (user.verified && user.notify) {
- mail(user, "Payment received", templates.paymentReceived, {
- username,
- sats:
- "⚡️" +
- new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(
- p.amount,
- ),
- link: `${process.env.URL}/payment/${p.id}`,
- });
- }
-
if (config.mqtt1) {
mqtt1.publish(
username,
@@ -210,22 +201,47 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => {
);
}
- if (p.confirmed) autowithdraw(user);
-
+ await completePayment(p, user);
return p;
};
-export let autowithdraw = async (user) => {
- let { username, autowithdraw, id, destination, threshold, reserve } = user;
- try {
+let fmt = (sats) =>
+ "⚡️" +
+ new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(sats);
+
+export let completePayment = async (p, user) => {
+ let { id, autowithdraw, threshold, reserve, destination, username } = user;
+ let link = (id) => `${URL}/payment/${id}`;
+ if (p.confirmed) {
+ let withdrawal;
if (autowithdraw) {
- let to = destination.trim();
- let balance = await g(`balance:${id}`);
- let amount = balance - reserve;
- if (balance > threshold) await pay({ amount, to, user });
+ try {
+ let to = destination.trim();
+ let balance = await g(`balance:${id}`);
+ let amount = balance - reserve;
+ if (balance > threshold) {
+ l("initiating autowithdrawal", amount, to, balance, threshold);
+ let w = await pay({ amount, to, user });
+ withdrawal = {
+ amount: fmt(-w.amount),
+ link: link(w.id),
+ };
+ }
+ } catch (e) {
+ withdrawal = { failed: true };
+ warn(username, "autowithdraw failed", e.message);
+ }
+ }
+
+ let t = locales[user.language || "en"];
+ if (user.verified && user.notify) {
+ mail(user, t.paymentReceived, templates.paymentReceived, {
+ ...t,
+ username,
+ payment: { amount: fmt(p.amount), link: link(p.id) },
+ withdrawal,
+ });
}
- } catch (e) {
- warn(username, "autowithdraw failed", e);
}
};
@@ -251,8 +267,9 @@ let pay = async ({ amount, to, user }) => {
pr = to;
}
- if (pr) await sendLightning({ user, pr, amount, maxfee });
- else await sendOnchain({ amount, address: to, user, subtract: true });
+ return pr
+ ? await sendLightning({ user, pr, amount, maxfee })
+ : await sendOnchain({ amount, address: to, user, subtract: true });
};
let decode = async (hex) => {
@@ -338,7 +355,7 @@ export let sendOnchain = async (params) => {
let amount = total - change;
- await debit({
+ let p = await debit({
hash: txid,
amount,
fee,
@@ -347,7 +364,8 @@ export let sendOnchain = async (params) => {
type,
});
- return node.sendRawTransaction(signed);
+ await node.sendRawTransaction(signed);
+ return p;
};
let getMaxFee = (n) =>
diff --git a/routes/payments.js b/routes/payments.js
index f8f8af3d..7d568773 100644
--- a/routes/payments.js
+++ b/routes/payments.js
@@ -7,7 +7,7 @@ import { l, err, warn } from "$lib/logging";
import { bail, fail, getInvoice, btc, sats, SATS } from "$lib/utils";
import { requirePin } from "$lib/auth";
import {
- autowithdraw,
+ completePayment,
debit,
credit,
types,
@@ -30,7 +30,7 @@ export default {
async create({ body, user }, res) {
let { amount, hash, maxfee, fund, memo, payreq, rate, username } = body;
- let balance = await g(`balance:${user.id}`);
+ let balance = await g(`balance:${user.id}`);
try {
if (await g("freeze")) fail("Problem sending payment");
@@ -272,7 +272,7 @@ export default {
emit(p.uid, "payment", p);
let user = await g(`user:${p.uid}`);
- autowithdraw(user);
+ await completePayment(p, user);
}
}
}
@@ -305,7 +305,7 @@ export default {
async send({ body, user }, res) {
try {
await requirePin({ body, user });
- let txid = await sendOnchain({ ...body, user });
+ let { hash: txid } = await sendOnchain({ ...body, user });
res.send({ txid });
} catch (e) {
diff --git a/templates/payments/received.html b/templates/payments/received.html
index e27356c1..937e093c 100644
--- a/templates/payments/received.html
+++ b/templates/payments/received.html
@@ -8,17 +8,34 @@
<html>
<body>
- <div style="margin-bottom: 10px">Hello {{username}},</div>
+ <div style="margin-bottom: 10px">{{hello}} {{username}},</div>
<div style="margin-bottom: 10px">
- You received a payment of {{sats}}
+ {{youReceived}} {{payment.amount}}
</div>
<div style="margin-bottom: 10px">
- <a href="{{link}}">{{link}}</a>
+ <a href="{{payment.link}}">{{payment.link}}</a>
</div>
- <div>Regards,</div>
- <div>The Coinos Team</div>
+ {{#if withdrawal.amount}}
+ <div style="margin-bottom: 10px">
+ {{withdrawalTriggered}} {{withdrawal.amount}}
+ </div>
+
+ <div style="margin-bottom: 10px">
+ <a href="{{withdrawal.link}}">{{withdrawal.link}}</a>
+ </div>
+ {{else if withdrawal.failed}}
+ <div style="margin-bottom: 10px">
+ {{problemWithdrawing}}
+ </div>
+ {{/if}}
+
+ <a href="{{payment.link}}">
+ <img src="{{URL}}/images/logo.webp" style="width: 200px; margin: 50px;"/>
+ </a>
+
+ <a href="{{URL}}/unsubscribe">Unsubscribe</a>
</body>
</html>