src/Library/TelegramBot/CandidatesPhotoTelegramBot/BotNavigation.php line 76

Open in your IDE?
  1. <?php
  2. namespace App\Library\TelegramBot\CandidatesPhotoTelegramBot;
  3. use App\Entity\CalendarEvent;
  4. use App\Entity\Person;
  5. use App\Entity\TelegramUser;
  6. use App\Entity\User;
  7. use App\Enum\CalendarEvent\Type;
  8. use App\Enum\Candidate\Status;
  9. use App\Library\TelegramBot\CandidatesPhotoTelegramBot\Enum\Menu\Menu;
  10. use App\Library\TelegramBot\CandidatesPhotoTelegramBot\Enum\EditableMessage\EditableMessage;
  11. use App\Library\Utils\StringUtils;
  12. use App\Service\CalendarEvent\CalendarEventService;
  13. use App\Service\Candidate\CandidateService;
  14. use App\Service\Person\PersonService;
  15. use App\Service\User\UserService;
  16. class BotNavigation
  17. {
  18.     /**
  19.      * @var CandidatesPhotoTelegramBot
  20.      */
  21.     private $bot;
  22.     /**
  23.      * @var CalendarEventService
  24.      */
  25.     private $calendarEventService;
  26.     /**
  27.      * @var UserService
  28.      */
  29.     private $userService;
  30.     /**
  31.      * @var CandidateService
  32.      */
  33.     private $candidateService;
  34.     public function __construct(CandidatesPhotoTelegramBot $telegramBot,
  35.                                 CalendarEventService       $calendarEventServiceUserService $userServiceCandidateService $candidateService)
  36.     {
  37.         $this->bot $telegramBot;
  38.         $this->calendarEventService $calendarEventService;
  39.         $this->userService $userService;
  40.         $this->candidateService $candidateService;
  41.     }
  42.     public function onAnyBotMenuItemSelect($chatIdTelegramUser $user$selectedItemId$navSelectedMenuItem)
  43.     {
  44.         switch($selectedItemId) {
  45.             case Menu::PERSON_MENU_ID:
  46.                 $this->bot->onPersonMenuItemSelect($chatId$user$selectedItemId$navSelectedMenuItem);
  47.                 break;
  48.             case Menu::CALENDAR_EVENT_MENU_ID:
  49.                 $this->bot->onCalendarEventMenuItemSelect($chatId$user$selectedItemId$navSelectedMenuItem);
  50.                 break;
  51.             case Menu::AUTHOR_MENU_ID:
  52.                 $this->bot->onAuthorMenuItemSelect($chatId$user$selectedItemId$navSelectedMenuItem);
  53.                 break;
  54.             case Menu::STATUS_MENU_ID:
  55.                 $this->bot->onStatusMenuItemSelect($chatId$user$selectedItemId$navSelectedMenuItem);
  56.                 break;
  57.             case Menu::CANDIDATE_FIELD_MENU_ID:
  58.                 $this->onCandidateFieldMenuItemSelect($chatId$user$selectedItemId$navSelectedMenuItem);
  59.                 break;
  60.             case Menu::OTHER_MENU_ID:
  61.                 $this->onOtherMenuItemSelect($chatId$user$selectedItemId$navSelectedMenuItem);
  62.                 break;
  63.             case Menu::AFTER_CANDIDATE_SEND_MENU_ID:
  64.                 $this->onAfterCandidateSendMenuItemSelect($chatId$user$selectedItemId$navSelectedMenuItem);
  65.                 break;
  66.             default:
  67.                 throw new \Exception("Unknown: " $selectedItemId);
  68.         }
  69.     }
  70.     private function onAfterCandidateSendMenuItemSelect($chatIdTelegramUser $user$selectedItemId$navSelectedMenuItem)
  71.     {
  72.         switch($navSelectedMenuItem['id']) {
  73.             case Menu::COPY_LAST_CANDIDATE_MENU_ITEM_ID:
  74.                 $this->bot->onCopyLastCandidateMenuItemSelect($chatId$user$navSelectedMenuItemMenu::CANDIDATE_FIELD_MENU_ID);
  75.                 break;
  76.             default:
  77.                 throw new \Exception("Unknown: " $navSelectedMenuItem['id']);
  78.         }
  79.     }
  80.     private function onOtherMenuItemSelect($chatIdTelegramUser $user$selectedItemId$navSelectedMenuItem)
  81.     {
  82.         switch($navSelectedMenuItem['id']) {
  83.             case Menu::COPY_LAST_CANDIDATE_MENU_ITEM_ID:
  84.                 $this->bot->onCopyLastCandidateMenuItemSelect($chatId$user$navSelectedMenuItem);
  85.                 break;
  86.             case Menu::RESET_CANDIDATE_MENU_ITEM_ID:
  87.                 $this->bot->onResetCandidateMenuItemSelect($chatId$user$navSelectedMenuItemMenu::CANDIDATE_FIELD_MENU_ID);
  88.                 break;
  89.             case "back":
  90.                 $this->sendSelectCandidateFieldMenu($chatId$user);
  91.                 break;
  92.             default:
  93.                 throw new \Exception("Unknown: " $navSelectedMenuItem['id']);
  94.         }
  95.     }
  96.     /**
  97.      * @param $chatId
  98.      * @param TelegramUser $user
  99.      * @param array|Person[] $persons
  100.      * @param $searchText
  101.      * @return void
  102.      */
  103.     public function sendSelectPersonMenu($chatIdTelegramUser $user, array $persons$searchText)
  104.     {
  105.         $msg "По Вашему запросу \"" $searchText .  "\" найден" StringUtils::getEnding(count($persons), """о""о") . " " count($persons)
  106.                 . " человек" StringUtils::getEnding(count($persons), """а""ов") . ":\n";
  107.         $personIndex = -1;
  108.         $menuItemsData = [];
  109.         foreach ($persons as $person) {
  110.             $personIndex++;
  111.             $menuItemText =
  112.                 PersonService::getFirstLastNameWithOld($person) .
  113.                     (count($persons) > 1
  114.                     ? (" " . ($person->getCity() ? $person->getCity()->getName() : "Город не указан") . "")
  115.                     : "")
  116.                 . (!$person->isSameName($searchText) ? " – ‼️Имя НЕ совпадает" " – ✅ Имя совпадает")
  117.             ;
  118.             $menuItemsData[$person->getId()] = [
  119.                 "text" => $menuItemText,
  120.                 "city" => $person->getCity() ? $person->getCity()->getName() : "Город не указан",
  121.             ];
  122.             $msg .= "\n" . ($personIndex 1) . ". " $menuItemText;
  123.         }
  124.         $msg .= "\n\nПодтвердите выбор участника, нажав на его имя или введите имя заново:";
  125.         $msg $this->createMainEditableMessageText(null$msg);
  126.         $this->createAndSendMenu($chatId$user$msgMenu::PERSON_MENU_ID$menuItemsData);
  127.     }
  128.     public function createMainEditableMessageText(?string $topText nullstring $footerText null): string
  129.     {
  130.         $msg "";
  131.         if ($topText) {
  132.             $msg .= $topText "\n\n";
  133.         } else {
  134.             $msg .= "Черновик анкеты нового участника мероприятия:\n\n";
  135.         }
  136.         $navStatusSelectedItem $this->bot->getNavigationSelectedItem($this->bot->telegramUserMenu::STATUS_MENU_ID);
  137.         $candidate $this->bot->candidate;
  138.         $student $candidate->getPerson() ? $candidate->getPerson()->getStudent() : null;
  139.         $ce $candidate->getCalendarEvent();
  140.         $person $candidate->getPerson();
  141.         $nameText = ($candidate->getPerson() ? $candidate->getPerson()->getName() : "Не указано");
  142.         $levelText = ($candidate->getPerson() && $candidate->getPerson()->getStudent()
  143.             ? $candidate->getPerson()->getStudent()->getLevelText()
  144.             : "нет уровня");
  145.         $cityText = ($candidate->getPerson() && $candidate->getPerson()->getCity()
  146.             ? ", " $candidate->getPerson()->getCity()->getName()
  147.             : "");
  148.         $imageText = ($candidate->getImage() ? "Указана" "Не указана");
  149.         $ceText = ($candidate->getCalendarEvent()
  150.             ? $candidate->getCalendarEvent()->getShortInfo()
  151.             : "Не указано");
  152.         $commentText StringUtils::addDot(($candidate->getComment() ?: "Не указан"));
  153.         $reviewApproveCommentText = ($candidate->getReviewApproveComment() ?: "Не указан");
  154.         $curatorText = ($candidate->getCurator()
  155.             ? $candidate->getCurator()->getName()
  156.             : "Не указан");
  157.         $statusText = ($navStatusSelectedItem
  158.             Status::getText($navStatusSelectedItem['id'])
  159.             : ($candidate->getStatus() == Status::STATUS_DRAFT
  160.                 Status::getText(Status::STATUS_NEW) : $candidate->getStatusText()));
  161.         $formDateText = ($candidate->getCreatedAt()
  162.             ? $candidate->getCreatedAt()->format("d.m.Y")
  163.             : "Не указана");
  164.         $msg .= "🙍‍♂️ <b>Имя</b>: $nameText ($levelText{$cityText}).
  165. 📷 <b>Фотография</b>: $imageText.
  166. 🗓️ <b>Мероприятие</b>: $ceText.";
  167.         if ($ce && $person) {
  168.             $msg .= "
  169. {$ce->getFromLevelEmoji()} <b>Уровень</b>: "
  170.                 . ($ce->getFromLevelText() ?: "без уровня")
  171.                 . ($ce->isStudentLevelCorrespond($person) ? " ✅" " ❌") . ".";
  172.         }
  173.         if ($ce && $person && $ce->getRequireCalendarEventsParticipations()->count()) {
  174.             $msg .= "
  175. 📅 <b>Участие</b>: {$ce->getRequireCalendarEventsParticipationsText()}"
  176.             . ($this->calendarEventService->getMissingRequiredCandidateCalendarEvents($person$ce) ? " ❌" :  " ✅") . ".";
  177.         }
  178.         if ($ce && $person && $ce->getDenyCalendarEventsParticipations()->count()) {
  179.             $msg .= "
  180. 📅 <b>Исключить</b>: {$ce->getDenyCalendarEventsParticipationsText()}"
  181.                 . ($this->calendarEventService->getDeniedCalendarEventsForPerson($person$ce) ? " ❌" :  " ✅") . ".";
  182.         }
  183. $msg .= "
  184. 💬 <b>Комментарий</b>: $commentText";
  185.         if (!($student && $student->isCandidateReviewIsNotRequired()) && ($candidate->getCalendarEvent() && !$candidate->getCalendarEvent()->isRegistrationOpen())
  186.                 || ($navStatusSelectedItem
  187.                         && $navStatusSelectedItem['id'] == Status::STATUS_APPROVED
  188.                         && $candidate->getCalendarEvent()
  189.                         && Type::isApproveReviewCommentRequireType($candidate->getCalendarEvent()->getType()))) {
  190.             $msg .= "
  191. 💬 <b>Комментарий согласования о пройденном отборе</b>: $reviewApproveCommentText.";
  192.         }
  193.         $msg .= "
  194. 👤 <b>Куратор</b>: $curatorText.
  195. 📊 <b>Статус</b>: $statusText.
  196. 📅 <b>Дата анкеты</b>: $formDateText.";
  197.         if ($footerText) {
  198.             $msg .= "\n\n" $footerText;
  199.         }
  200.         return $msg;
  201.     }
  202.     public function editOrSendMainEditableMessage(TelegramUser $userstring $chatIdstring $text, array $buttons null)
  203.     {
  204.         $replyMarkup null;
  205.         if ($buttons) {
  206.             $replyMarkup $this->bot->createReplyMarkupFromButtonsArray($buttons);
  207.         }
  208.         $this->bot->editOrSendEditableMessage($user$chatIdEditableMessage::EDITABLE_MESSAGE_ID__MAIN$text$replyMarkup);
  209.     }
  210.     public function editOrSendPhotoEditableMessage(TelegramUser $userstring $chatIdstring $imageFileId)
  211.     {
  212.         $this->bot->editOrSendEditableMessage($user$chatIdEditableMessage::EDITABLE_MESSAGE_ID__PHOTOnullnull$imageFileId);
  213.     }
  214.     public function deleteMainEditableMessage(TelegramUser $userstring $chatId)
  215.     {
  216.         $editableMessageId $this->bot->getEditableMessageId($userEditableMessage::EDITABLE_MESSAGE_ID__MAIN);
  217.         if ($editableMessageId) {
  218.             try {
  219.                 $this->bot->deleteRemoteMessage($chatId$editableMessageId);
  220.             } catch (\Throwable $exception) {
  221.                 //ignore
  222.             }
  223.         }
  224.     }
  225.     public function sendSelectCandidateFieldMenu($chatIdTelegramUser $user$msg null,
  226.         $resetNav true)
  227.     {
  228.         $msg $this->createMainEditableMessageText(null,
  229.             $msg ?? "Выберите поле анкеты участника для изменения:");
  230.         if ($resetNav) {
  231.             $this->resetCandidateFieldNamedNav($user);
  232.         }
  233.         $this->createAndSendMenu($chatId$user$msgMenu::CANDIDATE_FIELD_MENU_ID,
  234.             $this->getCandidateFieldNameData2());
  235.     }
  236.     public function createCandidateFieldsMenu(): array
  237.     {
  238.         return $this->createMenu(Menu::CANDIDATE_FIELD_MENU_ID,
  239.             $this->getCandidateFieldNameData2());
  240.     }
  241.     public function resetCandidateFieldNamedNav(TelegramUser $user$defaultNavItemId null)
  242.     {
  243.         $candidateFieldNameData $this->createCandidateFieldsMenu();
  244.         $this->bot->setNavigationItems($userMenu::CANDIDATE_FIELD_MENU_ID,
  245.             $candidateFieldNameData['navItems']);
  246.         if (!$defaultNavItemId) {
  247.             $defaultNavItemId $candidateFieldNameData['navItemIds'][Menu::PERSON_MENU_ID];
  248.         }
  249.         $this->bot->setNavigationSelectedItemId($user,
  250.             Menu::CANDIDATE_FIELD_MENU_ID$defaultNavItemId);
  251.     }
  252.     public function resetEditableMessages(TelegramUser $user)
  253.     {
  254.         $this->bot->setEditableMessageId($userEditableMessage::EDITABLE_MESSAGE_ID__MAINnull);
  255.         $this->bot->setEditableMessageId($userEditableMessage::EDITABLE_MESSAGE_ID__PHOTOnull);
  256.         $this->bot->setNavigationSelectedItemId($this->bot->telegramUserMenu::STATUS_MENU_IDnull);
  257.     }
  258.     public function getCandidateFieldNameData2(): array
  259.     {
  260.         $navStatusSelectedItem $this->bot->getNavigationSelectedItem($this->bot->telegramUserMenu::STATUS_MENU_ID);
  261.         $dataToEdit = [
  262.             Menu::PERSON_MENU_ID => ["text" => "🙍‍♂️ Имя"],
  263.             Menu::PHOTO_MENU_ID => ["text" => "📷 Фотография"],
  264.             Menu::CALENDAR_EVENT_MENU_ID => ["text" => "🗓️ Мероприятие"],
  265.             Menu::COMMENT_MENU_ID => ["text" => "💬 Комментарий"],
  266.         ];
  267.         if ($this->bot->candidate && $this->bot->candidate->getCalendarEvent()
  268.             && (!$this->bot->candidate->getCalendarEvent()->isRegistrationOpen() ||
  269.             $navStatusSelectedItem
  270.             && $navStatusSelectedItem['id'] == Status::STATUS_APPROVED)) {
  271.             $dataToEdit[Menu::REVIEW_APPROVE_COMMENT_MENU_ID] = [
  272.                 "text" => "💬 Комментарий согласования о пройденном отборе"
  273.             ];
  274.         }
  275.         $dataToEdit array_merge($dataToEdit,  [
  276.             Menu::AUTHOR_MENU_ID => ["text" => "👤 Куратор"],
  277.             Menu::STATUS_MENU_ID => ["text" => "📊 Статус"],
  278.             Menu::DATE_MENU_ID => ["text" => "📅 Дата анкеты"],
  279.             "confirm" => ["text" => "✅ Отправить анкету участника"],
  280.             Menu::OTHER_MENU_ID => ["text" => "Другое"]
  281.         ]);
  282.         return $dataToEdit;
  283.     }
  284.     public function sendSelectCalendarEventMenu($chatIdTelegramUser $user)
  285.     {
  286.         $msg $this->createMainEditableMessageText(null"Выберите мероприятие:");
  287.         /**
  288.          * @var CalendarEvent[] $calendarEvents
  289.          */
  290.         $calendarEvents $this->calendarEventService->getCalendarEventsForRegister();
  291.         $menuItemsData = [];
  292.         foreach ($calendarEvents as $calendarEvent) {
  293.             $menuItemsData[$calendarEvent->getId()] = ["text" => $calendarEvent->getText(['quotes' => true])];
  294.         }
  295.         $this->createAndSendMenu($chatId$user$msgMenu::CALENDAR_EVENT_MENU_ID$menuItemsData);
  296.     }
  297.     private function createLastCandidateMenuItemData($text null): array
  298.     {
  299.         $menuItemData = [
  300.                 Menu::COPY_LAST_CANDIDATE_MENU_ITEM_ID => ["text" => "📄 Копировать анкету последнего кандидата"],
  301.         ];
  302.         if ($text) {
  303.             $menuItemData[Menu::COPY_LAST_CANDIDATE_MENU_ITEM_ID]['text'] = $text;
  304.         }
  305.         return $menuItemData;
  306.     }
  307.     private function createResetCandidateMenuItemData($text null): array
  308.     {
  309.         $menuItemData = [
  310.             Menu::RESET_CANDIDATE_MENU_ITEM_ID => ["text" => "🧹 Сбросить анкету участника и начать заново"],
  311.         ];
  312.         if ($text) {
  313.             $menuItemData[Menu::RESET_CANDIDATE_MENU_ITEM_ID]['text'] = $text;
  314.         }
  315.         return $menuItemData;
  316.     }
  317.     public function sendAfterCandidateSendMenu($chatIdTelegramUser $user)
  318.     {
  319.         $msg "✅ Участник успешно зарегистрирован.\n\nДля регистрации следующего участника" .
  320.                 " введите 🙍‍♂️ имя или отправьте 📷 фотографию."
  321.                 " Либо можете зарегистрировать на еще одно мероприятие этого кандидата.";
  322.         $msg $this->createMainEditableMessageText("Анкета"
  323.             " зарегистрированного участника мероприятия:"$msg);
  324.         $menuItemsData array_merge(
  325.             $this->createLastCandidateMenuItemData("📋 Копировать анкету и изменить Имя или Меропирятие")
  326.         );
  327.         $this->createAndSendMenu($chatId$user$msgMenu::AFTER_CANDIDATE_SEND_MENU_ID$menuItemsData);
  328.     }
  329.     public function sendSelectAuthorMenu($chatIdTelegramUser $user)
  330.     {
  331.         $msg $this->createMainEditableMessageText(null"Выберите куратора участника:");
  332.         /**
  333.          * @var User[] $authors
  334.          */
  335.         $authors $this->userService->getCurators();
  336.         $menuItemsData = [];
  337.         foreach ($authors as $author) {
  338.             $menuItemsData[$author->getId()] = ["text" => $author->getName()];
  339.         }
  340.         $this->createAndSendMenu($chatId$user$msgMenu::AUTHOR_MENU_ID$menuItemsData);
  341.     }
  342.     public function sendSelectStatusMenu($chatIdTelegramUser $user)
  343.     {
  344.         $msg $this->createMainEditableMessageText(null"Выберите статус участника:");
  345.         $menuItemsData = [
  346.             Status::STATUS_NEW => ["text" => Status::getText(Status::STATUS_NEW)],
  347.             Status::STATUS_APPROVED => ["text" => Status::getText(Status::STATUS_APPROVED)],
  348.         ];
  349.         $this->createAndSendMenu($chatId$user$msgMenu::STATUS_MENU_ID$menuItemsData);
  350.     }
  351.     /**
  352.      * @param array{string, array} $menuItemsData
  353.      * Example:
  354.      * [
  355.      *   "menu_item_id_1" => ["text" => "Menu Item 1", "some_data_key" => "data", ...],
  356.      * ]
  357.      * @return array{buttons: array, navItems: array}
  358.      */
  359.     public function createMenu($menuId, array $menuItemsData): array
  360.     {
  361.         $buttons = [];
  362.         $navItems = [];
  363.         $navItemIds = [];
  364.         foreach ($menuItemsData as $menuItemId => $menuItemData) {
  365.             $navItems[$menuId "__" $menuItemId] = ['id' => $menuItemId];
  366.             foreach ($menuItemData as $key => $value) {
  367.                 $navItems[$menuId "__" $menuItemId][$key] = $value;
  368.             }
  369.             $buttons[$menuId "__" $menuItemId] = $menuItemData['text'];
  370.             $navItemIds[$menuItemId] = $menuId "__" $menuItemId;
  371.         }
  372.         return [
  373.             'buttons' => $buttons,
  374.             'navItems' => $navItems,
  375.             'navItemIds' => $navItemIds,
  376.         ];
  377.     }
  378.     /**
  379.      * @param array{string, array} $menuItemsData
  380.      * Example:
  381.      * [
  382.      *   "menu_item_id_1" => ["text" => "Menu Item 1", "some_data_key" => "data", ...],
  383.      * ]
  384.      */
  385.     public function createAndSendMenu($chatIdTelegramUser $user$msg$menuId, array $menuItemsData)
  386.     {
  387.         $menuResult $this->createMenu($menuId$menuItemsData);
  388.         $buttons $menuResult['buttons'];
  389.         $navItems $menuResult['navItems'];
  390.         $this->bot->setNavigationItems($user$menuId$navItems);
  391.         $this->editOrSendMainEditableMessage($user$chatId$msg$buttons);
  392.     }
  393.     public function sendSelectOtherMenu($chatIdTelegramUser $user$msg null)
  394.     {
  395.         $msg $this->createMainEditableMessageText(null$msg ?? "Выберите пункт меню:");
  396.         $menuItemsData array_merge(
  397.             $this->createLastCandidateMenuItemData(),
  398.             $this->createResetCandidateMenuItemData(),
  399.             [
  400.                 "back" => ["text" => "🔙 Назад"]
  401.             ]
  402.         );
  403.         $this->createAndSendMenu($chatId$user$msgMenu::OTHER_MENU_ID$menuItemsData);
  404.     }
  405.     public function onCandidateFieldMenuItemSelect($chatIdTelegramUser $user$selectedItemId$navSelectedMenuItem)
  406.     {
  407.         $selectedCandidateFieldId $navSelectedMenuItem['id'];
  408.         switch($selectedCandidateFieldId) {
  409.             case Menu::PERSON_MENU_ID:
  410.                 $msg "🙍‍♂️ Введите имя участника:";
  411.                 $this->sendSelectCandidateFieldMenu($chatId$user$msgfalse);
  412.                 return;
  413.             case Menu::COMMENT_MENU_ID:
  414.                 $msg "💬 Введите комментарий для участника:";
  415.                 $this->sendSelectCandidateFieldMenu($chatId$user$msgfalse);
  416.                 return;
  417.             case Menu::REVIEW_APPROVE_COMMENT_MENU_ID:
  418.                 $msg "💬 Введите комментарий согласования о пройденном отборе участника:";
  419.                 $this->sendSelectCandidateFieldMenu($chatId$user$msgfalse);
  420.                 return;
  421.             case Menu::DATE_MENU_ID:
  422.                 $msg "📅 Введите дату анкеты участника в формате ДД.ММ.ГГГГ:";
  423.                 $this->sendSelectCandidateFieldMenu($chatId$user$msgfalse);
  424.                 return;
  425.             case Menu::CALENDAR_EVENT_MENU_ID:
  426.                 $this->sendSelectCalendarEventMenu($chatId$user);
  427.                 break;
  428.             case Menu::AUTHOR_MENU_ID:
  429.                 $this->sendSelectAuthorMenu($chatId$user);
  430.                 break;
  431.             case Menu::STATUS_MENU_ID:
  432.                 $this->sendSelectStatusMenu($chatId$user);
  433.                 break;
  434.             case Menu::PHOTO_MENU_ID:
  435.                 $msg "📷 Прикрепите и отправьте или"
  436.                     " перешлите из другого чата фотографию:";
  437.                 $this->sendSelectCandidateFieldMenu($chatId$user$msgfalse);
  438.                 return;
  439.             case "confirm":
  440.                 $this->bot->onCandidateConfirmMenuItemSelect($chatId$user);
  441.                 break;
  442.             case "other":
  443.                 $this->sendSelectOtherMenu($chatId$user);
  444.                 break;
  445.             default:
  446.                 throw new \Exception("Unknown: " $navSelectedMenuItem['id']);
  447.         }
  448.     }
  449. }