From 062de44400866ca3708673cfbabf334ee0aedf51 Mon Sep 17 00:00:00 2001 From: vas3k Date: Sat, 15 Jun 2024 17:39:16 +0200 Subject: [PATCH] fix: moar helpdesk refactoring --- club/settings.py | 7 - helpdeskbot/config.py | 11 +- helpdeskbot/handlers/{reply.py => answers.py} | 14 +- helpdeskbot/handlers/question.py | 126 ++++++++---------- helpdeskbot/help_desk_common.py | 59 ++++---- helpdeskbot/main.py | 23 ++-- ...html => helpdeskbot_channel_question.html} | 4 +- .../messages/helpdeskbot_review_question.html | 2 +- .../messages/helpdeskbot_room_question.html | 7 + 9 files changed, 124 insertions(+), 129 deletions(-) rename helpdeskbot/handlers/{reply.py => answers.py} (87%) rename helpdeskbot/templates/messages/{helpdeskbot_question.html => helpdeskbot_channel_question.html} (54%) create mode 100644 helpdeskbot/templates/messages/helpdeskbot_room_question.html diff --git a/club/settings.py b/club/settings.py index 64bebfb3d..75bd67382 100644 --- a/club/settings.py +++ b/club/settings.py @@ -274,13 +274,6 @@ TELEGRAM_BOT_WEBHOOK_HOST = "0.0.0.0" TELEGRAM_BOT_WEBHOOK_PORT = 8816 -TELEGRAM_HELP_DESK_BOT_TOKEN = os.getenv("TELEGRAM_HELP_DESK_BOT_TOKEN") -TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID = os.getenv("TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID") -TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID = os.getenv("TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID") -TELEGRAM_HELP_DESK_BOT_WEBHOOK_URL = "https://vas3k.club/telegram/helpdeskbot/webhook/" -TELEGRAM_HELP_DESK_BOT_WEBHOOK_HOST = "0.0.0.0" -TELEGRAM_HELP_DESK_BOT_WEBHOOK_PORT = 8817 - STRIPE_API_KEY = os.getenv("STRIPE_API_KEY") or "" STRIPE_PUBLIC_KEY = os.getenv("STRIPE_PUBLIC_KEY") or "" STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET") or "" diff --git a/helpdeskbot/config.py b/helpdeskbot/config.py index 0112aeefe..9c5abcfec 100644 --- a/helpdeskbot/config.py +++ b/helpdeskbot/config.py @@ -1,3 +1,12 @@ -DAILY_QUESTION_LIMIT = 3 +import os + +DAILY_QUESTION_LIMIT = 2 QUESTION_TITLE_MAX_LEN = 150 QUESTION_BODY_MAX_LEN = 2500 + +TELEGRAM_HELP_DESK_BOT_TOKEN = os.getenv("TELEGRAM_HELP_DESK_BOT_TOKEN") +TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID = os.getenv("TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID") +TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID = os.getenv("TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID") +TELEGRAM_HELP_DESK_BOT_WEBHOOK_URL = "https://vas3k.club/telegram/helpdeskbot/webhook/" +TELEGRAM_HELP_DESK_BOT_WEBHOOK_HOST = "0.0.0.0" +TELEGRAM_HELP_DESK_BOT_WEBHOOK_PORT = 8817 diff --git a/helpdeskbot/handlers/reply.py b/helpdeskbot/handlers/answers.py similarity index 87% rename from helpdeskbot/handlers/reply.py rename to helpdeskbot/handlers/answers.py index 332030ddb..a2cf31d7f 100644 --- a/helpdeskbot/handlers/reply.py +++ b/helpdeskbot/handlers/answers.py @@ -3,10 +3,10 @@ from telegram import Update from telegram.ext import CallbackContext -from helpdeskbot.help_desk_common import channel_msg_link, send_msg +from helpdeskbot import config +from helpdeskbot.help_desk_common import get_channel_message_link, send_message from helpdeskbot.room import get_rooms from helpdeskbot.models import Question -from club import settings log = logging.getLogger(__name__) @@ -62,8 +62,8 @@ def handle_reply_from_room_chat(update: Update) -> None: message_text = f"💬 {reply_chat_link} от {from_user_link} из чата {room_invite_link}:\n\n" \ f"{update.message.text}" - chat_id = settings.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID - send_msg(chat_id=chat_id, text=message_text, reply_to_message_id=question.discussion_msg_id) + chat_id = config.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID + send_message(chat_id=chat_id, text=message_text, reply_to_message_id=question.discussion_msg_id) def notify_user_about_reply(update: Update, question: Question, from_room_chat: bool) -> None: @@ -86,7 +86,7 @@ def notify_user_about_reply(update: Update, question: Question, from_room_chat: from_user_link = f"{from_user.first_name}" reply_text = message.text - question_link = f"❓ Ссылка на твой вопрос" + question_link = f"❓ Ссылка на твой вопрос" if from_room_chat: room = question.room @@ -101,7 +101,7 @@ def notify_user_about_reply(update: Update, question: Question, from_room_chat: f"{reply_text}\n\n" \ f"{question_link}" - send_msg(chat_id=int(user_id), text=message_text) + send_message(chat_id=int(user_id), text=message_text) def on_reply_message(update: Update, context: CallbackContext) -> None: @@ -113,7 +113,7 @@ def on_reply_message(update: Update, context: CallbackContext) -> None: reply_to = update.message.reply_to_message if reply_to.forward_from_chat: - if reply_to.forward_from_chat.id == int(settings.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID): + if reply_to.forward_from_chat.id == int(config.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID): handle_reply_from_channel(update) return None else: diff --git a/helpdeskbot/handlers/question.py b/helpdeskbot/handlers/question.py index 0326c4b05..9ce092f05 100644 --- a/helpdeskbot/handlers/question.py +++ b/helpdeskbot/handlers/question.py @@ -9,11 +9,10 @@ from telegram.ext import CallbackContext, ConversationHandler, CommandHandler, MessageHandler, Filters from helpdeskbot import config -from helpdeskbot.help_desk_common import channel_msg_link, send_msg, edit_msg, chat_msg_link, msg_reply +from helpdeskbot.help_desk_common import get_channel_message_link, send_message, edit_message, get_chat_message_link, send_reply from helpdeskbot.models import Question, HelpDeskUser from helpdeskbot.room import get_rooms from bot.handlers.common import get_club_user -from club import settings from notifications.telegram.common import render_html_message log = logging.getLogger(__name__) @@ -61,7 +60,6 @@ class QuestionKeyboard(Enum): def get_rooms_markup() -> list: room_names = list(rooms.keys()) room_names.append(DO_NOT_SEND_ROOM) - num_columns = 2 return [room_names[i:i + num_columns] for i in range(0, len(room_names), num_columns)] @@ -83,6 +81,8 @@ def from_user_data(cls, user_data: Dict[str, str]) -> "QuestionDto": title=user_data.get(QuestionKeyboard.TITLE.value, ""), body=user_data.get(QuestionKeyboard.BODY.value, ""), room=user_data.get(QuestionKeyboard.ROOM.value, "") + if user_data.get(QuestionKeyboard.ROOM.value, "") != DO_NOT_SEND_ROOM + else None ) def to_json(self): @@ -100,21 +100,20 @@ def start(update: Update, context: CallbackContext) -> State: help_desk_user_ban = HelpDeskUser.objects.filter(user=user).first() if help_desk_user_ban and help_desk_user_ban.is_banned: - msg_reply(update, "🙈 Вас забанили от пользования Вастрик Справочной") + send_reply(update, "🙈 Вас забанили от пользования Вастрик Справочной") return ConversationHandler.END - yesterday = datetime.utcnow() - timedelta(hours=24) - question_number = Question.objects.filter(user=user) \ - .filter(created_at__gte=yesterday) \ + question_count_24h = Question.objects.filter(user=user) \ + .filter(created_at__gte=datetime.utcnow() - timedelta(hours=24)) \ .count() - if question_number >= config.DAILY_QUESTION_LIMIT: - msg_reply(update, "🙅‍♂️ Вы достигли своего дневного лимита вопросов. Приходите завтра!") + if question_count_24h >= config.DAILY_QUESTION_LIMIT: + send_reply(update, "🙅‍♂️ Вы достигли своего дневного лимита вопросов. Приходите завтра!") return ConversationHandler.END context.user_data.clear() - msg_reply( + send_reply( update, render_html_message("helpdeskbot_welcome.html", user=user), reply_markup=question_markup, @@ -130,7 +129,7 @@ def input_response(update: Update, context: CallbackContext) -> State: user_data[field] = text del user_data[CUR_FIELD_KEY] - msg_reply( + send_reply( update, "Принято 👌 Что дальше?", reply_markup=question_markup, @@ -141,9 +140,9 @@ def input_response(update: Update, context: CallbackContext) -> State: def request_title_value(update: Update, context: CallbackContext) -> State: context.user_data[CUR_FIELD_KEY] = QuestionKeyboard.TITLE.value - msg_reply( + send_reply( update, - f"Введите заголовок вопроса. Он должен кратко и понятно описывать ваш запрос. " + f"Введите заголовок вашего вопроса. Постарайтесь быть краткими и понятными. " f"Максимум {config.QUESTION_TITLE_MAX_LEN} символов.", reply_markup=ReplyKeyboardRemove() ) @@ -153,10 +152,10 @@ def request_title_value(update: Update, context: CallbackContext) -> State: def request_body_value(update: Update, context: CallbackContext) -> State: context.user_data[CUR_FIELD_KEY] = QuestionKeyboard.BODY.value - msg_reply( + send_reply( update, f"Введите текст вопроса. Опишите побольше деталей и контекста. " - f"Например, ваш город/страну или уже опробованные варианты решений.", + f"Например, ваш город/страну и уже опробованные варианты решений.", reply_markup=ReplyKeyboardRemove() ) @@ -165,10 +164,10 @@ def request_body_value(update: Update, context: CallbackContext) -> State: def request_room_choose(update: Update, context: CallbackContext) -> State: context.user_data[CUR_FIELD_KEY] = QuestionKeyboard.ROOM.value - msg_reply( + send_reply( update, "Выберите один из чатов, в который бот перепостит ваш вопрос. " - "Это не обязательно, но увеличивает возможность того, что вам кто-то ответит.", + "Это не обязательно, но может увеличить вероятность того, что там найдётся кто-то, кто знает ответ.", reply_markup=room_choose_markup, ) return State.INPUT_RESPONSE @@ -180,24 +179,24 @@ def review_question(update: Update, context: CallbackContext) -> State: title = user_data.get(QuestionKeyboard.TITLE.value, None) body = user_data.get(QuestionKeyboard.BODY.value, None) if not title or not body: - msg_reply(update, "☝️ Заголовок и текст вопроса обязательны для заполнения") + send_reply(update, "☝️ Заголовок и текст вопроса обязательны для заполнения") return edit_question(update, context) if len(title) > config.QUESTION_TITLE_MAX_LEN: - msg_reply( + send_reply( update, f"😬 Заголовок не должен быть длиннее {config.QUESTION_TITLE_MAX_LEN} символов (у вас {len(title)})" ) return edit_question(update, context) if len(body) > config.QUESTION_BODY_MAX_LEN: - msg_reply( + send_reply( update, f"😬 Текст вопроса не может быть длиннее {config.QUESTION_BODY_MAX_LEN} символов (у вас {len(body)})" ) return edit_question(update, context) - msg_reply( + send_reply( update, render_html_message( "helpdeskbot_review_question.html", @@ -215,66 +214,45 @@ def publish_question(update: Update, user_data: Dict[str, str]) -> str: if not user: return ConversationHandler.END - title = user_data[QuestionKeyboard.TITLE.value] - body = user_data[QuestionKeyboard.BODY.value] - json_text = { - "title": title, - "body": body - } - - room_title = user_data.get(QuestionKeyboard.ROOM.value, None) - if room_title and room_title != DO_NOT_SEND_ROOM: - json_text["room"] = room_title + data = QuestionDto.from_user_data(user_data) + room = rooms[data.room] if data.room else None question = Question( user=user, - json_text=json_text + json_text=data.to_json() ) question.save() - room_chat_msg_text = render_html_message( - "helpdeskbot_question.html", - question=QuestionDto.from_user_data(user_data), - user=user, - telegram_user=update.effective_user, - ) - - room = rooms[room_title] if room_title and room_title != DO_NOT_SEND_ROOM else None - room_chat_msg = None - if room and room.chat_id: - room_chat_msg = send_msg(room.chat_id, room_chat_msg_text) - - channel_msg_text = room_chat_msg_text - - if room_chat_msg: - question.room = room - question.room_chat_msg_id = room_chat_msg.message_id - - group_msg_link = chat_msg_link( - chat_id=room.chat_id.replace("-100", ""), - message_id=room_chat_msg.message_id + channel_message = send_message( + chat_id=config.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID, + text=render_html_message( + "helpdeskbot_channel_question.html", + question=data, + room=room, + user=user, + telegram_user=update.effective_user, ) - channel_msg_text = (f"{channel_msg_text}\n\n" + - hyperlink_format(href=group_msg_link, text="🔗 Ссылка на вопрос в чате")) - - channel_msg = send_msg( - chat_id=settings.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID, - text=channel_msg_text ) - question.channel_msg_id = channel_msg.message_id + question.channel_msg_id = channel_message.message_id question.save() - msg_link = channel_msg_link(channel_msg.message_id) - if room_chat_msg: - room_chat_msg_text = (f"{room_chat_msg_text}\n\n" + - hyperlink_format(href=msg_link, text="🔗 Ответы на вопрос в канале")) - edit_msg(chat_id=room.chat_id, message_id=room_chat_msg.message_id, new_text=room_chat_msg_text) - return msg_link + if room and room.chat_id: + send_message( + chat_id=room.chat_id, + text=render_html_message( + "helpdeskbot_room_question.html", + question=data, + room=room, + user=user, + telegram_user=update.effective_user, + channel_message_link=get_channel_message_link(channel_message.message_id), + ) + ) def edit_question(update: Update, context: CallbackContext) -> State: - msg_reply( + send_reply( update, "Окей, что редактируем?", reply_markup=question_markup, @@ -288,9 +266,9 @@ def finish_review(update: Update, context: CallbackContext) -> State: if text == ReviewKeyboard.CREATE.value: link = publish_question(update, user_data) - msg_reply( + send_reply( update, - f"🎉 Вопрос опубликован: ссылка и ответы", + f"🎉 Вопрос опубликован: ссылка и ответы в канале", reply_markup=ReplyKeyboardRemove(), ) return ConversationHandler.END @@ -299,9 +277,9 @@ def finish_review(update: Update, context: CallbackContext) -> State: return edit_question(update, context) elif text == ReviewKeyboard.CANCEL.value: - msg_reply( + send_reply( update, - "🫡 Создание вопроса отменено. Можно начать заново", + "🫡 Создание вопроса отменено. Можно начать заново — /start", reply_markup=ReplyKeyboardRemove(), ) return ConversationHandler.END @@ -311,7 +289,7 @@ def finish_review(update: Update, context: CallbackContext) -> State: def fallback(update: Update, context: CallbackContext) -> State: - msg_reply( + send_reply( update, "Вы не выбрали действие. Пожалуйста, кликните на один из пунктов меню 👇", reply_markup=question_markup, @@ -320,9 +298,9 @@ def fallback(update: Update, context: CallbackContext) -> State: def error_fallback(update: Update, context: CallbackContext) -> int: - msg_reply( + send_reply( update, - "Что-то пошло не так. Придётся начать всё заново :(" + "Что-то пошло не так. Придётся начать всё заново — /start" ) return ConversationHandler.END diff --git a/helpdeskbot/help_desk_common.py b/helpdeskbot/help_desk_common.py index 6dd51baee..6b7863fb5 100644 --- a/helpdeskbot/help_desk_common.py +++ b/helpdeskbot/help_desk_common.py @@ -1,36 +1,43 @@ from telegram import Bot, ParseMode, Update, ReplyMarkup -from club import settings +from helpdeskbot import config -def get_bot(): - return Bot(token=settings.TELEGRAM_HELP_DESK_BOT_TOKEN) +bot = Bot(token=config.TELEGRAM_HELP_DESK_BOT_TOKEN) -def send_msg(chat_id: int, - text: str, - reply_to_message_id: int = None, - parse_mode: ParseMode = ParseMode.HTML - ): - bot = get_bot() - return bot.send_message(chat_id=chat_id, text=text, reply_to_message_id=reply_to_message_id, parse_mode=parse_mode) +def send_message( + chat_id: int, + text: str, + reply_to_message_id: int = None, + parse_mode: ParseMode = ParseMode.HTML, + disable_web_page_preview=True +): + return bot.send_message( + chat_id=chat_id, + text=text, + reply_to_message_id=reply_to_message_id, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + ) -def edit_msg(chat_id: int, - message_id: int, - new_text: str, - parse_mode: ParseMode = ParseMode.HTML - ): - bot = get_bot() +def edit_message( + chat_id: int, + message_id: int, + new_text: str, + parse_mode: ParseMode = ParseMode.HTML +): return bot.edit_message_text(text=new_text, chat_id=chat_id, message_id=message_id, parse_mode=parse_mode) -def msg_reply(update: Update, - text: str, - parse_mode: ParseMode = ParseMode.HTML, - reply_markup: ReplyMarkup = None, - disable_web_page_preview: bool = True, - ): +def send_reply( + update: Update, + text: str, + parse_mode: ParseMode = ParseMode.HTML, + reply_markup: ReplyMarkup = None, + disable_web_page_preview: bool = True, +): update.message.reply_text( text=text, parse_mode=parse_mode, @@ -39,10 +46,10 @@ def msg_reply(update: Update, ) -def channel_msg_link(message_id: str) -> str: - channel_link_id = str(settings.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID).replace("-100", "") - return chat_msg_link(channel_link_id, message_id) +def get_channel_message_link(message_id: str) -> str: + channel_link_id = str(config.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID).replace("-100", "") + return get_chat_message_link(channel_link_id, message_id) -def chat_msg_link(chat_id: str, message_id: str) -> str: +def get_chat_message_link(chat_id: str, message_id: str) -> str: return f"https://t.me/c/{chat_id}/{message_id}" diff --git a/helpdeskbot/main.py b/helpdeskbot/main.py index 7f78387c6..9f76e8f1d 100644 --- a/helpdeskbot/main.py +++ b/helpdeskbot/main.py @@ -2,17 +2,17 @@ import os import sys -import django - # IMPORTANT: this should go before any django-related imports (models, apps, settings) # These lines must be kept together till THE END +import django sys.path.append(os.path.join(os.path.dirname(__file__), "..")) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "club.settings") django.setup() # THE END +from helpdeskbot import config from helpdeskbot.handlers.question import update_discussion_message_id, QuestionHandler -from helpdeskbot.handlers.reply import on_reply_message +from helpdeskbot.handlers.answers import on_reply_message from django.conf import settings from telegram import Update, ParseMode @@ -39,16 +39,16 @@ def on_telegram_admin_bot_message(update: Update, context: CallbackContext) -> N return None message = update.message - if message.chat.id == int(settings.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID) \ + if message.chat.id == int(config.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID) \ and message.forward_from_chat \ - and message.forward_from_chat.id == int(settings.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID) \ + and message.forward_from_chat.id == int(config.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID) \ and message.forward_from_message_id: update_discussion_message_id(update) def main() -> None: # Initialize telegram - updater = Updater(settings.TELEGRAM_HELP_DESK_BOT_TOKEN, use_context=True) + updater = Updater(config.TELEGRAM_HELP_DESK_BOT_TOKEN, use_context=True) # Get the dispatcher to register handlers dispatcher = updater.dispatcher @@ -65,12 +65,13 @@ def main() -> None: # ^ polling is useful for development since you don't need to expose webhook endpoints else: updater.start_webhook( - listen=settings.TELEGRAM_HELP_DESK_BOT_WEBHOOK_HOST, - port=settings.TELEGRAM_HELP_DESK_BOT_WEBHOOK_PORT, - url_path=settings.TELEGRAM_HELP_DESK_BOT_TOKEN + listen=config.TELEGRAM_HELP_DESK_BOT_WEBHOOK_HOST, + port=config.TELEGRAM_HELP_DESK_BOT_WEBHOOK_PORT, + url_path=config.TELEGRAM_HELP_DESK_BOT_TOKEN ) - log.info(f"Set webhook: {settings.TELEGRAM_HELP_DESK_BOT_WEBHOOK_URL + settings.TELEGRAM_HELP_DESK_BOT_TOKEN}") - updater.bot.set_webhook(settings.TELEGRAM_HELP_DESK_BOT_WEBHOOK_URL + settings.TELEGRAM_HELP_DESK_BOT_TOKEN) + log.info(f"Set webhook: {config.TELEGRAM_HELP_DESK_BOT_WEBHOOK_URL + config.TELEGRAM_HELP_DESK_BOT_TOKEN}") + updater.bot.set_webhook( + config.TELEGRAM_HELP_DESK_BOT_WEBHOOK_URL + config.TELEGRAM_HELP_DESK_BOT_TOKEN) # Wait all threads updater.idle() diff --git a/helpdeskbot/templates/messages/helpdeskbot_question.html b/helpdeskbot/templates/messages/helpdeskbot_channel_question.html similarity index 54% rename from helpdeskbot/templates/messages/helpdeskbot_question.html rename to helpdeskbot/templates/messages/helpdeskbot_channel_question.html index 92e48863e..fcfcf6757 100644 --- a/helpdeskbot/templates/messages/helpdeskbot_question.html +++ b/helpdeskbot/templates/messages/helpdeskbot_channel_question.html @@ -1,5 +1,5 @@ -{% if question.room %}{{ question.room }} +{% if question.room %}{{ question.room }} {% endif %}{{ question.title }} от {{ user.full_name }} - {{ question.body }} +{{ question.body }} diff --git a/helpdeskbot/templates/messages/helpdeskbot_review_question.html b/helpdeskbot/templates/messages/helpdeskbot_review_question.html index 422767f3c..a72fe1d6c 100644 --- a/helpdeskbot/templates/messages/helpdeskbot_review_question.html +++ b/helpdeskbot/templates/messages/helpdeskbot_review_question.html @@ -1,3 +1,3 @@ Создание вопроса завершено, давайте проверим, что все верно ⬇️ -{% include "messages/helpdeskbot_question.html" %} +{% include "messages/helpdeskbot_channel_question.html" %} diff --git a/helpdeskbot/templates/messages/helpdeskbot_room_question.html b/helpdeskbot/templates/messages/helpdeskbot_room_question.html new file mode 100644 index 000000000..a55ceffb8 --- /dev/null +++ b/helpdeskbot/templates/messages/helpdeskbot_room_question.html @@ -0,0 +1,7 @@ +{{ question.title }} от {{ user.full_name }} + +{{ question.body }} + +{% if channel_message_link %}🔗 Ссылка на вопрос в канале +{% endif %} +↩️ Вы можете ответить реплаем на это сообщение. Ваш ответ перешлется в канал.