2022-06-04 14:10:44 +02:00
|
|
|
import array
|
2022-09-11 08:13:22 +02:00
|
|
|
import asyncio
|
2021-06-25 18:40:27 +02:00
|
|
|
import json
|
2022-09-10 07:43:16 +02:00
|
|
|
import os
|
2022-06-04 14:10:44 +02:00
|
|
|
import random
|
2022-09-11 08:13:22 +02:00
|
|
|
import re
|
2022-06-04 14:10:44 +02:00
|
|
|
import time
|
2022-09-11 08:13:22 +02:00
|
|
|
import warnings
|
2022-09-10 13:53:09 +02:00
|
|
|
from typing import Dict, Optional
|
2021-06-25 18:40:27 +02:00
|
|
|
|
2021-10-12 21:36:15 +02:00
|
|
|
from fastapi.encoders import jsonable_encoder
|
2021-07-09 19:45:29 +02:00
|
|
|
from fastapi_plugins import redis_plugin
|
2023-04-21 22:58:29 +02:00
|
|
|
from loguru import logger
|
2021-06-13 10:16:54 +02:00
|
|
|
|
2022-10-03 20:22:00 +02:00
|
|
|
from app.api.sse_manager import SSEManager
|
2022-09-10 13:53:09 +02:00
|
|
|
from app.external.sse_starlette import ServerSentEvent
|
|
|
|
|
|
2022-09-10 15:48:03 +02:00
|
|
|
sse_mgr = SSEManager()
|
|
|
|
|
sse_mgr.setup()
|
2022-09-10 13:53:09 +02:00
|
|
|
|
2021-07-10 17:15:02 +02:00
|
|
|
|
2022-07-13 07:08:00 +02:00
|
|
|
class ProcessResult:
|
|
|
|
|
return_code: int
|
|
|
|
|
stdout: str
|
|
|
|
|
stderr: str
|
|
|
|
|
|
|
|
|
|
def __init__(self, return_code, stdout, stderr) -> None:
|
|
|
|
|
self.return_code = return_code
|
|
|
|
|
self.stdout = stdout
|
|
|
|
|
self.stderr = stderr
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
2023-05-17 16:02:28 +02:00
|
|
|
return (
|
|
|
|
|
f"ProcessResult: \nreturn_code: {self.return_code}\n"
|
|
|
|
|
f"stdout: {self.stdout}\n"
|
|
|
|
|
f"stderr: {self.stderr}"
|
|
|
|
|
)
|
2022-07-13 07:08:00 +02:00
|
|
|
|
|
|
|
|
|
2022-09-10 15:48:03 +02:00
|
|
|
def build_sse_event(event: str, json_data: Optional[Dict]):
|
|
|
|
|
return ServerSentEvent(
|
|
|
|
|
event=event,
|
|
|
|
|
data=json.dumps(jsonable_encoder(json_data)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2022-09-10 15:52:11 +02:00
|
|
|
async def broadcast_sse_msg(event: str, json_data: Optional[Dict]):
|
|
|
|
|
"""Broadcasts a message to all connected clients
|
2021-07-09 19:45:29 +02:00
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
2022-09-10 13:53:09 +02:00
|
|
|
event : str
|
|
|
|
|
The SSE event
|
|
|
|
|
data : dictionary, optional
|
2021-07-09 19:45:29 +02:00
|
|
|
The data to include
|
|
|
|
|
"""
|
|
|
|
|
|
2022-09-10 15:48:03 +02:00
|
|
|
await sse_mgr.broadcast_to_all(build_sse_event(event, json_data))
|
2021-07-09 19:45:29 +02:00
|
|
|
|
|
|
|
|
|
2021-12-25 16:53:42 +01:00
|
|
|
async def redis_get(key: str) -> str:
|
|
|
|
|
v = await redis_plugin.redis.get(key)
|
|
|
|
|
|
|
|
|
|
if not v:
|
2022-09-11 19:33:46 +02:00
|
|
|
logstr = f"Key '{key}' not found in Redis DB."
|
|
|
|
|
if "tor_web_addr" in key:
|
2023-04-21 22:58:29 +02:00
|
|
|
logger.info(logstr)
|
2022-09-11 19:33:46 +02:00
|
|
|
else:
|
2023-04-21 22:58:29 +02:00
|
|
|
logger.warning(logstr)
|
2021-12-25 16:53:42 +01:00
|
|
|
return ""
|
|
|
|
|
|
2022-09-11 19:33:46 +02:00
|
|
|
try:
|
|
|
|
|
return v.decode("utf-8")
|
|
|
|
|
except AttributeError:
|
|
|
|
|
return v
|
2021-12-25 16:53:42 +01:00
|
|
|
|
|
|
|
|
|
2022-03-16 09:46:58 +01:00
|
|
|
# TODO
|
2023-05-17 16:02:28 +02:00
|
|
|
# idea is to have a second redis channel called system, that the API subscribes to.
|
|
|
|
|
# If for example the 'state' value gets changed by the _cache.sh script, it should
|
|
|
|
|
# publish this to this channel so the API can forward the change to thru the SSE to
|
|
|
|
|
# the WebUI
|
2022-03-16 09:46:58 +01:00
|
|
|
|
2022-03-20 10:04:00 +01:00
|
|
|
|
2021-09-05 08:56:53 +02:00
|
|
|
class SSE:
|
2021-10-05 19:34:06 +02:00
|
|
|
SYSTEM_INFO = "system_info"
|
2022-03-24 20:17:56 +01:00
|
|
|
SYSTEM_SHUTDOWN_NOTICE = "system_shutdown_initiated"
|
|
|
|
|
SYSTEM_SHUTDOWN_ERROR = "system_shutdown_error"
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
SYSTEM_STARTUP_INFO = "system_startup_info"
|
2022-03-24 20:17:56 +01:00
|
|
|
SYSTEM_REBOOT_NOTICE = "system_reboot_initiated"
|
|
|
|
|
SYSTEM_REBOOT_ERROR = "system_reboot_error"
|
2021-10-05 19:34:06 +02:00
|
|
|
HARDWARE_INFO = "hardware_info"
|
|
|
|
|
|
2022-05-05 13:27:59 +02:00
|
|
|
INSTALL_APP = "install"
|
2021-10-05 19:34:06 +02:00
|
|
|
INSTALLED_APP_STATUS = "installed_app_status"
|
2021-07-09 19:45:29 +02:00
|
|
|
|
2021-07-18 07:59:19 +02:00
|
|
|
BTC_NETWORK_STATUS = "btc_network_status"
|
|
|
|
|
BTC_MEMPOOL_STATUS = "btc_mempool_status"
|
|
|
|
|
BTC_NEW_BLOC = "btc_new_bloc"
|
|
|
|
|
BTC_INFO = "btc_info"
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-08-02 20:30:18 +02:00
|
|
|
LN_INFO = "ln_info"
|
2021-10-05 19:34:06 +02:00
|
|
|
LN_INFO_LITE = "ln_info_lite"
|
2021-07-25 18:15:26 +02:00
|
|
|
LN_INVOICE_STATUS = "ln_invoice_status"
|
2021-07-27 21:10:25 +02:00
|
|
|
LN_PAYMENT_STATUS = "ln_payment_status"
|
2021-10-03 20:55:28 +02:00
|
|
|
LN_ONCHAIN_PAYMENT_STATUS = "ln_onchain_payment_status"
|
2022-01-15 14:27:08 +01:00
|
|
|
LN_FEE_REVENUE = "ln_fee_revenue"
|
|
|
|
|
LN_FORWARD_SUCCESSES = "ln_forward_successes"
|
2021-08-03 21:20:24 +02:00
|
|
|
WALLET_BALANCE = "wallet_balance"
|
2022-05-12 19:49:01 +02:00
|
|
|
|
|
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
# https://gist.github.com/risent/4cab3878d995bec7d1c2
|
|
|
|
|
# https://firebase.blog/posts/2015/02/the-2120-ways-to-ensure-unique_68
|
|
|
|
|
# https://gist.github.com/mikelehen/3596a30bd69384624c11
|
|
|
|
|
class _PushID(object):
|
|
|
|
|
# Modeled after base64 web-safe chars, but ordered by ASCII.
|
|
|
|
|
PUSH_CHARS = (
|
|
|
|
|
"-0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "_abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
# Timestamp of last push, used to prevent local collisions if you
|
2022-06-04 14:46:03 +02:00
|
|
|
# push twice in one ms.
|
2022-06-04 14:10:44 +02:00
|
|
|
self.last_push_time = 0
|
|
|
|
|
|
|
|
|
|
# We generate 72-bits of randomness which get turned into 12
|
|
|
|
|
# characters and appended to the timestamp to prevent
|
|
|
|
|
# collisions with other clients. We store the last characters
|
|
|
|
|
# we generated because in the event of a collision, we'll use
|
|
|
|
|
# those same characters except "incremented" by one.
|
|
|
|
|
self.last_rand_chars = array.array("i", [i for i in range(12)])
|
|
|
|
|
|
|
|
|
|
def next_id(self):
|
|
|
|
|
now = int(time.time() * 1000)
|
|
|
|
|
duplicate_time = now == self.last_push_time
|
|
|
|
|
self.last_push_time = now
|
|
|
|
|
time_stamp_chars = array.array("u", "12345678")
|
|
|
|
|
|
|
|
|
|
for i in range(7, -1, -1):
|
|
|
|
|
time_stamp_chars[i] = self.PUSH_CHARS[now % 64]
|
|
|
|
|
now = int(now / 64)
|
|
|
|
|
|
|
|
|
|
if now != 0:
|
|
|
|
|
raise ValueError("We should have converted the entire timestamp.")
|
|
|
|
|
|
|
|
|
|
uid = "".join(time_stamp_chars)
|
|
|
|
|
|
|
|
|
|
if not duplicate_time:
|
|
|
|
|
for i in range(12):
|
|
|
|
|
self.last_rand_chars[i] = int(random.random() * 64)
|
|
|
|
|
else:
|
|
|
|
|
# If the timestamp hasn't changed since last push, use the
|
|
|
|
|
# same random number, except incremented by 1.
|
|
|
|
|
for i in range(11, -1, -1):
|
|
|
|
|
if self.last_rand_chars[i] == 63:
|
|
|
|
|
self.last_rand_chars[i] = 0
|
|
|
|
|
else:
|
|
|
|
|
break
|
|
|
|
|
self.last_rand_chars[i] += 1
|
|
|
|
|
|
|
|
|
|
for i in range(12):
|
|
|
|
|
uid += self.PUSH_CHARS[self.last_rand_chars[i]]
|
|
|
|
|
|
|
|
|
|
if len(uid) != 20:
|
|
|
|
|
raise ValueError("Length should be 20.")
|
|
|
|
|
|
|
|
|
|
return uid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pid_gen = _PushID()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def next_push_id() -> str:
|
|
|
|
|
"""Generates a unique random 20 character long string id
|
|
|
|
|
|
|
|
|
|
* They're based on timestamp so that they sort *after* any existing ids.
|
2023-05-17 16:02:28 +02:00
|
|
|
* They contain 72-bits of random data after the timestamp so that IDs won't collide
|
|
|
|
|
* with other clients' IDs. They sort *lexicographically* (so the timestamp is
|
|
|
|
|
* converted to characters that will sort properly).
|
|
|
|
|
* They're monotonically increasing. Even if you generate more than one in the same
|
|
|
|
|
* timestamp, the latter ones will sort after the former ones. We do this by using
|
|
|
|
|
* the previous random bits but "incrementing" them by 1 (only in the case of a
|
|
|
|
|
* timestamp collision).
|
2022-06-04 14:10:44 +02:00
|
|
|
"""
|
|
|
|
|
return pid_gen.next_id()
|
2022-09-10 07:43:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def config_get_hex_str(value: str, name: str = "") -> str:
|
|
|
|
|
if value is None or len(value) == 0:
|
|
|
|
|
raise ValueError(f"{name} cannot be null or empty")
|
|
|
|
|
|
2022-09-11 19:33:46 +02:00
|
|
|
if _is_hex(value):
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(value):
|
|
|
|
|
raise ValueError(f"{name} is not a valid path")
|
|
|
|
|
|
|
|
|
|
with open(value, "rb") as f:
|
|
|
|
|
m = f.read()
|
|
|
|
|
m = m.hex()
|
|
|
|
|
return m
|
|
|
|
|
|
2022-09-10 07:43:16 +02:00
|
|
|
|
2022-09-11 19:33:46 +02:00
|
|
|
def _is_hex(s):
|
|
|
|
|
try:
|
|
|
|
|
int(s, 16)
|
|
|
|
|
return True
|
|
|
|
|
except ValueError:
|
|
|
|
|
return False
|
2022-09-11 08:13:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def call_script(scriptPath) -> str:
|
|
|
|
|
warnings.warn("call_script is deprecated. Use call_script2 instead.")
|
|
|
|
|
|
|
|
|
|
cmd = f"bash {scriptPath}"
|
|
|
|
|
proc = await asyncio.create_subprocess_shell(
|
|
|
|
|
cmd,
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
)
|
|
|
|
|
stdout, stderr = await proc.communicate()
|
|
|
|
|
if stdout:
|
|
|
|
|
return stdout.decode()
|
|
|
|
|
if stderr:
|
2023-04-21 22:58:29 +02:00
|
|
|
logger.error(stderr.decode())
|
2022-09-11 08:13:22 +02:00
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def call_script2(script_path) -> ProcessResult:
|
|
|
|
|
"""
|
|
|
|
|
Call a local bash script and return the results
|
|
|
|
|
|
|
|
|
|
:param str script_path: full path with arguments
|
|
|
|
|
:return: The process result
|
|
|
|
|
:rtype: ProcessResult
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
cmd = f"bash {script_path}"
|
|
|
|
|
proc = await asyncio.create_subprocess_shell(
|
|
|
|
|
cmd,
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
)
|
|
|
|
|
stdout, stderr = await proc.communicate()
|
|
|
|
|
|
|
|
|
|
return ProcessResult(
|
|
|
|
|
proc.returncode,
|
|
|
|
|
stdout.decode() if stdout else "",
|
|
|
|
|
stderr.decode() if stderr else "",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def call_sudo_script(scriptPath) -> str:
|
|
|
|
|
cmd = f"sudo bash {scriptPath}"
|
|
|
|
|
proc = await asyncio.create_subprocess_shell(
|
|
|
|
|
cmd,
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
)
|
|
|
|
|
stdout, stderr = await proc.communicate()
|
|
|
|
|
if stdout:
|
|
|
|
|
return stdout.decode()
|
|
|
|
|
if stderr:
|
2023-04-21 22:58:29 +02:00
|
|
|
logger.error(stderr.decode())
|
2022-09-11 08:13:22 +02:00
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_key_value_lines(lines: list) -> dict:
|
|
|
|
|
Dict = {}
|
|
|
|
|
for line in lines:
|
|
|
|
|
line = line.strip()
|
|
|
|
|
if len(line) == 0:
|
|
|
|
|
continue
|
|
|
|
|
if not re.match("^[a-zA-Z0-9]*=", line):
|
|
|
|
|
continue
|
|
|
|
|
key, value = line.strip().split("=", 1)
|
|
|
|
|
Dict[key] = value.strip('"').strip("'")
|
|
|
|
|
return Dict
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_key_value_text(text: str) -> dict:
|
|
|
|
|
return parse_key_value_lines(text.splitlines())
|