fix: stop updating internal invoices inside the listener unlike external one (#3984)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
@@ -140,22 +140,18 @@ class Payment(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||||
async def check_status(
|
async def check_status(self) -> PaymentStatus:
|
||||||
self, skip_internal_payment_notifications: bool | None = False
|
|
||||||
) -> PaymentStatus:
|
|
||||||
logger.warning("payment.check_status() is deprecated.")
|
logger.warning("payment.check_status() is deprecated.")
|
||||||
from lnbits.core.services.payments import check_payment_status
|
from lnbits.core.services.payments import check_payment_status
|
||||||
|
|
||||||
return await check_payment_status(self, skip_internal_payment_notifications)
|
return await check_payment_status(self)
|
||||||
|
|
||||||
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||||
async def check_fiat_status(
|
async def check_fiat_status(self) -> FiatPaymentStatus:
|
||||||
self, skip_internal_payment_notifications: bool | None = False
|
|
||||||
) -> FiatPaymentStatus:
|
|
||||||
logger.warning("payment.check_fiat_status() is deprecated.")
|
logger.warning("payment.check_fiat_status() is deprecated.")
|
||||||
from lnbits.core.services.fiat_providers import check_fiat_status
|
from lnbits.core.services.fiat_providers import check_fiat_status
|
||||||
|
|
||||||
return await check_fiat_status(self, skip_internal_payment_notifications)
|
return await check_fiat_status(self)
|
||||||
|
|
||||||
|
|
||||||
class PaymentFilters(FilterModel):
|
class PaymentFilters(FilterModel):
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ async def handle_fiat_payment_confirmation(
|
|||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
|
||||||
|
|
||||||
async def check_fiat_status(
|
async def check_fiat_status(payment: Payment) -> FiatPaymentStatus:
|
||||||
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
|
||||||
) -> FiatPaymentStatus:
|
|
||||||
if not payment.is_internal:
|
if not payment.is_internal:
|
||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
if payment.success:
|
if payment.success:
|
||||||
@@ -58,10 +56,9 @@ async def check_fiat_status(
|
|||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
||||||
|
|
||||||
if skip_internal_payment_notifications:
|
|
||||||
return fiat_status
|
|
||||||
|
|
||||||
if fiat_status.success:
|
if fiat_status.success:
|
||||||
|
await handle_fiat_payment_confirmation(payment)
|
||||||
|
|
||||||
# notify receivers asynchronously
|
# notify receivers asynchronously
|
||||||
from lnbits.tasks import internal_invoice_queue
|
from lnbits.tasks import internal_invoice_queue
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from lnbits.core.crud.payments import get_daily_stats
|
|||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
|
|
||||||
from lnbits.db import Connection, Filters
|
from lnbits.db import Connection, Filters
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
||||||
@@ -630,18 +629,14 @@ async def check_transaction_status(
|
|||||||
return await check_payment_status(payment)
|
return await check_payment_status(payment)
|
||||||
|
|
||||||
|
|
||||||
async def check_payment_status(
|
async def check_payment_status(payment: Payment) -> PaymentStatus:
|
||||||
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
|
||||||
) -> PaymentStatus:
|
|
||||||
if payment.is_internal:
|
if payment.is_internal:
|
||||||
if payment.success:
|
if payment.success:
|
||||||
return PaymentSuccessStatus()
|
return PaymentSuccessStatus()
|
||||||
if payment.failed:
|
if payment.failed:
|
||||||
return PaymentFailedStatus()
|
return PaymentFailedStatus()
|
||||||
if payment.is_in and payment.fiat_provider:
|
if payment.is_in and payment.fiat_provider:
|
||||||
fiat_status = await check_fiat_status(
|
fiat_status = await check_fiat_status(payment)
|
||||||
payment, skip_internal_payment_notifications
|
|
||||||
)
|
|
||||||
return PaymentStatus(paid=fiat_status.paid)
|
return PaymentStatus(paid=fiat_status.paid)
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
funding_source = get_funding_source()
|
funding_source = get_funding_source()
|
||||||
@@ -783,9 +778,14 @@ async def _pay_internal_invoice(
|
|||||||
await update_payment(internal_payment, conn=conn)
|
await update_payment(internal_payment, conn=conn)
|
||||||
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
||||||
|
|
||||||
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
await _send_payment_notification_in_background(
|
||||||
|
wallet.id, payment, conn=conn
|
||||||
|
) # notify the sender
|
||||||
|
await _send_payment_notification_in_background(
|
||||||
|
internal_payment.wallet_id, internal_payment, conn=conn
|
||||||
|
) # notify the receiver
|
||||||
|
|
||||||
# notify receiver asynchronously
|
# notify receiver asynchronously (extension listeners)
|
||||||
from lnbits.tasks import internal_invoice_queue
|
from lnbits.tasks import internal_invoice_queue
|
||||||
|
|
||||||
logger.debug(f"enqueuing internal invoice {internal_payment.checking_id}")
|
logger.debug(f"enqueuing internal invoice {internal_payment.checking_id}")
|
||||||
@@ -1079,29 +1079,29 @@ async def _send_payment_notification_in_background(
|
|||||||
send_payment_notification_in_background(wallet, payment)
|
send_payment_notification_in_background(wallet, payment)
|
||||||
|
|
||||||
|
|
||||||
async def update_invoice_callback(checking_id: str) -> Payment | None:
|
async def update_invoice_from_paid_invoices_stream(checking_id: str) -> Payment | None:
|
||||||
"""
|
"""
|
||||||
Takes a checking_id of an incoming payment, from either paid_invoices_stream()
|
Takes a checking_id of an incoming payment from paid_invoices_stream()
|
||||||
or internal_invoice_queue. Checks its status, updates and returns it.
|
Checks its status, updates its status and returns it.
|
||||||
returns None if no payment was found or it not and incoming payment.
|
returns None if no incoming payment was found or the status is not successful
|
||||||
"""
|
"""
|
||||||
payment = await get_standalone_payment(checking_id, incoming=True)
|
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||||
if not payment:
|
if not payment:
|
||||||
logger.warning(f"No payment found for '{checking_id}'.")
|
logger.warning(f"No incoming payment found for '{checking_id}'.")
|
||||||
return None
|
return None
|
||||||
if not payment.is_in:
|
|
||||||
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
|
status = await check_payment_status(payment)
|
||||||
|
|
||||||
|
if not status.success:
|
||||||
|
logger.error(
|
||||||
|
"Unexpected status response from paid_invoices_stream. Skipping update."
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
status = await check_payment_status(
|
|
||||||
payment, skip_internal_payment_notifications=True
|
|
||||||
)
|
|
||||||
payment.fee = status.fee_msat or payment.fee
|
payment.fee = status.fee_msat or payment.fee
|
||||||
# only overwrite preimage if status.preimage provides it
|
# only overwrite preimage if status.preimage provides it
|
||||||
payment.preimage = status.preimage or payment.preimage
|
payment.preimage = status.preimage or payment.preimage
|
||||||
|
|
||||||
payment.status = PaymentState.SUCCESS
|
payment.status = PaymentState.SUCCESS
|
||||||
payment = await update_payment(payment)
|
payment = await update_payment(payment)
|
||||||
if payment.fiat_provider:
|
|
||||||
await handle_fiat_payment_confirmation(payment)
|
|
||||||
return payment
|
return payment
|
||||||
|
|||||||
+6
-3
@@ -6,7 +6,10 @@ from collections.abc import Callable, Coroutine
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.core.services.payments import update_invoice_callback
|
from lnbits.core.services.payments import (
|
||||||
|
get_standalone_payment,
|
||||||
|
update_invoice_from_paid_invoices_stream,
|
||||||
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
|
|
||||||
@@ -112,7 +115,7 @@ async def internal_invoice_listener() -> None:
|
|||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
checking_id = await internal_invoice_queue.get()
|
checking_id = await internal_invoice_queue.get()
|
||||||
logger.info(f"got an internal payment notification {checking_id}")
|
logger.info(f"got an internal payment notification {checking_id}")
|
||||||
payment = await update_invoice_callback(checking_id)
|
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||||
if payment:
|
if payment:
|
||||||
logger.success(f"internal invoice {checking_id} settled")
|
logger.success(f"internal invoice {checking_id} settled")
|
||||||
await invoice_callback_dispatcher(payment)
|
await invoice_callback_dispatcher(payment)
|
||||||
@@ -128,7 +131,7 @@ async def invoice_listener() -> None:
|
|||||||
funding_source = get_funding_source()
|
funding_source = get_funding_source()
|
||||||
async for checking_id in funding_source.paid_invoices_stream():
|
async for checking_id in funding_source.paid_invoices_stream():
|
||||||
logger.info(f"got a payment notification {checking_id}")
|
logger.info(f"got a payment notification {checking_id}")
|
||||||
payment = await update_invoice_callback(checking_id)
|
payment = await update_invoice_from_paid_invoices_stream(checking_id)
|
||||||
if payment:
|
if payment:
|
||||||
logger.success(f"fundingsource invoice {checking_id} settled")
|
logger.success(f"fundingsource invoice {checking_id} settled")
|
||||||
await invoice_callback_dispatcher(payment)
|
await invoice_callback_dispatcher(payment)
|
||||||
|
|||||||
+39
-38
@@ -3,6 +3,7 @@ import base64
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -123,8 +124,8 @@ class LndRestWallet(Wallet):
|
|||||||
hashlib.sha256(unhashed_description).digest()
|
hashlib.sha256(unhashed_description).digest()
|
||||||
).decode("ascii")
|
).decode("ascii")
|
||||||
|
|
||||||
preimage, _payment_hash = random_secret_and_hash()
|
preimage, payment_hash = random_secret_and_hash()
|
||||||
_data["r_hash"] = base64.b64encode(bytes.fromhex(_payment_hash)).decode()
|
_data["r_hash"] = base64.b64encode(bytes.fromhex(payment_hash)).decode()
|
||||||
_data["r_preimage"] = base64.b64encode(bytes.fromhex(preimage)).decode()
|
_data["r_preimage"] = base64.b64encode(bytes.fromhex(preimage)).decode()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -132,34 +133,7 @@ class LndRestWallet(Wallet):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if len(data) == 0:
|
return self._parse_create_invoice_response(r, data, preimage)
|
||||||
return InvoiceResponse(ok=False, error_message="no data")
|
|
||||||
|
|
||||||
if "error" in data:
|
|
||||||
return InvoiceResponse(
|
|
||||||
ok=False, error_message=f"""Server error: '{data["error"]}'"""
|
|
||||||
)
|
|
||||||
|
|
||||||
if r.is_error:
|
|
||||||
return InvoiceResponse(
|
|
||||||
ok=False, error_message=f"Server error: '{r.text}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "payment_request" not in data or "r_hash" not in data:
|
|
||||||
return InvoiceResponse(
|
|
||||||
ok=False, error_message="Server error: 'missing required fields'"
|
|
||||||
)
|
|
||||||
|
|
||||||
payment_request = data["payment_request"]
|
|
||||||
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
|
||||||
checking_id = payment_hash
|
|
||||||
return InvoiceResponse(
|
|
||||||
ok=True,
|
|
||||||
checking_id=checking_id,
|
|
||||||
payment_request=payment_request,
|
|
||||||
preimage=preimage,
|
|
||||||
)
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
ok=False, error_message="Server error: 'invalid json response'"
|
ok=False, error_message="Server error: 'invalid json response'"
|
||||||
@@ -242,12 +216,13 @@ class LndRestWallet(Wallet):
|
|||||||
logger.warning(f"Error getting invoice status: {e}")
|
logger.warning(f"Error getting invoice status: {e}")
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
if r.is_error or data.get("settled") is None:
|
if r.is_error or data.get("state") is None:
|
||||||
# this must also work when checking_id is not a hex recognizable by lnd
|
# this must also work when checking_id is not a hex recognizable by lnd
|
||||||
# it will return an error and no "settled" attribute on the object
|
# it will return an error and no "state" attribute on the object
|
||||||
|
logger.warning(f"Error checking invoice from LND REST API: {r.text}")
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
if data.get("settled") is True:
|
if data.get("state") == "SETTLED":
|
||||||
return PaymentSuccessStatus()
|
return PaymentSuccessStatus()
|
||||||
|
|
||||||
if data.get("state") == "CANCELED":
|
if data.get("state") == "CANCELED":
|
||||||
@@ -264,6 +239,7 @@ class LndRestWallet(Wallet):
|
|||||||
"ascii"
|
"ascii"
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
logger.warning("Invalid checking_id format, must be hex: {checking_id}")
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
url = f"/v2/router/track/{checking_id}"
|
url = f"/v2/router/track/{checking_id}"
|
||||||
@@ -311,13 +287,12 @@ class LndRestWallet(Wallet):
|
|||||||
async for line in r.aiter_lines():
|
async for line in r.aiter_lines():
|
||||||
try:
|
try:
|
||||||
inv = json.loads(line)["result"]
|
inv = json.loads(line)["result"]
|
||||||
if not inv["settled"]:
|
if not inv.get("state") == "SETTLED":
|
||||||
continue
|
continue
|
||||||
|
payment_hash = base64.b64decode(inv.get("r_hash")).hex()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(exc)
|
logger.debug(exc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
|
||||||
yield payment_hash
|
yield payment_hash
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -363,8 +338,6 @@ class LndRestWallet(Wallet):
|
|||||||
return InvoiceResponse(ok=False, error_message=str(exc))
|
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
payment_request = data["payment_request"]
|
payment_request = data["payment_request"]
|
||||||
payment_hash = base64.b64encode(bytes.fromhex(payment_hash)).decode("ascii")
|
|
||||||
|
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
ok=True, checking_id=payment_hash, payment_request=payment_request
|
ok=True, checking_id=payment_hash, payment_request=payment_request
|
||||||
)
|
)
|
||||||
@@ -399,3 +372,31 @@ class LndRestWallet(Wallet):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return InvoiceResponse(ok=False, error_message=str(exc))
|
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
|
def _parse_create_invoice_response(
|
||||||
|
self, r: Any, data: dict, preimage: str
|
||||||
|
) -> InvoiceResponse:
|
||||||
|
if not data:
|
||||||
|
return InvoiceResponse(ok=False, error_message="no data")
|
||||||
|
if "error" in data:
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=False, error_message=f"Server error: '{data['error']}'"
|
||||||
|
)
|
||||||
|
if r.is_error:
|
||||||
|
return InvoiceResponse(ok=False, error_message=f"Server error: '{r.text}'")
|
||||||
|
if "payment_request" not in data or "r_hash" not in data:
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=False, error_message="Server error: 'missing required fields'"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
||||||
|
except Exception:
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=False, error_message=f"Unable to b64decode to {data['r_hash']}."
|
||||||
|
)
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=payment_hash,
|
||||||
|
payment_request=data["payment_request"],
|
||||||
|
preimage=preimage,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1744,11 +1744,10 @@ async def test_check_fiat_status_handles_internal_states(mocker: MockerFixture):
|
|||||||
amount=1000,
|
amount=1000,
|
||||||
fee=0,
|
fee=0,
|
||||||
bolt11="bolt11",
|
bolt11="bolt11",
|
||||||
status=PaymentState.PENDING,
|
status=PaymentState.SUCCESS,
|
||||||
fiat_provider="stripe",
|
fiat_provider="stripe",
|
||||||
extra={"fiat_checking_id": "stripe_checking_id"},
|
extra={"fiat_checking_id": "stripe_checking_id"},
|
||||||
),
|
)
|
||||||
skip_internal_payment_notifications=True,
|
|
||||||
)
|
)
|
||||||
assert queue_put.await_count == 1
|
assert queue_put.await_count == 1
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ from lnbits.core.services import create_invoice, create_user_account, pay_invoic
|
|||||||
from lnbits.core.services.payments import update_wallet_balance
|
from lnbits.core.services.payments import update_wallet_balance
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError
|
from lnbits.exceptions import InvoiceError, PaymentError
|
||||||
from lnbits.settings import Settings
|
from lnbits.settings import Settings
|
||||||
from lnbits.tasks import create_task, wait_for_paid_invoices
|
from lnbits.tasks import (
|
||||||
|
create_task,
|
||||||
|
internal_invoice_listener,
|
||||||
|
internal_invoice_queue,
|
||||||
|
wait_for_paid_invoices,
|
||||||
|
)
|
||||||
from lnbits.wallets.base import PaymentResponse
|
from lnbits.wallets.base import PaymentResponse
|
||||||
from lnbits.wallets.fake import FakeWallet
|
from lnbits.wallets.fake import FakeWallet
|
||||||
|
|
||||||
@@ -231,7 +236,15 @@ async def test_notification_for_internal_payment(
|
|||||||
):
|
):
|
||||||
test_name = "test_notification_for_internal_payment"
|
test_name = "test_notification_for_internal_payment"
|
||||||
|
|
||||||
|
# Drain stale items left by session-scoped fixtures (e.g. update_wallet_balance)
|
||||||
|
while not internal_invoice_queue.empty():
|
||||||
|
try:
|
||||||
|
internal_invoice_queue.get_nowait()
|
||||||
|
except asyncio.QueueEmpty:
|
||||||
|
break
|
||||||
|
|
||||||
on_paid_mock = mocker.AsyncMock()
|
on_paid_mock = mocker.AsyncMock()
|
||||||
|
create_task(internal_invoice_listener())
|
||||||
create_task(wait_for_paid_invoices(test_name, on_paid_mock)())
|
create_task(wait_for_paid_invoices(test_name, on_paid_mock)())
|
||||||
payment = await create_invoice(
|
payment = await create_invoice(
|
||||||
wallet_id=to_wallet.id,
|
wallet_id=to_wallet.id,
|
||||||
|
|||||||
@@ -2073,7 +2073,7 @@
|
|||||||
{
|
{
|
||||||
"response_type": "json",
|
"response_type": "json",
|
||||||
"response": {
|
"response": {
|
||||||
"settled": true
|
"state": "SETTLED"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -2155,8 +2155,15 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"lndrest": {
|
"lndrest": {
|
||||||
"description": "lndrest.py doesn't handle the 'failed' status for `get_invoice_status`",
|
"get_invoice_status_endpoint": [
|
||||||
"get_invoice_status_endpoint": []
|
{
|
||||||
|
"description": "error status",
|
||||||
|
"response_type": "json",
|
||||||
|
"response": {
|
||||||
|
"state": "CANCELED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"alby": {
|
"alby": {
|
||||||
"description": "alby.py doesn't handle the 'failed' status for `get_invoice_status`",
|
"description": "alby.py doesn't handle the 'failed' status for `get_invoice_status`",
|
||||||
@@ -2243,13 +2250,6 @@
|
|||||||
"response_type": "json",
|
"response_type": "json",
|
||||||
"response": {}
|
"response": {}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"description": "error status",
|
|
||||||
"response_type": "json",
|
|
||||||
"response": {
|
|
||||||
"seetled": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "bad json",
|
"description": "bad json",
|
||||||
"response_type": "data",
|
"response_type": "data",
|
||||||
|
|||||||
Reference in New Issue
Block a user