Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

feat(handler): add SubmitSteps and GetSteps HTTP endpoints

+109
+2
cmd/server/main.go
··· 114 114 mux.HandleFunc("GET /docs/{rkey}/accept", h.AcceptInvite) 115 115 mux.HandleFunc("POST /api/docs/{rkey}/comments", h.CommentCreate) 116 116 mux.HandleFunc("GET /api/docs/{rkey}/comments", h.CommentList) 117 + mux.HandleFunc("POST /api/docs/{rkey}/steps", h.SubmitSteps) 118 + mux.HandleFunc("GET /api/docs/{rkey}/steps", h.GetSteps) 117 119 118 120 // WebSocket 119 121 mux.HandleFunc("GET /ws/docs/{rkey}", h.CollaboratorWebSocket)
+107
internal/handler/handler.go
··· 941 941 } 942 942 return colors[hash%len(colors)] 943 943 } 944 + 945 + // SubmitSteps receives ProseMirror steps from a collaborator, appends them 946 + // to the step log, and broadcasts confirmed steps to the room. 947 + // 948 + // POST /api/docs/{rkey}/steps 949 + // Body: {"clientVersion": N, "steps": ["...json..."], "clientID": "did:..."} 950 + // Response 200: {"version": N} 951 + // Response 409: {"version": N, "steps": ["...json..."]} — client must rebase 952 + func (h *Handler) SubmitSteps(w http.ResponseWriter, r *http.Request) { 953 + user := h.currentUser(r) 954 + if user == nil { 955 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 956 + return 957 + } 958 + rkey := r.PathValue("rkey") 959 + 960 + var body struct { 961 + ClientVersion int `json:"clientVersion"` 962 + Steps []string `json:"steps"` 963 + ClientID string `json:"clientID"` 964 + } 965 + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 966 + http.Error(w, "Bad request", http.StatusBadRequest) 967 + return 968 + } 969 + if len(body.Steps) == 0 { 970 + http.Error(w, "No steps", http.StatusBadRequest) 971 + return 972 + } 973 + 974 + newVersion, err := h.DB.AppendSteps(rkey, body.ClientVersion, body.Steps, body.ClientID) 975 + if err != nil { 976 + // Version conflict — return steps the client missed. 977 + missed, dbErr := h.DB.GetStepsSince(rkey, body.ClientVersion) 978 + if dbErr != nil { 979 + log.Printf("SubmitSteps: GetStepsSince: %v", dbErr) 980 + http.Error(w, "Internal error", http.StatusInternalServerError) 981 + return 982 + } 983 + currentVersion, _ := h.DB.GetDocVersion(rkey) 984 + stepJSONs := make([]string, len(missed)) 985 + for i, s := range missed { 986 + stepJSONs[i] = s.JSON 987 + } 988 + w.Header().Set("Content-Type", "application/json") 989 + w.WriteHeader(http.StatusConflict) 990 + json.NewEncoder(w).Encode(map[string]interface{}{ 991 + "version": currentVersion, 992 + "steps": stepJSONs, 993 + }) 994 + return 995 + } 996 + 997 + // Broadcast to other room members via WebSocket. 998 + if room := h.CollaborationHub.GetRoom(rkey); room != nil { 999 + type stepsMsg struct { 1000 + Type string `json:"type"` 1001 + Steps []string `json:"steps"` 1002 + Version int `json:"version"` 1003 + ClientID string `json:"clientID"` 1004 + } 1005 + data, _ := json.Marshal(stepsMsg{ 1006 + Type: "steps", 1007 + Steps: body.Steps, 1008 + Version: newVersion, 1009 + ClientID: body.ClientID, 1010 + }) 1011 + room.Broadcast(data) 1012 + } 1013 + 1014 + h.jsonResponse(w, map[string]int{"version": newVersion}, http.StatusOK) 1015 + } 1016 + 1017 + // GetSteps returns all steps since the given version. 1018 + // 1019 + // GET /api/docs/{rkey}/steps?since={v} 1020 + // Response 200: {"version": N, "steps": ["...json..."]} 1021 + func (h *Handler) GetSteps(w http.ResponseWriter, r *http.Request) { 1022 + user := h.currentUser(r) 1023 + if user == nil { 1024 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 1025 + return 1026 + } 1027 + rkey := r.PathValue("rkey") 1028 + 1029 + sinceStr := r.URL.Query().Get("since") 1030 + var since int 1031 + if sinceStr != "" { 1032 + fmt.Sscanf(sinceStr, "%d", &since) 1033 + } 1034 + 1035 + rows, err := h.DB.GetStepsSince(rkey, since) 1036 + if err != nil { 1037 + log.Printf("GetSteps: %v", err) 1038 + http.Error(w, "Internal error", http.StatusInternalServerError) 1039 + return 1040 + } 1041 + version, _ := h.DB.GetDocVersion(rkey) 1042 + stepJSONs := make([]string, len(rows)) 1043 + for i, s := range rows { 1044 + stepJSONs[i] = s.JSON 1045 + } 1046 + h.jsonResponse(w, map[string]interface{}{ 1047 + "version": version, 1048 + "steps": stepJSONs, 1049 + }, http.StatusOK) 1050 + }