chromium/chromeos/ash/components/boca/session_api/get_session_request.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/boca/session_api/get_session_request.h"

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>
#include <string>

#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/strings/utf_string_conversions.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "chromeos/ash/components/boca/proto/bundle.pb.h"
#include "chromeos/ash/components/boca/proto/roster.pb.h"
#include "chromeos/ash/components/boca/proto/session.pb.h"
#include "chromeos/ash/components/boca/session_api/constants.h"
#include "chromeos/ash/components/boca/session_api/get_session_request.h"
#include "google_apis/common/api_error_codes.h"
#include "google_apis/common/base_requests.h"
#include "google_apis/common/request_sender.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/base/url_util.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"

namespace ash::boca {
namespace {

// TODO(b/359985023):Remove manual serialization after server is enabled to
// send proto.
::boca::StudentStatus::StudentState StudentStatusJsonToProto(
    const std::string& status) {
  if (status == "STUDENT_STATE_UNKNOWN") {
    return ::boca::StudentStatus::STUDENT_STATE_UNKNOWN;
  }
  if (status == "ADDED") {
    return ::boca::StudentStatus::ADDED;
  }
  if (status == "ACTIVE") {
    return ::boca::StudentStatus::ACTIVE;
  }
  if (status == "REMOVED_BY_OTHER_SESSION") {
    return ::boca::StudentStatus::REMOVED_BY_OTHER_SESSION;
  }
  if (status == "REMOVED_BY_BEING_TEACHER") {
    return ::boca::StudentStatus::REMOVED_BY_BEING_TEACHER;
  }
  if (status == "REMOVED_BY_TEACHER") {
    return ::boca::StudentStatus::REMOVED_BY_TEACHER;
  }
  return ::boca::StudentStatus::STUDENT_STATE_UNKNOWN;
}

::boca::Session::SessionState SessionStateJsonToProto(
    const std::string& state) {
  if (state == "SESSION_STATE_UNKNOWN") {
    return ::boca::Session::SESSION_STATE_UNKNOWN;
  }
  if (state == "PLANNING") {
    return ::boca::Session::PLANNING;
  }
  if (state == "ACTIVE") {
    return ::boca::Session::ACTIVE;
  }
  if (state == "PAST") {
    return ::boca::Session::PAST;
  }
  return ::boca::Session::SESSION_STATE_UNKNOWN;
}

::boca::LockedNavigationOptions::NavigationType NavigationTypeJsonToProto(
    const std::string& type) {
  if (type == "NAVIGATION_TYPE_UNKNOWN") {
    return ::boca::LockedNavigationOptions::NAVIGATION_TYPE_UNKNOWN;
  }
  if (type == "OPEN_NAVIGATION") {
    return ::boca::LockedNavigationOptions::OPEN_NAVIGATION;
  }
  if (type == "BLOCK_NAVIGATION") {
    return ::boca::LockedNavigationOptions::BLOCK_NAVIGATION;
  }
  if (type == "DOMAIN_NAVIGATION") {
    return ::boca::LockedNavigationOptions::DOMAIN_NAVIGATION;
  }
  if (type == "LIMITED_NAVIGATION") {
    return ::boca::LockedNavigationOptions::LIMITED_NAVIGATION;
  }
  return ::boca::LockedNavigationOptions::NAVIGATION_TYPE_UNKNOWN;
}

void ParseTeacher(base::Value::Dict* session_dict, ::boca::Session* session) {
  if (session_dict->FindDict(kTeacher)) {
    auto* teacher = session->mutable_teacher();
    if (auto* ptr = session_dict->FindDict(kTeacher)->FindString(kEmail)) {
      teacher->set_email(*ptr);
    }
    if (auto* ptr = session_dict->FindDict(kTeacher)->FindString(kGaiaId)) {
      teacher->set_gaia_id(*ptr);
    }
    if (auto* ptr = session_dict->FindDict(kTeacher)->FindString(kFullName)) {
      teacher->set_full_name(*ptr);
    }

    if (auto* ptr = session_dict->FindDict(kTeacher)->FindString(kPhotoUrl)) {
      teacher->set_photo_url(*ptr);
    }
  }
}

void ParseRoster(base::Value::Dict* session_dict, ::boca::Session* session) {
  auto* roster_dict = session_dict->FindDict(kRoster);
  if (roster_dict) {
    auto* roster = session->mutable_roster();
    if (auto* ptr = roster_dict->FindString(kRosterTitle)) {
      roster->set_title(*ptr);
    }

    auto* student_list_dict = roster_dict->FindList(kStudentGroups);
    if (student_list_dict) {
      for (auto& students_dict : *student_list_dict) {
        auto* student_groups =
            session->mutable_roster()->mutable_student_groups()->Add();
        if (auto* ptr =
                students_dict.GetIfDict()->FindString(kStudentGroupTitle)) {
          student_groups->set_title(*ptr);
        }
        if (auto* items = students_dict.GetIfDict()->FindList(kStudents)) {
          for (auto& item : *items) {
            auto* item_dict = item.GetIfDict();

            auto* students = student_groups->mutable_students()->Add();
            if (auto* ptr = item_dict->FindString(kEmail)) {
              students->set_email(*ptr);
            }
            if (auto* ptr = item_dict->FindString(kFullName)) {
              students->set_full_name(*ptr);
            }
            if (auto* ptr = item_dict->FindString(kGaiaId)) {
              students->set_gaia_id(*ptr);
            }
            if (auto* ptr = item_dict->FindString(kPhotoUrl)) {
              students->set_photo_url(*ptr);
            }
          }
        }
      }
    }
  }
}

void ParseSessionConfig(base::Value::Dict* session_dict,
                        ::boca::Session* session) {
  if (session_dict->FindDict(kStudentGroupsConfig)) {
    auto* student_groups = session->mutable_student_group_configs();
    auto* config = session_dict->FindDict(kStudentGroupsConfig)
                       ->FindDict(kMainStudentGroupName);
    if (config) {
      ::boca::SessionConfig session_config;

      auto* caption_config_dict = config->FindDict(kCaptionsConfig);
      if (caption_config_dict) {
        auto* caption_config = session_config.mutable_captions_config();
        caption_config->set_captions_enabled(
            caption_config_dict->FindBool(kCaptionsEnabled).value_or(false));
        caption_config->set_translations_enabled(
            caption_config_dict->FindBool(kTranslationsEnabled)
                .value_or(false));
      }
      auto* on_task_config_dict = config->FindDict(kOnTaskConfig);
      if (on_task_config_dict) {
        auto* active_bundle_dict = on_task_config_dict->FindDict(kActiveBundle);
        if (active_bundle_dict) {
          auto* on_task_config = session_config.mutable_on_task_config();
          auto* active_bundle = on_task_config->mutable_active_bundle();
          active_bundle->set_locked(
              active_bundle_dict->FindBool(kLocked).value_or(false));
          auto* content_configs_list =
              active_bundle_dict->FindList(kContentConfigs);
          if (content_configs_list) {
            for (auto& item : *content_configs_list) {
              auto* content_configs =
                  active_bundle->mutable_content_configs()->Add();
              auto* item_dict = item.GetIfDict();
              if (auto* ptr = item_dict->FindString(kUrl)) {
                content_configs->set_url(*ptr);
              }
              if (auto* ptr = item_dict->FindString(kTitle)) {
                content_configs->set_title(*ptr);
              }
              if (auto* ptr = item_dict->FindString(kFavIcon)) {
                content_configs->set_favicon_url(*ptr);
              }
              if (item_dict->FindDict(kLockedNavigationOptions) &&
                  item_dict->FindDict(kLockedNavigationOptions)
                      ->FindString(kNavigationType)) {
                auto* lock_options =
                    content_configs->mutable_locked_navigation_options();
                lock_options->set_navigation_type(NavigationTypeJsonToProto(
                    *item_dict->FindDict(kLockedNavigationOptions)
                         ->FindString(kNavigationType)));
              }
            }
          }
        }
      }
      (*student_groups)[kMainStudentGroupName] = std::move(session_config);
    }
  }
}

void ParseStudentStatus(base::Value::Dict* session_dict,
                        ::boca::Session* session) {  // Student status.
  auto* student_status_dict = session_dict->FindDict(kStudentStatus);
  if (student_status_dict) {
    // Roster feature is disabled, always fetch the first item.
    if (session->roster().student_groups().size() > 0) {
      for (auto id : session->roster().student_groups()[0].students()) {
        if (auto* ptr = student_status_dict->FindDict(id.gaia_id())
                            ->FindString(kStudentStatusState)) {
          auto* student_statuses = session->mutable_student_statuses();
          ::boca::StudentStatus student_status;
          student_status.set_state(StudentStatusJsonToProto(*ptr));
          (*student_statuses)[id.gaia_id()] = std::move(student_status);
        }
      }
    }
  }
}

std::unique_ptr<::boca::Session> ParseResponse(std::string response) {
  std::unique_ptr<base::Value> raw_value = google_apis::ParseJson(response);

  if (!raw_value) {
    return nullptr;
  }

  auto session_dict = std::move(raw_value->GetIfDict());
  if (!session_dict) {
    return nullptr;
  }

  std::unique_ptr<::boca::Session> session =
      std::make_unique<::boca::Session>();

  if (auto* ptr = session_dict->FindString(kSessionId)) {
    session->set_session_id(*ptr);
  }

  if (session_dict->FindDict(kDuration)) {
    auto* duration = session->mutable_duration();
    duration->set_seconds(
        session_dict->FindDict(kDuration)->FindInt(kSeconds).value_or(0));
    duration->set_nanos(
        session_dict->FindDict(kDuration)->FindInt(kNanos).value_or(0));
  }

  if (session_dict->FindDict(kStartTime)) {
    auto* start_time = session->mutable_start_time();
    start_time->set_seconds(
        session_dict->FindDict(kStartTime)->FindInt(kSeconds).value_or(0));
    start_time->set_nanos(
        session_dict->FindDict(kStartTime)->FindInt(kNanos).value_or(0));
  }

  if (auto* ptr = session_dict->FindString(kSessionState)) {
    session->set_session_state(SessionStateJsonToProto(*ptr));
  }

  ParseTeacher(session_dict, session.get());

  ParseRoster(session_dict, session.get());

  ParseSessionConfig(session_dict, session.get());

  ParseStudentStatus(session_dict, session.get());

  return session;
}

}  // namespace

GetSessionRequest::GetSessionRequest(google_apis::RequestSender* sender,
                                     const std::string gaia_id,
                                     Callback callback)
    : UrlFetchRequestBase(sender,
                          google_apis::ProgressCallback(),
                          google_apis::ProgressCallback()),
      gaia_id_(gaia_id),
      url_base_(kSchoolToolsApiBaseUrl),
      callback_(std::move(callback)) {}

GetSessionRequest::~GetSessionRequest() = default;

void GetSessionRequest::OverrideURLForTesting(std::string url) {
  url_base_ = std::move(url);
}

GURL GetSessionRequest::GetURL() const {
  auto url = GURL(url_base_).Resolve(base::ReplaceStringPlaceholders(
      kGetSessionUrlTemplate, {gaia_id_}, nullptr));
  return url;
}

google_apis::ApiErrorCode GetSessionRequest::MapReasonToError(
    google_apis::ApiErrorCode code,
    const std::string& reason) {
  return code;
}

bool GetSessionRequest::IsSuccessfulErrorCode(google_apis::ApiErrorCode error) {
  return error == google_apis::HTTP_SUCCESS;
}

void GetSessionRequest::ProcessURLFetchResults(
    const network::mojom::URLResponseHead* response_head,
    base::FilePath response_file,
    std::string response_body) {
  google_apis::ApiErrorCode error = GetErrorCode();
  switch (error) {
    case google_apis::HTTP_SUCCESS:
      blocking_task_runner()->PostTaskAndReplyWithResult(
          FROM_HERE, base::BindOnce(&ParseResponse, std::move(response_body)),
          base::BindOnce(&GetSessionRequest::OnDataParsed,
                         weak_ptr_factory_.GetWeakPtr()));
      break;
    default:
      RunCallbackOnPrematureFailure(error);
      OnProcessURLFetchResultsComplete();
      break;
  }
}

void GetSessionRequest::RunCallbackOnPrematureFailure(
    google_apis::ApiErrorCode error) {
  std::move(callback_).Run(base::unexpected(error));
}

void GetSessionRequest::OnDataParsed(std::unique_ptr<::boca::Session> session) {
  if (!session) {
    std::move(callback_).Run(base::unexpected(google_apis::PARSE_ERROR));
  } else {
    std::move(callback_).Run(std::move(session));
  }
  OnProcessURLFetchResultsComplete();
}

}  // namespace ash::boca