blob: 334e64a3e4b4e2eb92a2544b9479de7fd4698d11 [file] [log] [blame]
// 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 -->"