add reviewedBy field

This commit is contained in:
Michael Black
2026-05-27 12:44:53 -05:00
parent 6c0b93b527
commit a99f8bf3d6
4 changed files with 345 additions and 15 deletions
+66
View File
@@ -32,6 +32,17 @@ type personOrOrg struct {
Image string `json:"image,omitempty"`
InteractionStat []interactionStat `json:"interactionStatistic,omitempty"`
AgentInteractionStat []interactionStat `json:"agentInteractionStatistic,omitempty"`
ReviewedBy []*verifier `json:"reviewedBy,omitempty"`
}
// verifier is the schema.org Person shape emitted under Person.reviewedBy.
// Narrower than personOrOrg: just enough to identify the verifying entity.
type verifier struct {
Type string `json:"@type"`
Name string `json:"name,omitempty"`
AlternateName string `json:"alternateName,omitempty"`
Identifier string `json:"identifier,omitempty"`
URL string `json:"url,omitempty"`
}
type sharedContent struct {
@@ -91,6 +102,9 @@ const maxComments = 10
// maxRecentPosts caps ProfilePage.hasPart[].
const maxRecentPosts = 10
// maxReviewedBy caps Person.reviewedBy entries.
const maxReviewedBy = 10
// authorFeedFetchLimit oversamples getAuthorFeed because posts_no_replies
// still returns reposts; we drop those client-side. 3x is a safe margin.
const authorFeedFetchLimit = 3 * maxRecentPosts
@@ -226,6 +240,48 @@ func buildAuthor(author *appbsky.ActorDefs_ProfileViewBasic) *personOrOrg {
return p
}
// buildReviewedBy maps a profile's verifications to Person entries for
// Person.reviewedBy. Skips !IsValid and missing Issuer; caps at
// maxReviewedBy. URL falls back to DID-form when the verifier's handle is
// missing or "handle.invalid".
func buildReviewedBy(state *appbsky.ActorDefs_VerificationState) []*verifier {
if state == nil || len(state.Verifications) == 0 {
return nil
}
var out []*verifier
for _, v := range state.Verifications {
if v == nil || !v.IsValid || v.Issuer == "" {
continue
}
if len(out) >= maxReviewedBy {
break
}
entry := &verifier{
Type: "Person",
Identifier: v.Issuer,
}
var handle string
if v.Handle != nil {
handle = *v.Handle
}
if v.DisplayName != nil && *v.DisplayName != "" {
entry.Name = *v.DisplayName
if handle != "" && handle != "handle.invalid" {
entry.AlternateName = "@" + handle
}
} else if handle != "" && handle != "handle.invalid" {
entry.Name = "@" + handle
}
if url := bskyProfileURL(handle); url != "" {
entry.URL = url
} else {
entry.URL = "https://bsky.app/profile/" + v.Issuer
}
out = append(out, entry)
}
return out
}
// postHasHideLabel reports whether the post-view or self-labels include
// any non-negated label in labelSet. Self-labels have no negation.
func postHasHideLabel(pv *appbsky.FeedDefs_PostView, labelSet map[string]bool) bool {
@@ -319,6 +375,13 @@ func buildPostNode(pv *appbsky.FeedDefs_PostView, replies []*appbsky.FeedDefs_Th
InteractionStat: buildPostStats(pv),
}
// Surface verifications on the post author only; replies do not.
if node.Author != nil {
if rb := buildReviewedBy(pv.Author.Verification); len(rb) > 0 {
node.Author.ReviewedBy = rb
}
}
if pv.ReplyCount != nil {
node.CommentCount = pv.ReplyCount
} else {
@@ -448,6 +511,9 @@ func buildProfileJSONLD(pv *appbsky.ActorDefs_ProfileViewDetailed, recentPosts [
{Type: "InteractionCounter", InteractionType: "https://schema.org/FollowAction", UserInteractionCount: deref(pv.FollowsCount)},
{Type: "InteractionCounter", InteractionType: "https://schema.org/WriteAction", UserInteractionCount: deref(pv.PostsCount)},
}
if rb := buildReviewedBy(pv.Verification); len(rb) > 0 {
person.ReviewedBy = rb
}
page := profilePage{
Context: schemaOrgContext,
+273
View File
@@ -154,6 +154,48 @@ func withPostLabel(val string, neg bool) func(*appbsky.FeedDefs_PostView) {
}
}
// verifierSpec is a compact specifier for a verification entry in fixtures.
type verifierSpec struct {
issuer, handle, displayName string
isValid bool
}
// makeVerificationState builds a VerificationState from the supplied specs.
func makeVerificationState(specs ...verifierSpec) *appbsky.ActorDefs_VerificationState {
state := &appbsky.ActorDefs_VerificationState{
VerifiedStatus: "valid",
TrustedVerifierStatus: "none",
}
for _, s := range specs {
v := &appbsky.ActorDefs_VerificationView{
Issuer: s.issuer,
IsValid: s.isValid,
CreatedAt: "2024-01-01T00:00:00Z",
Uri: "at://" + s.issuer + "/app.bsky.graph.verification/" + s.issuer,
}
if s.handle != "" {
h := s.handle
v.Handle = &h
}
if s.displayName != "" {
d := s.displayName
v.DisplayName = &d
}
state.Verifications = append(state.Verifications, v)
}
return state
}
// withVerifications sets pv.Author.Verification.
func withVerifications(state *appbsky.ActorDefs_VerificationState) func(*appbsky.FeedDefs_PostView) {
return func(pv *appbsky.FeedDefs_PostView) {
if pv.Author == nil {
return
}
pv.Author.Verification = state
}
}
// unmarshalLD parses the JSON-LD blob produced by buildPostJSONLD.
func unmarshalLD(t *testing.T, s string) map[string]any {
t.Helper()
@@ -776,3 +818,234 @@ func TestBuildPostJSONLD_ReplyAuthorHasIdentifier(t *testing.T) {
t.Errorf("reply author identifier should be DID, got %v", auth["identifier"])
}
}
func TestBuildReviewedBy_NilAndEmpty(t *testing.T) {
if got := buildReviewedBy(nil); got != nil {
t.Errorf("nil state should yield nil, got %v", got)
}
empty := &appbsky.ActorDefs_VerificationState{}
if got := buildReviewedBy(empty); got != nil {
t.Errorf("empty Verifications should yield nil, got %v", got)
}
allInvalid := makeVerificationState(
verifierSpec{issuer: "did:plc:v1", handle: "v1.example.com", displayName: "V One", isValid: false},
verifierSpec{issuer: "did:plc:v2", handle: "v2.example.com", displayName: "V Two", isValid: false},
)
if got := buildReviewedBy(allInvalid); got != nil {
t.Errorf("all-invalid state should yield nil, got %v", got)
}
}
func TestBuildReviewedBy_FiltersInvalid(t *testing.T) {
state := makeVerificationState(
verifierSpec{issuer: "did:plc:v1", handle: "v1.example.com", displayName: "V One", isValid: true},
verifierSpec{issuer: "did:plc:v2", handle: "v2.example.com", displayName: "V Two", isValid: false},
verifierSpec{issuer: "did:plc:v3", handle: "v3.example.com", displayName: "V Three", isValid: true},
verifierSpec{issuer: "", handle: "noid.example.com", displayName: "No Issuer", isValid: true},
)
got := buildReviewedBy(state)
if len(got) != 2 {
t.Fatalf("expected 2 valid verifiers, got %d: %v", len(got), got)
}
if got[0].Identifier != "did:plc:v1" {
t.Errorf("FIFO order broken; first identifier = %q", got[0].Identifier)
}
if got[1].Identifier != "did:plc:v3" {
t.Errorf("expected v3 at index 1, got %q", got[1].Identifier)
}
}
func TestBuildReviewedBy_NameFallbacks(t *testing.T) {
cases := []struct {
name string
spec verifierSpec
wantName, wantAlternateName, wantURL, wantIdentif string
}{
{
name: "DisplayName + handle",
spec: verifierSpec{issuer: "did:plc:v1", handle: "alice.example.com", displayName: "Alice Verifier", isValid: true},
wantName: "Alice Verifier",
wantAlternateName: "@alice.example.com",
wantURL: "https://bsky.app/profile/alice.example.com",
wantIdentif: "did:plc:v1",
},
{
name: "handle only",
spec: verifierSpec{issuer: "did:plc:v2", handle: "bob.example.com", isValid: true},
wantName: "@bob.example.com",
wantAlternateName: "",
wantURL: "https://bsky.app/profile/bob.example.com",
wantIdentif: "did:plc:v2",
},
{
name: "DisplayName only (no handle)",
spec: verifierSpec{issuer: "did:plc:v3", displayName: "Carol Verifier", isValid: true},
wantName: "Carol Verifier",
wantAlternateName: "",
wantURL: "https://bsky.app/profile/did:plc:v3",
wantIdentif: "did:plc:v3",
},
{
name: "DisplayName + handle.invalid",
spec: verifierSpec{issuer: "did:plc:v4", handle: "handle.invalid", displayName: "Dave Verifier", isValid: true},
wantName: "Dave Verifier",
wantAlternateName: "",
wantURL: "https://bsky.app/profile/did:plc:v4",
wantIdentif: "did:plc:v4",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := buildReviewedBy(makeVerificationState(tc.spec))
if len(got) != 1 {
t.Fatalf("expected 1 entry, got %d", len(got))
}
v := got[0]
if v.Type != "Person" {
t.Errorf("@type = %q, want Person", v.Type)
}
if v.Name != tc.wantName {
t.Errorf("name = %q, want %q", v.Name, tc.wantName)
}
if v.AlternateName != tc.wantAlternateName {
t.Errorf("alternateName = %q, want %q", v.AlternateName, tc.wantAlternateName)
}
if v.URL != tc.wantURL {
t.Errorf("url = %q, want %q", v.URL, tc.wantURL)
}
if v.Identifier != tc.wantIdentif {
t.Errorf("identifier = %q, want %q", v.Identifier, tc.wantIdentif)
}
})
}
}
func TestBuildReviewedBy_Cap(t *testing.T) {
specs := make([]verifierSpec, 0, 12)
for i := 0; i < 12; i++ {
specs = append(specs, verifierSpec{
issuer: fmt.Sprintf("did:plc:v%02d", i),
handle: fmt.Sprintf("v%02d.example.com", i),
displayName: fmt.Sprintf("V%02d", i),
isValid: true,
})
}
got := buildReviewedBy(makeVerificationState(specs...))
if len(got) != maxReviewedBy {
t.Errorf("expected cap at %d, got %d", maxReviewedBy, len(got))
}
if got[0].Identifier != "did:plc:v00" {
t.Errorf("first kept entry should be v00, got %q", got[0].Identifier)
}
}
func TestBuildPostJSONLD_AuthorReviewedBy(t *testing.T) {
state := makeVerificationState(verifierSpec{
issuer: "did:plc:verifier1",
handle: "verifier.example.com",
displayName: "Trusted Verifier",
isValid: true,
})
pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "hi",
withVerifications(state))
out, err := buildPostJSONLD(pv, nil, "u", hideEmbedLabels, hideReplyLabels)
if err != nil {
t.Fatal(err)
}
main := unmarshalLD(t, out)["mainEntity"].(map[string]any)
auth := main["author"].(map[string]any)
rb, ok := auth["reviewedBy"].([]any)
if !ok {
t.Fatalf("post author should have reviewedBy, got %v", auth["reviewedBy"])
}
if len(rb) != 1 {
t.Fatalf("expected 1 verifier, got %d", len(rb))
}
v := rb[0].(map[string]any)
if v["@type"] != "Person" {
t.Errorf("verifier @type = %v, want Person", v["@type"])
}
if v["name"] != "Trusted Verifier" {
t.Errorf("verifier name = %v", v["name"])
}
if v["identifier"] != "did:plc:verifier1" {
t.Errorf("verifier identifier = %v", v["identifier"])
}
if v["url"] != "https://bsky.app/profile/verifier.example.com" {
t.Errorf("verifier url = %v", v["url"])
}
}
func TestBuildPostJSONLD_ReplyAuthorNoReviewedBy(t *testing.T) {
// Replies do not surface verifications even when the reply author
// carries Verification.
state := makeVerificationState(verifierSpec{
issuer: "did:plc:verifier1",
handle: "verifier.example.com",
displayName: "Trusted Verifier",
isValid: true,
})
pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "main")
reply := makePostView("bob.bsky.social", "did:plc:bob", "rep1", "hi",
withVerifications(state))
out, _ := buildPostJSONLD(pv, buildReplies(reply), "u", hideEmbedLabels, hideReplyLabels)
main := unmarshalLD(t, out)["mainEntity"].(map[string]any)
c := main["comment"].([]any)[0].(map[string]any)
auth := c["author"].(map[string]any)
if _, present := auth["reviewedBy"]; present {
t.Errorf("reply author must not carry reviewedBy, got %v", auth["reviewedBy"])
}
}
func TestBuildProfileJSONLD_MainEntityReviewedBy(t *testing.T) {
pv := newProfileViewDetailed()
pv.Verification = makeVerificationState(verifierSpec{
issuer: "did:plc:verifier1",
handle: "verifier.example.com",
displayName: "Trusted Verifier",
isValid: true,
})
out, err := buildProfileJSONLD(pv, nil, hideEmbedLabels, hideReplyLabels)
if err != nil {
t.Fatal(err)
}
main := unmarshalLD(t, out)["mainEntity"].(map[string]any)
rb, ok := main["reviewedBy"].([]any)
if !ok {
t.Fatalf("profile mainEntity should have reviewedBy, got %v", main["reviewedBy"])
}
v := rb[0].(map[string]any)
if v["identifier"] != "did:plc:verifier1" {
t.Errorf("verifier identifier = %v", v["identifier"])
}
if v["url"] != "https://bsky.app/profile/verifier.example.com" {
t.Errorf("verifier url = %v", v["url"])
}
}
func TestBuildProfileJSONLD_HasPartAuthorReviewedBy(t *testing.T) {
// Recent posts inherit verifications via their post-view Author.
pv := newProfileViewDetailed()
state := makeVerificationState(verifierSpec{
issuer: "did:plc:verifier1",
handle: "verifier.example.com",
displayName: "Trusted Verifier",
isValid: true,
})
post := makePostView("alice.bsky.social", "did:plc:alice", "rp1", "hi",
withVerifications(state))
out, _ := buildProfileJSONLD(pv, []*appbsky.FeedDefs_PostView{post}, hideEmbedLabels, hideReplyLabels)
page := unmarshalLD(t, out)
hp := page["hasPart"].([]any)
if len(hp) != 1 {
t.Fatalf("expected 1 hasPart entry, got %d", len(hp))
}
auth := hp[0].(map[string]any)["author"].(map[string]any)
rb, ok := auth["reviewedBy"].([]any)
if !ok || len(rb) != 1 {
t.Fatalf("hasPart author should carry reviewedBy, got %v", auth["reviewedBy"])
}
if rb[0].(map[string]any)["identifier"] != "did:plc:verifier1" {
t.Errorf("verifier identifier wrong: %v", rb[0])
}
}
+2 -5
View File
@@ -3,7 +3,7 @@ module github.com/bluesky-social/social-app/bskyweb
go 1.26
require (
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a
github.com/bluesky-social/indigo v0.0.0-20260527160159-3b7634c713b8
github.com/flosch/pongo2/v6 v6.0.0
github.com/ipfs/go-log v1.0.5
github.com/joho/godotenv v1.5.1
@@ -18,11 +18,10 @@ require github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indir
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -32,7 +31,6 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/arc/v2 v2.0.6 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-block-format v0.2.0 // indirect
@@ -82,7 +80,6 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
+4 -10
View File
@@ -2,12 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a h1:S12KN45uIkRglMHC8PqD/Vsz0+u3KbIaBF/6rit8/Pg=
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
github.com/bluesky-social/indigo v0.0.0-20260527160159-3b7634c713b8 h1:heYuihJZQZMNij7+dKIEfApG10xrGnDV2rtKpvFyYYU=
github.com/bluesky-social/indigo v0.0.0-20260527160159-3b7634c713b8/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -19,6 +15,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg=
github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
@@ -48,8 +46,6 @@ github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/arc/v2 v2.0.6 h1:4NU7uP5vSoK6TbaMj3NtY478TTAWLso/vL1gpNrInHg=
github.com/hashicorp/golang-lru/arc/v2 v2.0.6/go.mod h1:cfdDIX05DWvYV6/shsxDfa/OVcRieOt+q4FnM8x+Xno=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
@@ -211,8 +207,6 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSD
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4=
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 h1:yJ9/LwIGIk/c0CdoavpC9RNSGSruIspSZtxG3Nnldic=
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6/go.mod h1:39U9RRVr4CKbXpXYopWn+FSH5s+vWu6+RmguSPWAq5s=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=