WebhookController.java

package edu.ucsb.cs156.frontiers.controllers;


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import edu.ucsb.cs156.frontiers.entities.Course;
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
import edu.ucsb.cs156.frontiers.entities.User;
import edu.ucsb.cs156.frontiers.enums.OrgStatus;
import edu.ucsb.cs156.frontiers.repositories.CourseRepository;
import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository;
import edu.ucsb.cs156.frontiers.repositories.UserRepository;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@Tag(name = "Webhooks Controller")
@RestController
@RequestMapping("/api/webhooks")
@Slf4j
public class WebhookController {


    private final CourseRepository courseRepository;
    private final RosterStudentRepository rosterStudentRepository;

    public WebhookController(CourseRepository courseRepository, RosterStudentRepository rosterStudentRepository) {
        this.courseRepository = courseRepository;
        this.rosterStudentRepository = rosterStudentRepository;
    }

    /**
    * Accepts webhooks from GitHub, currently to update the membership status of a RosterStudent.
    * @param jsonBody body of the webhook. The description of the currently used webhook is available in docs/webhooks.md
    *
    * @return either the word success so GitHub will not flag the webhook as a failure, or the updated RosterStudent
    */
    @PostMapping("/github")
    public ResponseEntity<String> createGitHubWebhook(@RequestBody JsonNode jsonBody) throws JsonProcessingException {
        log.info("Received GitHub webhook: {}", jsonBody.toString());

        if(!jsonBody.has("action")){
            return ResponseEntity.ok().body("success");
        }
        
        String action = jsonBody.get("action").asText();
        log.info("Webhook action: {}", action);
        
        // Early return if not an action we care about
        if(!action.equals("member_added") && !action.equals("member_invited")) {
            return ResponseEntity.ok().body("success");
        }
        
        // Extract GitHub login based on payload structure
        String githubLogin = null;
        String installationId = null;
        
        // For member_added events, the structure is different
        if (action.equals("member_added")) {
            if (!jsonBody.has("membership") || 
                !jsonBody.get("membership").has("user") || 
                !jsonBody.get("membership").get("user").has("login") ||
                !jsonBody.has("installation") || 
                !jsonBody.get("installation").has("id")) {
                return ResponseEntity.ok().body("success");
            }
            
            githubLogin = jsonBody.get("membership").get("user").get("login").asText();
            installationId = jsonBody.get("installation").get("id").asText();
        } 
        // For member_invited events, use the original structure
        else { // must be "member_invited" based on earlier check
            if (!jsonBody.has("user") || 
                !jsonBody.get("user").has("login") || 
                !jsonBody.has("installation") || 
                !jsonBody.get("installation").has("id")) {
                return ResponseEntity.ok().body("success");
            }
            
            githubLogin = jsonBody.get("user").get("login").asText();
            installationId = jsonBody.get("installation").get("id").asText();
        }
        
        log.info("GitHub login: {}, Installation ID: {}", githubLogin, installationId);
        
        Optional<Course> course = courseRepository.findByInstallationId(installationId);
        log.info("Course found: {}", course.isPresent());
        
        if(!course.isPresent()){
            log.warn("No course found with installation ID: {}", installationId);
            return ResponseEntity.ok().body("success");
        }
        
        Optional<RosterStudent> student = rosterStudentRepository.findByCourseAndGithubLogin(course.get(), githubLogin);
        log.info("Student found: {}", student.isPresent());
        
        if(!student.isPresent()){
            log.warn("No student found with GitHub login: {} in course: {}", githubLogin, course.get().getCourseName());
            return ResponseEntity.ok().body("success");
        }
        
        RosterStudent updatedStudent = student.get();
        log.info("Current student org status: {}", updatedStudent.getOrgStatus());
        
        // Update status based on action
        if(action.equals("member_added")) {
            updatedStudent.setOrgStatus(OrgStatus.MEMBER);
            log.info("Setting status to MEMBER");
        } else { // must be "member_invited" based on earlier check
            updatedStudent.setOrgStatus(OrgStatus.INVITED);
            log.info("Setting status to INVITED");
        }
        
        rosterStudentRepository.save(updatedStudent);
        log.info("Student saved with new org status: {}", updatedStudent.getOrgStatus());
        return ResponseEntity.ok(updatedStudent.toString());
    }
}