Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

graft-source / internal / sync / comments.go
5.6 kB 172 lines
1package sync 2 3import ( 4 "fmt" 5 "strings" 6 7 "graft/internal/radicle" 8 "graft/internal/state" 9) 10 11// Origin values for a "comment" kind activity_log row — where the 12// comment actually happened, shown in the dashboard tooltip only (see 13// internal/status's event.Origin). 14const ( 15 originForgejo = "forgejo" 16 originRadicle = "radicle" 17 originActivityPub = "activitypub" 18 originATProto = "atproto" 19) 20 21// maxCommentSummary bounds how much of a comment's body lands in the 22// dashboard's summary column — comments run much longer than a commit 23// subject line or an issue title. 24const maxCommentSummary = 200 25 26func truncateComment(body string) string { 27 body = strings.TrimSpace(body) 28 r := []rune(body) 29 if len(r) <= maxCommentSummary { 30 return body 31 } 32 return string(r[:maxCommentSummary-1]) + "…" 33} 34 35// syncIssueComments cross-mirrors an issue's comments between Forgejo and 36// Radicle. ri is the already-fetched Radicle issue (its Discussion is 37// used directly — no extra API call), forgejoIndex identifies the same 38// issue on Forgejo. 39func (s *IssueSyncer) syncIssueComments(forgejoIndex int64, ri radicle.Issue) error { 40 fcs, err := s.Forgejo.ListIssueComments(forgejoIndex) 41 if err != nil { 42 return fmt.Errorf("list forgejo comments: %w", err) 43 } 44 45 for _, fc := range fcs { 46 if state.IsMirroredComment(fc.Body) { 47 continue 48 } 49 hash := hashText(fc.Body) 50 seen, err := s.State.CommentSeen(s.RepoPair, "issue", ri.ID, hash) 51 if err != nil { 52 return err 53 } 54 if seen { 55 continue 56 } 57 if err := s.Radicle.CommentIssue(ri.ID, state.MarkMirrored("**via Forgejo:**\n\n"+fc.Body)); err != nil { 58 return fmt.Errorf("mirror comment to radicle: %w", err) 59 } 60 if err := s.State.MarkCommentSeen(s.RepoPair, "issue", ri.ID, hash); err != nil { 61 return err 62 } 63 s.State.LogActivity(state.Activity{ 64 RepoPair: s.RepoPair, Series: s.Series, SeriesURL: s.SeriesURL, 65 Kind: "comment", Direction: state.ForgejoToRadicle, Origin: originForgejo, 66 Summary: truncateComment(fc.Body), 67 URL: s.RadicleWebURL + "/issues/" + ri.ID, 68 ForgejoID: forgejoIndex, RadicleID: ri.ID, 69 }) 70 } 71 72 for _, item := range ri.Comments() { 73 if state.IsMirroredComment(item.Body) { 74 continue 75 } 76 hash := hashText(item.Body) 77 seen, err := s.State.CommentSeen(s.RepoPair, "issue", ri.ID, hash) 78 if err != nil { 79 return err 80 } 81 if seen { 82 continue 83 } 84 if err := s.Forgejo.CreateIssueComment(forgejoIndex, state.MarkMirrored("**via Radicle:**\n\n"+item.Body)); err != nil { 85 return fmt.Errorf("mirror comment to forgejo: %w", err) 86 } 87 if err := s.State.MarkCommentSeen(s.RepoPair, "issue", ri.ID, hash); err != nil { 88 return err 89 } 90 s.State.LogActivity(state.Activity{ 91 RepoPair: s.RepoPair, Series: s.Series, SeriesURL: s.SeriesURL, 92 Kind: "comment", Direction: state.RadicleToForgejo, Origin: originRadicle, 93 Summary: truncateComment(item.Body), 94 URL: fmt.Sprintf("%s/issues/%d", s.ForgejoWebURL, forgejoIndex), 95 ForgejoID: forgejoIndex, RadicleID: ri.ID, 96 }) 97 } 98 return nil 99} 100 101// syncPatchComments is syncIssueComments' patch equivalent. Radicle-side 102// comments come from the patch's first revision (Patch.Comments; see its 103// doc comment — the same single-revision scoping CommentPatch already 104// has) and are NOT independently verified against a live node the way 105// issue comments are (see the Discussion field's doc comment in 106// radicle/client.go). Degrades to silently seeing no Radicle comments 107// if that schema assumption is wrong, rather than erroring. 108func (s *PatchSyncer) syncPatchComments(forgejoIndex int64, rp radicle.Patch) error { 109 if len(rp.Revisions) == 0 { 110 return nil 111 } 112 revisionID := rp.Revisions[0].ID 113 114 fcs, err := s.Forgejo.ListIssueComments(forgejoIndex) 115 if err != nil { 116 return fmt.Errorf("list forgejo comments: %w", err) 117 } 118 for _, fc := range fcs { 119 if state.IsMirroredComment(fc.Body) { 120 continue 121 } 122 hash := hashText(fc.Body) 123 seen, err := s.State.CommentSeen(s.RepoPair, "patch", rp.ID, hash) 124 if err != nil { 125 return err 126 } 127 if seen { 128 continue 129 } 130 if err := s.Radicle.CommentPatch(revisionID, state.MarkMirrored("**via Forgejo:**\n\n"+fc.Body)); err != nil { 131 return fmt.Errorf("mirror comment to radicle: %w", err) 132 } 133 if err := s.State.MarkCommentSeen(s.RepoPair, "patch", rp.ID, hash); err != nil { 134 return err 135 } 136 s.State.LogActivity(state.Activity{ 137 RepoPair: s.RepoPair, Series: s.Series, SeriesURL: s.SeriesURL, 138 Kind: "comment", Direction: state.ForgejoToRadicle, Origin: originForgejo, 139 Summary: truncateComment(fc.Body), 140 URL: s.RadicleWebURL + "/patches/" + rp.ID, 141 ForgejoID: forgejoIndex, RadicleID: rp.ID, 142 }) 143 } 144 145 for _, item := range rp.Comments() { 146 if state.IsMirroredComment(item.Body) { 147 continue 148 } 149 hash := hashText(item.Body) 150 seen, err := s.State.CommentSeen(s.RepoPair, "patch", rp.ID, hash) 151 if err != nil { 152 return err 153 } 154 if seen { 155 continue 156 } 157 if err := s.Forgejo.CreateIssueComment(forgejoIndex, state.MarkMirrored("**via Radicle:**\n\n"+item.Body)); err != nil { 158 return fmt.Errorf("mirror comment to forgejo: %w", err) 159 } 160 if err := s.State.MarkCommentSeen(s.RepoPair, "patch", rp.ID, hash); err != nil { 161 return err 162 } 163 s.State.LogActivity(state.Activity{ 164 RepoPair: s.RepoPair, Series: s.Series, SeriesURL: s.SeriesURL, 165 Kind: "comment", Direction: state.RadicleToForgejo, Origin: originRadicle, 166 Summary: truncateComment(item.Body), 167 URL: fmt.Sprintf("%s/pulls/%d", s.ForgejoWebURL, forgejoIndex), 168 ForgejoID: forgejoIndex, RadicleID: rp.ID, 169 }) 170 } 171 return nil 172}