| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package github |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "regexp" |
| "strings" |
| |
| "golang.org/x/oscar/internal/storage" |
| ) |
| |
| // DO NOT REMOVE/EDIT ANY LITERAL STRINGS OR FORMATTING USED IN THIS FILE, |
| // (unless you have a good reason and a plan to migrate any existing usage). |
| // Go names can be changed. |
| |
| // A Wrapper is used to wrap comments/edits made to GitHub so that |
| // they can later be identified without referencing a database. |
| // This is especially useful for stripping out edits to a user-generated |
| // post made by a bot. |
| // |
| // Wrapped strings ($body) are of the form: |
| // |
| // <!-- Generated by Oscar. DO NOT EDIT. {"bot":"$bot","kind":"$kind","meta":$meta} -->$body<!-- oscar-end --> |
| // |
| // where $bot and $kind are specified in [NewWrapper], and $meta is arbitrary |
| // metadata (if nil, metadata is omitted). |
| // |
| // Note that the wrapping strategy is not robust against a user |
| // intentionally removing or editing the tag(s). |
| type Wrapper struct { |
| bot, kind string |
| } |
| |
| // NewWrapper returns a wrapper for GitHub modifications. |
| // bot is the name of the bot (e.g. "gabyhelp") that is making the edits, |
| // and kind is a context string used to identify the purpose/type |
| // of the edit (e.g "overview" or "related"). |
| func NewWrapper(bot, kind string) *Wrapper { |
| return &Wrapper{ |
| bot: bot, kind: kind, |
| } |
| } |
| |
| // Wrap wraps body (the text of a GitHub modification). |
| // body must not already be wrapped, or contain the end tag (<!-- oscar-end -->). |
| // metadata is freeform metadata to include in the hidden tags. |
| // metadata is sanitized via [json.Marshal], so it may contain otherwise |
| // forbidden elements (e.g., "-->"). |
| func (w *Wrapper) Wrap(body string, metadata any) (string, error) { |
| // Body cannot contain the string [endTag]. |
| if strings.Contains(body, endTag) { |
| return "", fmt.Errorf("github: wrapped body cannot contain %q", endTag) |
| } |
| // DO NOT REMOVE/EDIT STRUCTURE OR CONTENTS. |
| return w.startTag(metadata) + body + endTag, nil |
| } |
| |
| // regexps for checking if a string is wrapped |
| var ( |
| reString = `<!-- Generated by Oscar\. DO NOT EDIT\. (\{.*?\}) -->(?s)(.*?)<!-- oscar-end -->` |
| containsRE *regexp.Regexp = regexp.MustCompile(reString) |
| isRE *regexp.Regexp = regexp.MustCompile(`^` + reString + `$`) |
| ) |
| |
| // IsWrapped reports whether the string is of the form |
| // returned by [Wrapper.Wrap] (for any [Wrapper]). |
| func IsWrapped(s string) bool { |
| return isRE.MatchString(s) |
| } |
| |
| // ContainsWrapped returns whether the given string |
| // contains one or more strings of the form output by |
| // [Wrapper.Wrap] (for any [Wrapper]). |
| func ContainsWrapped(s string) bool { |
| return containsRE.MatchString(s) |
| } |
| |
| // Unwrapped contains the parsed contents of a wrapped string. |
| type Unwrapped struct { |
| TagContent |
| Body string |
| } |
| |
| // TagContent contains the structured contents of a start tag. |
| type TagContent struct { |
| Bot string `json:"bot"` |
| Kind string `json:"kind"` |
| Meta json.RawMessage `json:"meta,omitempty"` |
| } |
| |
| // Parse parses the contents of a wrapped string. |
| // It returns false if the string is malformed. |
| func Parse(s string) (_ *Unwrapped, ok bool) { |
| m := isRE.FindStringSubmatch(s) |
| if len(m) != 3 { |
| return nil, false |
| } |
| // s is m[0] |
| tc, body := m[1], m[2] |
| |
| var tagContent TagContent |
| if err := json.Unmarshal([]byte(tc), &tagContent); err != nil { |
| return nil, false |
| } |
| |
| return &Unwrapped{ |
| TagContent: tagContent, |
| Body: body, |
| }, true |
| } |
| |
| // ParseAll parses the contents of all wrapped substrings in the |
| // string. It returns false if there are no wrapped substrings, |
| // or any of them are malformed. |
| func ParseAll(s string) (_ []*Unwrapped, ok bool) { |
| var us []*Unwrapped |
| matches := containsRE.FindAllString(s, -1) |
| if len(matches) == 0 { |
| return nil, false |
| } |
| for _, substr := range matches { |
| u, ok := Parse(substr) |
| if !ok { |
| return nil, false |
| } |
| us = append(us, u) |
| } |
| return us, true |
| } |
| |
| // Strip removes all wrapped substrings of the string |
| // and returns the result. |
| func Strip(s string) string { |
| return containsRE.ReplaceAllString(s, "") |
| } |
| |
| // startTag returns <!-- Generated by Oscar. DO NOT EDIT. {"bot":"$bot","kind":"$kind","meta":$metadata-json} --> |
| // where $metadata-json is the JSON representaion of the given metadata. |
| func (w *Wrapper) startTag(metadata any) string { |
| // DO NOT REMOVE/EDIT this function body. |
| var js json.RawMessage |
| if metadata != nil { |
| js = storage.JSON(metadata) |
| } |
| return fmt.Sprintf("<!-- Generated by Oscar. DO NOT EDIT. %s -->", |
| storage.JSON(TagContent{ |
| Bot: w.bot, |
| Kind: w.kind, |
| Meta: js, |
| })) |
| } |
| |
| // DO NOT REMOVE/EDIT. |
| const endTag = "<!-- oscar-end -->" |