chromium/ash/webui/boca_ui/provider/classroom_page_handler_impl.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 "ash/webui/boca_ui/provider/classroom_page_handler_impl.h"

#include "base/memory/weak_ptr.h"
#include "base/task/thread_pool.h"
#include "chromeos/ash/components/boca/boca_app_client.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "google_apis/classroom/classroom_api_courses_response_types.h"
#include "google_apis/classroom/classroom_api_list_courses_request.h"
#include "google_apis/classroom/classroom_api_list_students_request.h"
#include "google_apis/classroom/classroom_api_students_response_types.h"
#include "google_apis/common/auth_service.h"
#include "google_apis/common/request_sender.h"
#include "google_apis/gaia/gaia_constants.h"

namespace ash::boca {
namespace {
using ::google_apis::ApiErrorCode;
using ::google_apis::classroom::ListCoursesRequest;
using ::google_apis::classroom::ListStudentsRequest;

// TODO(b/343731449): Update this once policy to control has been added.
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("boca_classroom_integration", R"(
          semantics: {
            sender: "Boca"
            description: "Provide ChromeOS users easy access to roster and "
                         "courselist to control their in-classroom experience."
            internal {
              contacts {
                  email: "[email protected]"
              }
            }
            user_data {
              type: ACCESS_TOKEN
              type: EMAIL
              type: NAME
            }
            trigger: "User opens Boca app and goes through session start flow."
            data: "The request is authenticated with an OAuth2 access token "
                  "identifying the Google account. Rosters and courselists "
                  "containing emails and names are sent to Boca."
            destination: GOOGLE_OWNED_SERVICE
            last_reviewed: "2024-06-26"
          }
          policy: {
            cookies_allowed: NO
            setting: "This feature cannot be disabled by settings yet."
            policy_exception_justification: "Not implemented yet."
          })");
}  // namespace

ClassroomPageHandlerImpl::ClassroomPageHandlerImpl(
    std::unique_ptr<google_apis::RequestSender> sender)
    : sender_(std::move(sender)), weak_factory_(this) {}

ClassroomPageHandlerImpl::ClassroomPageHandlerImpl()
    : ClassroomPageHandlerImpl(CreateRequestSender()) {}

ClassroomPageHandlerImpl::~ClassroomPageHandlerImpl() = default;

void ClassroomPageHandlerImpl::ListCourses(const std::string& teacher_id,
                                           ListCoursesCallback callback) {
  valid_course_ids_.clear();
  ListCoursesHelper(teacher_id, /*page_token=*/"",
                    std::make_unique<CourseList>(), std::move(callback));
}

void ClassroomPageHandlerImpl::ListStudents(const std::string& course_id,
                                            ListStudentsCallback callback) {
  if (valid_course_ids_.find(course_id) == valid_course_ids_.end()) {
    return std::move(callback).Run(StudentList());
  }
  ListStudentsHelper(course_id, /*page_token=*/"",
                     std::make_unique<StudentList>(), std::move(callback));
}

void ClassroomPageHandlerImpl::ListCoursesHelper(
    const std::string& teacher_id,
    const std::string& page_token,
    std::unique_ptr<CourseList> fetched_courses,
    ListCoursesCallback callback) {
  sender_->StartRequestWithAuthRetry(std::make_unique<ListCoursesRequest>(
      sender_.get(), /*student_id=*/"", teacher_id, page_token,
      base::BindOnce(&ClassroomPageHandlerImpl::OnListCoursesFetched,
                     weak_factory_.GetWeakPtr(), teacher_id,
                     std::move(fetched_courses), std::move(callback))));
}

void ClassroomPageHandlerImpl::OnListCoursesFetched(
    const std::string& teacher_id,
    std::unique_ptr<CourseList> fetched_courses,
    ListCoursesCallback callback,
    base::expected<std::unique_ptr<google_apis::classroom::Courses>,
                   ApiErrorCode> result) {
  if (!result.has_value()) {
    std::move(callback).Run(std::move(*fetched_courses));
    return;
  }

  for (const auto& item : result.value()->items()) {
    mojom::CoursePtr course = mojom::Course::New(item->id(), item->name());
    fetched_courses->push_back(std::move(course));
    valid_course_ids_.insert(item->id());
  }

  if (!result.value()->next_page_token().empty()) {
    ListCoursesHelper(teacher_id, result.value()->next_page_token(),
                      std::move(fetched_courses), std::move(callback));
  } else {
    std::move(callback).Run(std::move(*fetched_courses));
  }
}

void ClassroomPageHandlerImpl::ListStudentsHelper(
    const std::string& course_id,
    const std::string& page_token,
    std::unique_ptr<StudentList> fetched_students,
    ListStudentsCallback callback) {
  sender_->StartRequestWithAuthRetry(std::make_unique<ListStudentsRequest>(
      sender_.get(), course_id, page_token,
      base::BindOnce(&ClassroomPageHandlerImpl::OnListStudentsFetched,
                     weak_factory_.GetWeakPtr(), course_id,
                     std::move(fetched_students), std::move(callback))));
}

void ClassroomPageHandlerImpl::OnListStudentsFetched(
    const std::string& course_id,
    std::unique_ptr<StudentList> fetched_students,
    ListStudentsCallback callback,
    base::expected<std::unique_ptr<google_apis::classroom::Students>,
                   ApiErrorCode> result) {
  if (!result.has_value()) {
    std::move(callback).Run(std::move(*fetched_students));
    return;
  }

  for (const auto& item : result.value()->items()) {
    mojom::IdentityPtr student = mojom::Identity::New(
        item->profile().id(), item->profile().name().full_name(),
        item->profile().email_address());

    fetched_students->push_back(std::move(student));
  }

  if (!result.value()->next_page_token().empty()) {
    ListStudentsHelper(course_id, result.value()->next_page_token(),
                       std::move(fetched_students), std::move(callback));
  } else {
    std::move(callback).Run(std::move(*fetched_students));
  }
}

// static
std::unique_ptr<google_apis::RequestSender>
ClassroomPageHandlerImpl::CreateRequestSender() {
  std::vector<std::string> scopes = {
      GaiaConstants::kClassroomReadOnlyRostersOAuth2Scope,
      GaiaConstants::kClassroomReadOnlyCoursesOAuth2Scope,
  };
  auto url_loader_factory = BocaAppClient::Get()->GetURLLoaderFactory();
  auto* identity_manager = BocaAppClient::Get()->GetIdentityManager();
  auto auth_service = std::make_unique<google_apis::AuthService>(
      identity_manager,
      identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSignin),
      url_loader_factory, scopes);
  return std::make_unique<google_apis::RequestSender>(
      std::move(auth_service), url_loader_factory,
      base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(),
           /* `USER_VISIBLE` is because the requested/returned data is visible
               to the user on System UI surfaces. */
           base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
      /*custom_user_agent=*/"", kTrafficAnnotation);
}

}  // namespace ash::boca