this repo has no description
0
fork

Configure Feed

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

syntax: ATURI type

+319
+103
atproto/syntax/aturi.go
··· 1 + package syntax 2 + 3 + import ( 4 + "fmt" 5 + "regexp" 6 + "strings" 7 + ) 8 + 9 + var aturiRegex = regexp.MustCompile(`^at:\/\/(?P<authority>[a-zA-Z0-9._:%-]+)(\/(?P<collection>[a-zA-Z0-9-.]+)(\/(?P<rkey>[a-zA-Z0-9_~.-]{1,512}))?)?$`) 10 + 11 + // String type which represents a syntaxtually valid AT URI, as would pass Lexicon syntax validation for the 'at-uri' field (no query or fragment parts) 12 + // 13 + // Always use [ParseATURI] instead of wrapping strings directly, especially when working with input. 14 + // 15 + // Syntax specification: https://atproto.com/specs/at-uri-scheme 16 + type ATURI string 17 + 18 + func ParseATURI(raw string) (ATURI, error) { 19 + if len(raw) > 8192 { 20 + return "", fmt.Errorf("ATURI is too long (8192 chars max)") 21 + } 22 + parts := aturiRegex.FindStringSubmatch(raw) 23 + if parts == nil || len(parts) < 2 || parts[0] == "" { 24 + return "", fmt.Errorf("AT-URI syntax didn't validate via regex") 25 + } 26 + // verify authority as either a DID or NSID 27 + _, err := ParseAtIdentifier(parts[1]) 28 + if err != nil { 29 + return "", fmt.Errorf("AT-URI authority section neither a DID nor Handle: %s", parts[1]) 30 + } 31 + if len(parts) >= 4 && parts[3] != "" { 32 + _, err := ParseNSID(parts[3]) 33 + if err != nil { 34 + return "", fmt.Errorf("AT-URI first path segment not an NSID: %s", parts[3]) 35 + } 36 + } 37 + if len(parts) >= 6 && parts[5] != "" { 38 + _, err := ParseRecordKey(parts[5]) 39 + if err != nil { 40 + return "", fmt.Errorf("AT-URI second path segment not a RecordKey: %s", parts[5]) 41 + } 42 + } 43 + return ATURI(raw), nil 44 + } 45 + 46 + func (n ATURI) Authority() (*AtIdentifier, error) { 47 + parts := strings.SplitN(string(n), "/", 4) 48 + if len(parts) < 3 { 49 + // something has gone wrong (would not validate) 50 + return nil, fmt.Errorf("AT-URI has no authority segment (invalid)") 51 + } 52 + return ParseAtIdentifier(parts[2]) 53 + } 54 + 55 + // Returns path segment, without leading slash, as would be used in an atproto repository key 56 + func (n ATURI) Path() string { 57 + parts := strings.SplitN(string(n), "/", 5) 58 + if len(parts) < 3 { 59 + // something has gone wrong (would not validate) 60 + return "" 61 + } 62 + if len(parts) == 3 { 63 + return parts[2] 64 + } 65 + return parts[2] + "/" + parts[3] 66 + } 67 + 68 + func (n ATURI) Collection() (NSID, error) { 69 + parts := strings.SplitN(string(n), "/", 5) 70 + if len(parts) < 4 { 71 + // something has gone wrong (would not validate) 72 + return NSID(""), fmt.Errorf("AT-URI has no collection segment") 73 + } 74 + // re-parsing is safest 75 + return ParseNSID(parts[3]) 76 + } 77 + 78 + func (n ATURI) RecordKey() (RecordKey, error) { 79 + parts := strings.SplitN(string(n), "/", 6) 80 + if len(parts) < 5 { 81 + // something has gone wrong (would not validate) 82 + return RecordKey(""), fmt.Errorf("AT-URI has no record key segment") 83 + } 84 + // re-parsing is safest 85 + return ParseRecordKey(parts[4]) 86 + } 87 + 88 + func (n ATURI) Normalize() ATURI { 89 + auth, err := n.Authority() 90 + if err != nil { 91 + // invalid AT-URI 92 + return n 93 + } 94 + coll, err := n.Collection() 95 + if err != nil { 96 + return ATURI("at://" + auth.Normalize().String()) 97 + } 98 + rkey, err := n.RecordKey() 99 + if err != nil { 100 + return ATURI("at://" + auth.Normalize().String() + "/" + coll.String()) 101 + } 102 + return ATURI("at://" + auth.Normalize().String() + "/" + coll.Normalize().String() + "/" + rkey.String()) 103 + }
+101
atproto/syntax/aturi_test.go
··· 1 + package syntax 2 + 3 + import ( 4 + "bufio" 5 + "fmt" 6 + "os" 7 + "testing" 8 + 9 + "github.com/stretchr/testify/assert" 10 + ) 11 + 12 + func TestInteropATURIsValid(t *testing.T) { 13 + assert := assert.New(t) 14 + file, err := os.Open("testdata/aturi_syntax_valid.txt") 15 + assert.NoError(err) 16 + defer file.Close() 17 + scanner := bufio.NewScanner(file) 18 + for scanner.Scan() { 19 + line := scanner.Text() 20 + if len(line) == 0 || line[0] == '#' { 21 + continue 22 + } 23 + _, err := ParseATURI(line) 24 + if err != nil { 25 + fmt.Println("FAILED, GOOD: " + line) 26 + } 27 + assert.NoError(err) 28 + } 29 + assert.NoError(scanner.Err()) 30 + } 31 + 32 + func TestInteropATURIsInvalid(t *testing.T) { 33 + assert := assert.New(t) 34 + file, err := os.Open("testdata/aturi_syntax_invalid.txt") 35 + assert.NoError(err) 36 + defer file.Close() 37 + scanner := bufio.NewScanner(file) 38 + for scanner.Scan() { 39 + line := scanner.Text() 40 + if len(line) == 0 || line[0] == '#' { 41 + continue 42 + } 43 + _, err := ParseATURI(line) 44 + if err == nil { 45 + fmt.Println("FAILED, BAD: " + line) 46 + } 47 + assert.Error(err) 48 + } 49 + assert.NoError(scanner.Err()) 50 + } 51 + 52 + func TestATURIParts(t *testing.T) { 53 + assert := assert.New(t) 54 + 55 + testVec := [][]string{ 56 + {"at://did:abc:123/io.nsid.someFunc/record-key", "did:abc:123", "io.nsid.someFunc", "record-key"}, 57 + {"at://e.com", "e.com", "", ""}, 58 + } 59 + 60 + for _, parts := range testVec { 61 + uri, err := ParseATURI(parts[0]) 62 + assert.NoError(err) 63 + auth, _ := uri.Authority() 64 + assert.Equal(parts[1], auth.String()) 65 + col, _ := uri.Collection() 66 + assert.Equal(parts[2], col.String()) 67 + rkey, _ := uri.RecordKey() 68 + assert.NoError(err) 69 + assert.Equal(parts[3], rkey.String()) 70 + } 71 + 72 + } 73 + 74 + func TestATURINormalize(t *testing.T) { 75 + assert := assert.New(t) 76 + 77 + testVec := [][]string{ 78 + {"at://did:abc:123/io.NsId.someFunc/record-KEY", "at://did:abc:123/io.nsid.someFunc/record-KEY"}, 79 + // XXX: { "at://E.com", "at://e.com" }, 80 + } 81 + 82 + for _, parts := range testVec { 83 + uri, err := ParseATURI(parts[0]) 84 + assert.NoError(err) 85 + assert.Equal(parts[1], uri.Normalize().String()) 86 + } 87 + } 88 + 89 + func TestATURINoPanic(t *testing.T) { 90 + for _, s := range []string{"", ".", "at://", "at:///", "at://e.com", "at://e.com/", "at://e.com//"} { 91 + bad := ATURI(s) 92 + _, _ = bad.Authority() 93 + _, _ = bad.Collection() 94 + _, _ = bad.RecordKey() 95 + _ = bad.Normalize() 96 + } 97 + } 98 + 99 + func (u ATURI) String() string { 100 + return string(u) 101 + }
+89
atproto/syntax/testdata/aturi_syntax_invalid.txt
··· 1 + 2 + # enforces spec basics 3 + a://did:plc:asdf123 4 + at//did:plc:asdf123 5 + at:/a/did:plc:asdf123 6 + at:/did:plc:asdf123 7 + AT://did:plc:asdf123 8 + http://did:plc:asdf123 9 + ://did:plc:asdf123 10 + at:did:plc:asdf123 11 + at:/did:plc:asdf123 12 + at:///did:plc:asdf123 13 + at://:/did:plc:asdf123 14 + at:/ /did:plc:asdf123 15 + at://did:plc:asdf123 16 + at://did:plc:asdf123/ 17 + at://did:plc:asdf123 18 + at://did:plc:asdf123/com.atproto.feed.post 19 + at://did:plc:asdf123/com.atproto.feed.post# 20 + at://did:plc:asdf123/com.atproto.feed.post#/ 21 + at://did:plc:asdf123/com.atproto.feed.post#/frag 22 + at://did:plc:asdf123/com.atproto.feed.post#fr ag 23 + //did:plc:asdf123 24 + at://name 25 + at://name.0 26 + at://diD:plc:asdf123 27 + at://did:plc:asdf123/com.atproto.feed.p@st 28 + at://did:plc:asdf123/com.atproto.feed.p$st 29 + at://did:plc:asdf123/com.atproto.feed.p%st 30 + at://did:plc:asdf123/com.atproto.feed.p&st 31 + at://did:plc:asdf123/com.atproto.feed.p()t 32 + at://did:plc:asdf123/com.atproto.feed_post 33 + at://did:plc:asdf123/-com.atproto.feed.post 34 + at://did:plc:asdf@123/com.atproto.feed.post 35 + at://DID:plc:asdf123 36 + at://user.bsky.123 37 + at://bsky 38 + at://did:plc: 39 + at://did:plc: 40 + at://frag 41 + 42 + # too long: 'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(8200) 43 + at://did:plc:asdf123/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 44 + 45 + # has specified behavior on edge cases 46 + at://user.bsky.social// 47 + at://user.bsky.social//com.atproto.feed.post 48 + at://user.bsky.social/com.atproto.feed.post// 49 + at://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more', 50 + at://did:plc:asdf123/short/stuff 51 + at://did:plc:asdf123/12345 52 + 53 + # enforces no trailing slashes 54 + at://did:plc:asdf123/ 55 + at://user.bsky.social/ 56 + at://did:plc:asdf123/com.atproto.feed.post/ 57 + at://did:plc:asdf123/com.atproto.feed.post/record/ 58 + at://did:plc:asdf123/com.atproto.feed.post/record/#/frag 59 + 60 + # enforces strict paths 61 + at://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf 62 + 63 + # is very permissive about fragments 64 + at://did:plc:asdf123# 65 + at://did:plc:asdf123## 66 + #at://did:plc:asdf123 67 + at://did:plc:asdf123#/asdf#/asdf 68 + 69 + # new less permissive about record keys for Lexicon use (with recordkey more specified) 70 + at://did:plc:asdf123/com.atproto.feed.post/%23 71 + at://did:plc:asdf123/com.atproto.feed.post/$@!*)(:,;~.sdf123 72 + at://did:plc:asdf123/com.atproto.feed.post/~'sdf123") 73 + at://did:plc:asdf123/com.atproto.feed.post/$ 74 + at://did:plc:asdf123/com.atproto.feed.post/@ 75 + at://did:plc:asdf123/com.atproto.feed.post/! 76 + at://did:plc:asdf123/com.atproto.feed.post/* 77 + at://did:plc:asdf123/com.atproto.feed.post/( 78 + at://did:plc:asdf123/com.atproto.feed.post/, 79 + at://did:plc:asdf123/com.atproto.feed.post/; 80 + at://did:plc:asdf123/com.atproto.feed.post/abc%30123 81 + at://did:plc:asdf123/com.atproto.feed.post/%30 82 + at://did:plc:asdf123/com.atproto.feed.post/%3 83 + at://did:plc:asdf123/com.atproto.feed.post/% 84 + at://did:plc:asdf123/com.atproto.feed.post/%zz 85 + at://did:plc:asdf123/com.atproto.feed.post/%%% 86 + 87 + # disallow dot / double-dot 88 + at://did:plc:asdf123/com.atproto.feed.post/. 89 + at://did:plc:asdf123/com.atproto.feed.post/..
+26
atproto/syntax/testdata/aturi_syntax_valid.txt
··· 1 + 2 + # enforces spec basics 3 + at://did:plc:asdf123 4 + at://user.bsky.social 5 + at://did:plc:asdf123/com.atproto.feed.post 6 + at://did:plc:asdf123/com.atproto.feed.post/record 7 + 8 + # very long: 'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(512) 9 + at://did:plc:asdf123/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 10 + 11 + # enforces no trailing slashes 12 + at://did:plc:asdf123 13 + at://user.bsky.social 14 + at://did:plc:asdf123/com.atproto.feed.post 15 + at://did:plc:asdf123/com.atproto.feed.post/record 16 + 17 + # enforces strict paths 18 + at://did:plc:asdf123/com.atproto.feed.post/asdf123 19 + 20 + # is very permissive about record keys 21 + at://did:plc:asdf123/com.atproto.feed.post/asdf123 22 + at://did:plc:asdf123/com.atproto.feed.post/a 23 + 24 + at://did:plc:asdf123/com.atproto.feed.post/asdf-123 25 + at://did:abc:123 26 + at://did:abc:123/io.nsid.someFunc/record-key