Skip to content

Commit 82f9a68

Browse files
authored
Merge pull request #164 from ilyarolf/fix/admin-markup-and-stablecoin-fallbacks
Fix/admin markup and stablecoin fallbacks
2 parents 116bab9 + a47a02b commit 82f9a68

34 files changed

Lines changed: 1050 additions & 184 deletions

.env.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ KRYPTO_EXPRESS_API_URL = "https://kryptoexpress.pro/api"
2020
KRYPTO_EXPRESS_API_SECRET = ""
2121
REDIS_PASSWORD = ""
2222
REDIS_HOST = "redis"
23+
TELEGRAM_PROXY_URL = ""
2324
CRYPTO_FORWARDING_MODE = "false"
2425
BTC_FORWARDING_ADDRESS = ""
2526
LTC_FORWARDING_ADDRESS = ""
@@ -38,4 +39,4 @@ TOTAL_BONUS_CAP_PERCENT = "12"
3839
SQLADMIN_RAW_PASSWORD = ""
3940
JWT_EXPIRE_MINUTES = "30"
4041
JWT_ALGORITHM = "HS256"
41-
JWT_SECRET_KEY = ""
42+
JWT_SECRET_KEY = ""

bot.py

Lines changed: 59 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import logging
22
import sys
33
import traceback
4+
from contextlib import asynccontextmanager
45
from pathlib import Path
5-
from aiogram.client.default import DefaultBotProperties
66
from aiogram.fsm.storage.redis import RedisStorage
77
from aiogram.types import BufferedInputFile, URLInputFile
88
from redis.asyncio import Redis
99
from sqladmin import Admin
1010

1111
import config
12-
from aiogram import Bot, Dispatcher
13-
from aiogram.enums import ParseMode
12+
from aiogram import Dispatcher
1413
from fastapi import FastAPI, Request, status, HTTPException
1514

1615
from admin import authentication_backend
@@ -37,48 +36,16 @@
3736
from services.media import MediaService
3837
from services.notification import NotificationService
3938
from services.wallet import WalletService
39+
from utils.telegram import create_bot, create_telegram_session
4040
from utils.utils import validate_i18n
4141

4242
redis = Redis(host=config.REDIS_HOST, password=config.REDIS_PASSWORD)
43-
bot = Bot(config.TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
43+
session = create_telegram_session()
44+
bot = create_bot(config.TOKEN, session)
4445
dp = Dispatcher(storage=RedisStorage(redis))
45-
app = FastAPI()
46-
admin = Admin(app=app, engine=engine, authentication_backend=authentication_backend)
47-
admin.add_model_view(UserAdmin)
48-
admin.add_model_view(BuyAdmin)
49-
admin.add_model_view(ShippingOptionAdmin)
50-
admin.add_model_view(CouponAdmin)
51-
admin.add_model_view(CategoryAdmin)
52-
admin.add_model_view(SubcategoryAdmin)
53-
admin.add_model_view(ItemAdmin)
54-
admin.add_model_view(DepositAdmin)
55-
admin.add_model_view(BuyItemAdmin)
56-
admin.add_model_view(PaymentAdmin)
57-
admin.add_model_view(CartAdmin)
58-
admin.add_model_view(CartItemAdmin)
59-
admin.add_model_view(ReferralBonusAdmin)
60-
admin.add_model_view(ReviewAdmin)
61-
62-
app.include_router(processing_router)
6346

6447

65-
@app.post(config.WEBHOOK_PATH)
66-
async def webhook(request: Request):
67-
secret_token = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
68-
if secret_token != config.WEBHOOK_SECRET_TOKEN:
69-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized")
70-
71-
try:
72-
update_data = await request.json()
73-
await dp.feed_webhook_update(bot, update_data)
74-
return {"status": "ok"}
75-
except Exception as e:
76-
logging.error(f"Error processing webhook: {e}")
77-
return {"status": "error"}, status.HTTP_500_INTERNAL_SERVER_ERROR
78-
79-
80-
@app.on_event("startup")
81-
async def on_startup():
48+
async def _startup() -> None:
8249
await create_db_and_tables()
8350
await bot.set_webhook(
8451
url=config.WEBHOOK_URL,
@@ -110,30 +77,74 @@ async def on_startup():
11077
await ButtonMediaRepository.init_buttons_media()
11178
if config.CRYPTO_FORWARDING_MODE:
11279
for cryptocurrency in Cryptocurrency:
113-
is_addr_valid = WalletService.validate_withdrawal_address(
114-
cryptocurrency.get_forwarding_address(),
115-
cryptocurrency
116-
)
80+
forwarding_address = cryptocurrency.get_forwarding_address()
81+
is_addr_valid = WalletService.validate_withdrawal_address(forwarding_address, cryptocurrency)
11782
if is_addr_valid is False:
118-
logging.debug(
119-
f"Your withdrawal address for {cryptocurrency.name} cryptocurrency is not valid!"
83+
logging.error(
84+
"Your withdrawal address for %s cryptocurrency is not configured correctly: %s",
85+
cryptocurrency.name,
86+
forwarding_address
12087
)
121-
sys.exit()
88+
sys.exit(1)
12289
for admin in config.ADMIN_ID_LIST:
12390
try:
12491
await bot.send_message(admin, 'Bot is working')
12592
except Exception as e:
12693
logging.warning(e)
12794

12895

129-
@app.on_event("shutdown")
130-
async def on_shutdown():
96+
async def _shutdown() -> None:
13197
logging.warning('Shutting down..')
13298
await bot.delete_webhook()
13399
await dp.storage.close()
100+
await bot.session.close()
134101
logging.warning('Bye!')
135102

136103

104+
@asynccontextmanager
105+
async def lifespan(app: FastAPI):
106+
await _startup()
107+
try:
108+
yield
109+
finally:
110+
await _shutdown()
111+
112+
113+
app = FastAPI(lifespan=lifespan)
114+
admin = Admin(app=app, engine=engine, authentication_backend=authentication_backend)
115+
admin.add_model_view(UserAdmin)
116+
admin.add_model_view(BuyAdmin)
117+
admin.add_model_view(ShippingOptionAdmin)
118+
admin.add_model_view(CouponAdmin)
119+
admin.add_model_view(CategoryAdmin)
120+
admin.add_model_view(SubcategoryAdmin)
121+
admin.add_model_view(ItemAdmin)
122+
admin.add_model_view(DepositAdmin)
123+
admin.add_model_view(BuyItemAdmin)
124+
admin.add_model_view(PaymentAdmin)
125+
admin.add_model_view(CartAdmin)
126+
admin.add_model_view(CartItemAdmin)
127+
admin.add_model_view(ReferralBonusAdmin)
128+
admin.add_model_view(ReviewAdmin)
129+
130+
app.include_router(processing_router)
131+
132+
133+
@app.post(config.WEBHOOK_PATH)
134+
async def webhook(request: Request):
135+
secret_token = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
136+
if secret_token != config.WEBHOOK_SECRET_TOKEN:
137+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized")
138+
139+
try:
140+
update_data = await request.json()
141+
await dp.feed_webhook_update(bot, update_data)
142+
return {"status": "ok"}
143+
except Exception as e:
144+
logging.error(f"Error processing webhook: {e}")
145+
return {"status": "error"}, status.HTTP_500_INTERNAL_SERVER_ERROR
146+
147+
137148
@app.exception_handler(Exception)
138149
async def exception_handler(request: Request, exc: Exception):
139150
traceback_str = traceback.format_exc()

config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from enums.runtime_environment import RuntimeEnvironment
77
from utils.utils import get_sslipio_external_url, start_ngrok, hash_password
88

9-
load_dotenv(".env")
9+
load_dotenv(".env.bot.dev")
1010
RUNTIME_ENVIRONMENT = RuntimeEnvironment(os.environ.get("RUNTIME_ENVIRONMENT"))
1111
if RUNTIME_ENVIRONMENT == RuntimeEnvironment.DEV:
1212
WEBHOOK_HOST = start_ngrok()
@@ -35,6 +35,7 @@
3535
WEBHOOK_SECRET_TOKEN = os.environ.get("WEBHOOK_SECRET_TOKEN")
3636
REDIS_HOST = os.environ.get("REDIS_HOST", "redis")
3737
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD")
38+
TELEGRAM_PROXY_URL = os.environ.get("TELEGRAM_PROXY_URL")
3839
# VARIABLES FOR CRYPTO FORWARDING
3940
CRYPTO_FORWARDING_MODE = os.environ.get("CRYPTO_FORWARDING_MODE", False) == 'true'
4041
BTC_FORWARDING_ADDRESS = os.environ.get("BTC_FORWARDING_ADDRESS")

handlers/admin/announcement.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ async def send_generated_msg(**kwargs):
5151
language: Language = kwargs.get("language")
5252
kb_builder = AnnouncementsConstants.get_confirmation_builder(callback_data.announcement_type,
5353
language)
54-
msg = await ItemService.create_announcement_message(callback_data.announcement_type, session, language)
55-
await callback.message.answer(text=msg, reply_markup=kb_builder.as_markup())
54+
messages = await ItemService.create_announcement_message(callback_data.announcement_type, session, language)
55+
for index, message_text in enumerate(messages):
56+
reply_markup = kb_builder.as_markup() if index == 0 else None
57+
await callback.message.answer(text=message_text, reply_markup=reply_markup)
5658

5759

5860
async def send_confirmation(**kwargs):

handlers/user/my_profile.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,13 @@ async def receive_top_up_amount(message: Message,
156156
session,
157157
language)
158158
state_data = await state.get_data()
159-
await message.bot.edit_message_media(chat_id=state_data.get("chat_id"),
160-
message_id=state_data.get("msg_id"),
161-
media=media,
162-
reply_markup=kb_builder.as_markup())
159+
if state_data.get("chat_id") and state_data.get("msg_id"):
160+
await message.bot.edit_message_media(chat_id=state_data.get("chat_id"),
161+
message_id=state_data.get("msg_id"),
162+
media=media,
163+
reply_markup=kb_builder.as_markup())
164+
else:
165+
await NotificationService.answer_media(message, media, kb_builder.as_markup())
163166

164167

165168
@my_profile_router.callback_query(MyProfileCallback.filter(), IsUserExistFilter())

i18n/de.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@
304304
"referral_code_section": "\n\n<b>🔗 Ihr Empfehlungslink</b>\nt.me/{bot_username}?start={referral_code}\nTeilen Sie Ihren Link und wachsen Sie gemeinsam 🚀",
305305
"referral_button": "✨ Empfehlungssystem",
306306
"top_up_balance_deposit_msg": "💵 <b>Überweisen Sie den gewünschten Betrag in {crypto_name} an die Adresse, um das Bot-Guthaben aufzuladen.</b>\n\nZahlungsstatus: {status}\nZahlung gültig bis {payment_lifetime}.\n\n<b>Wichtig</b>\n<i>Für jede Einzahlung wird eine eindeutige {crypto_name}-Adresse vergeben\nDie Aufladung erfolgt innerhalb von 5 Minuten nach der Überweisung.\n\nNach erfolgreicher Guthabenaktualisierung erhalten Sie eine Benachrichtigung vom Bot.</i>\n\n<b>Ihre {crypto_name}-Adresse\n</b><code>{addr}</code>",
307-
"top_up_balance_payment_msg": "💵 <b>Überweisen Sie <code>{crypto_amount}</code> {crypto_name} an die Adresse, um das Bot-Guthaben für {fiat_amount} {currency_text} aufzuladen</b>\n\nZahlungsstatus: {status}\nZahlung gültig bis {payment_lifetime}.\n\n<b>Wichtig</b>\n<i>Für jede Einzahlung wird eine eindeutige {crypto_name}-Adresse vergeben\nDie Aufladung erfolgt innerhalb von 5 Minuten nach der Überweisung.\n\nNach erfolgreicher Guthabenaktualisierung erhalten Sie eine Benachrichtigung vom Bot.</i>\n\n<b>Ihre {crypto_name}-Adresse\n</b><code>{addr}</code>",
308-
"top_up_balance_request_fiat": "💵 <b>Bitte senden Sie den Betrag, den Sie in <u>{currency_text}</u> aufladen möchten\n⚠️ Achtung! Mindesteinzahlungsbetrag 5 {currency_text}</b>"
307+
"top_up_balance_payment_msg": "💵 <b>Senden Sie genau <code>{crypto_amount}</code> {crypto_name}</b>\n\n⚠️ <b>SENDEN SIE NICHT WENIGER.</b>\n⚠️ <b>SENDEN SIE NICHT MEHR.</b>\n⚠️ <b>NUR DER EXAKTE BETRAG WIRD AKZEPTIERT.</b>\n\n❌ Unterzahlung ist verboten.\n❌ Überzahlung ist verboten.\n❗ Wenn Sie einen anderen Betrag als <code>{crypto_amount}</code> {crypto_name} senden, gehen Ihre Gelder <b>dauerhaft verloren</b> und <b>können nicht wiederhergestellt</b> werden.\n\nZahlungsstatus: {status}\n⏳ Gültig bis: {payment_lifetime}\n\n<b>Einzahlungsadresse:</b>\n<code>{addr}</code>\n\nℹ️ Dies ist eine eindeutige {crypto_name}-Adresse, die nur für diese Zahlung erstellt wurde.\n✅ Das Guthaben wird innerhalb von 5 Minuten nach Eingang der Transaktion gutgeschrieben.",
308+
"top_up_balance_request_fiat": "💵 <b>Bitte senden Sie den Betrag, den Sie in <u>{currency_text}</u> aufladen möchten\n⚠️ Achtung! Mindesteinzahlungsbetrag 5 {currency_text}</b>",
309+
"top_up_balance_invalid_fiat_amount": "⚠️ <b>Ungültiger Betrag.</b>\nBitte senden Sie einen positiven Betrag von <code>5</code> bis unter <code>1000000</code> {currency_text} mit höchstens 2 Dezimalstellen."
309310
}
310311
}

i18n/en.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@
303303
"referral_code_section": "\n\n<b>\uD83D\uDD17 Your Referral Link</b>\nt.me/{bot_username}?start={referral_code}\nShare your link and grow together \uD83D\uDE80",
304304
"referral_button": "✨ Referral System",
305305
"top_up_balance_deposit_msg": "💵 <b>Deposit to the address the amount you want in {crypto_name} to top up the balance of bot.</b>\n\nPayment status: {status}\nPayment lifetime until {payment_lifetime}.\n\n<b>Important</b>\n<i>A unique {crypto_name} addresses is given for each deposit\nThe top up takes place within 5 minutes after the transfer.\n\nAfter a successful balance refresh, you will receive a notification from bot.</i>\n\n<b>Your {crypto_name} address\n</b><code>{addr}</code>",
306-
"top_up_balance_payment_msg": "\uD83D\uDCB5 <b>Deposit to the address the <code>{crypto_amount}</code> {crypto_name} to top up the balance of bot for {fiat_amount} {currency_text}</b>\n\nPayment status: {status}\nPayment lifetime until {payment_lifetime}.\n\n<b>Important</b>\n<i>A unique {crypto_name} addresses is given for each deposit\nThe top up takes place within 5 minutes after the transfer.\n\nAfter a successful balance refresh, you will receive a notification from bot.</i>\n\n<b>Your {crypto_name} address\n</b><code>{addr}</code>",
307-
"top_up_balance_request_fiat": "\uD83D\uDCB5 <b>Please send the amount you wish to top up balance in <u>{currency_text}</u>\n\uFE0F Attention! Minimal deposit amount 5 {currency_text}</b>"
306+
"top_up_balance_payment_msg": "💵 <b>Send exactly <code>{crypto_amount}</code> {crypto_name}</b>\n\n⚠️ <b>DO NOT SEND LESS.</b>\n⚠️ <b>DO NOT SEND MORE.</b>\n⚠️ <b>ONLY THE EXACT AMOUNT IS ACCEPTED.</b>\n\n❌ Underpayment is forbidden.\n❌ Overpayment is forbidden.\n❗ If you send any amount other than <code>{crypto_amount}</code> {crypto_name}, your funds will be <b>lost permanently</b> and <b>cannot be recovered</b>.\n\nPayment status: {status}\n⏳ Valid until: {payment_lifetime}\n\n<b>Deposit address:</b>\n<code>{addr}</code>\n\nℹ️ This is a unique {crypto_name} address created only for this payment.\n✅ The balance will be credited within 5 minutes after the transaction is received.",
307+
"top_up_balance_request_fiat": "\uD83D\uDCB5 <b>Please send the amount you wish to top up balance in <u>{currency_text}</u>\n\uFE0F Attention! Minimal deposit amount 5 {currency_text}</b>",
308+
"top_up_balance_invalid_fiat_amount": "⚠️ <b>Invalid amount.</b>\nPlease send a positive amount from <code>5</code> to less than <code>1000000</code> {currency_text} using up to 2 decimal places."
308309
}
309310
}

i18n/es.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@
304304
"referral_code_section": "\n\n<b>🔗 Tu Enlace de Referido</b>\nt.me/{bot_username}?start={referral_code}\nComparte tu enlace y crece junto con otros 🚀",
305305
"referral_button": "✨ Sistema de Referidos",
306306
"top_up_balance_deposit_msg": "💵 <b>Deposite en la dirección la cantidad que desee en {crypto_name} para recargar el saldo del bot.</b>\n\nEstado del pago: {status}\nVigencia del pago hasta {payment_lifetime}.\n\n<b>Importante</b>\n<i>Se asigna una dirección única de {crypto_name} para cada depósito\nLa recarga se realiza en 5 minutos después de la transferencia.\n\nTras una actualización exitosa del saldo, recibirá una notificación del bot.</i>\n\n<b>Su dirección de {crypto_name}\n</b><code>{addr}</code>",
307-
"top_up_balance_payment_msg": "💵 <b>Deposite en la dirección <code>{crypto_amount}</code> {crypto_name} para recargar el saldo del bot por {fiat_amount} {currency_text}</b>\n\nEstado del pago: {status}\nVigencia del pago hasta {payment_lifetime}.\n\n<b>Importante</b>\n<i>Se asigna una dirección única de {crypto_name} para cada depósito\nLa recarga se realiza en 5 minutos después de la transferencia.\n\nTras una actualización exitosa del saldo, recibirá una notificación del bot.</i>\n\n<b>Su dirección de {crypto_name}\n</b><code>{addr}</code>",
308-
"top_up_balance_request_fiat": "💵 <b>Por favor, envíe la cantidad que desea recargar en <u>{currency_text}</u>\n⚠️ ¡Atención! Depósito mínimo 5 {currency_text}</b>"
307+
"top_up_balance_payment_msg": "💵 <b>Envíe exactamente <code>{crypto_amount}</code> {crypto_name}</b>\n\n⚠️ <b>NO ENVÍE MENOS.</b>\n⚠️ <b>NO ENVÍE MÁS.</b>\n⚠️ <b>SOLO SE ACEPTA LA CANTIDAD EXACTA.</b>\n\n❌ El pago insuficiente está prohibido.\n❌ El pago en exceso está prohibido.\n❗ Si envía cualquier cantidad distinta de <code>{crypto_amount}</code> {crypto_name}, sus fondos se <b>perderán permanentemente</b> y <b>no podrán recuperarse</b>.\n\nEstado del pago: {status}\n⏳ Válido hasta: {payment_lifetime}\n\n<b>Dirección de depósito:</b>\n<code>{addr}</code>\n\nℹ️ Esta es una dirección única de {crypto_name} creada solo para este pago.\n✅ El saldo se acreditará dentro de los 5 minutos posteriores a la recepción de la transacción.",
308+
"top_up_balance_request_fiat": "💵 <b>Por favor, envíe la cantidad que desea recargar en <u>{currency_text}</u>\n⚠️ ¡Atención! Depósito mínimo 5 {currency_text}</b>",
309+
"top_up_balance_invalid_fiat_amount": "⚠️ <b>Cantidad no válida.</b>\nEnvíe una cantidad positiva desde <code>5</code> hasta menos de <code>1000000</code> {currency_text} usando hasta 2 decimales."
309310
}
310311
}

i18n/fr.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@
304304
"referral_code_section": "\n\n<b>🔗 Votre Lien de Parrainage</b>\nt.me/{bot_username}?start={referral_code}\nPartagez votre lien et grandissez ensemble 🚀",
305305
"referral_button": "✨ Système de Parrainage",
306306
"top_up_balance_deposit_msg": "💵 <b>Déposez sur l'adresse le montant souhaité en {crypto_name} pour recharger le solde du bot.</b>\n\nStatut du paiement: {status}\nValidité du paiement jusqu'au {payment_lifetime}.\n\n<b>Important</b>\n<i>Une adresse {crypto_name} unique est attribuée pour chaque dépôt\nLe rechargement s'effectue dans les 5 minutes suivant le transfert.\n\nAprès une actualisation réussie du solde, vous recevrez une notification du bot.</i>\n\n<b>Votre adresse {crypto_name}\n</b><code>{addr}</code>",
307-
"top_up_balance_payment_msg": "💵 <b>Déposez sur l'adresse <code>{crypto_amount}</code> {crypto_name} pour recharger le solde du bot pour {fiat_amount} {currency_text}</b>\n\nStatut du paiement: {status}\nValidité du paiement jusqu'au {payment_lifetime}.\n\n<b>Important</b>\n<i>Une adresse {crypto_name} unique est attribuée pour chaque dépôt\nLe rechargement s'effectue dans les 5 minutes suivant le transfert.\n\nAprès une actualisation réussie du solde, vous recevrez une notification du bot.</i>\n\n<b>Votre adresse {crypto_name}\n</b><code>{addr}</code>",
308-
"top_up_balance_request_fiat": "💵 <b>Veuillez envoyer le montant que vous souhaitez recharger en <u>{currency_text}</u>\n⚠️ Attention ! Dépôt minimum 5 {currency_text}</b>"
307+
"top_up_balance_payment_msg": "💵 <b>Envoyez exactement <code>{crypto_amount}</code> {crypto_name}</b>\n\n⚠️ <b>N'ENVOYEZ PAS MOINS.</b>\n⚠️ <b>N'ENVOYEZ PAS PLUS.</b>\n⚠️ <b>SEUL LE MONTANT EXACT EST ACCEPTÉ.</b>\n\n❌ Le sous-paiement est interdit.\n❌ Le surpaiement est interdit.\n❗ Si vous envoyez un montant différent de <code>{crypto_amount}</code> {crypto_name}, vos fonds seront <b>perdus définitivement</b> et <b>ne pourront pas être récupérés</b>.\n\nStatut du paiement: {status}\n⏳ Valide jusqu'au : {payment_lifetime}\n\n<b>Adresse de dépôt :</b>\n<code>{addr}</code>\n\nℹ️ Il s'agit d'une adresse {crypto_name} unique créée uniquement pour ce paiement.\n✅ Le solde sera crédité dans les 5 minutes suivant la réception de la transaction.",
308+
"top_up_balance_request_fiat": "💵 <b>Veuillez envoyer le montant que vous souhaitez recharger en <u>{currency_text}</u>\n⚠️ Attention ! Dépôt minimum 5 {currency_text}</b>",
309+
"top_up_balance_invalid_fiat_amount": "⚠️ <b>Montant invalide.</b>\nVeuillez envoyer un montant positif de <code>5</code> à moins de <code>1000000</code> {currency_text} avec au maximum 2 décimales."
309310
}
310311
}

0 commit comments

Comments
 (0)