this repo has no description
0
fork

Configure Feed

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

refactor query string parsing to use params struct

+212 -81
+76 -38
search/parse_query.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "fmt" 6 5 "log/slog" 7 6 "strings" 8 7 ··· 11 10 ) 12 11 13 12 // ParseQuery takes a query string and pulls out some facet patterns ("from:handle.net") as filters 14 - func ParseQuery(ctx context.Context, dir identity.Directory, raw string) (string, []map[string]interface{}) { 15 - var filters []map[string]interface{} 13 + func ParsePostQuery(ctx context.Context, dir identity.Directory, raw string) PostSearchParams { 16 14 quoted := false 17 15 parts := strings.FieldsFunc(raw, func(r rune) bool { 18 16 if r == '"' { ··· 21 19 return r == ' ' && !quoted 22 20 }) 23 21 22 + params := PostSearchParams{} 23 + 24 24 keep := make([]string, 0, len(parts)) 25 25 for _, p := range parts { 26 - p = strings.Trim(p, "\"") 26 + // pass-through quoted, either phrase or single token 27 + if strings.HasPrefix(p, "\"") { 28 + keep = append(keep, p) 29 + continue 30 + } 27 31 32 + // tags (array) 28 33 if strings.HasPrefix(p, "#") && len(p) > 1 { 29 - filters = append(filters, map[string]interface{}{ 30 - "term": map[string]interface{}{ 31 - "tag": map[string]interface{}{ 32 - "value": p[1:], 33 - "case_insensitive": true, 34 - }, 35 - }, 36 - }) 34 + params.Tags = append(params.Tags, p[1:]) 35 + continue 36 + } 37 + 38 + // handle (mention) 39 + if strings.HasPrefix(p, "@") && len(p) > 1 { 40 + handle, err := syntax.ParseHandle(p[1:]) 41 + if err != nil { 42 + keep = append(keep, p) 43 + continue 44 + } 45 + id, err := dir.LookupHandle(ctx, handle) 46 + if err != nil { 47 + if err != identity.ErrHandleNotFound { 48 + slog.Error("failed to resolve handle", "err", err) 49 + } 50 + continue 51 + } 52 + params.Mentions = &id.DID 37 53 continue 38 54 } 39 - if strings.HasPrefix(p, "did:") { 40 - filters = append(filters, map[string]interface{}{ 41 - "term": map[string]interface{}{"did": p}, 42 - }) 55 + 56 + tokParts := strings.SplitN(p, ":", 2) 57 + if len(tokParts) == 1 { 58 + keep = append(keep, p) 43 59 continue 44 60 } 45 - if strings.HasPrefix(p, "from:") && len(p) > 6 { 46 - h := p[5:] 47 - if h[0] == '@' { 48 - h = h[1:] 61 + 62 + switch tokParts[0] { 63 + case "did": 64 + // TODO: not really clear what to do here; treating like a mention doesn't really make sense? 65 + case "from", "to", "mentions": 66 + raw := tokParts[1] 67 + if strings.HasPrefix(raw, "@") && len(raw) > 1 { 68 + raw = raw[1:] 49 69 } 50 - handle, err := syntax.ParseHandle(h) 70 + handle, err := syntax.ParseHandle(raw) 51 71 if err != nil { 52 - keep = append(keep, p) 53 72 continue 54 73 } 55 74 id, err := dir.LookupHandle(ctx, handle) ··· 59 78 } 60 79 continue 61 80 } 62 - filters = append(filters, map[string]interface{}{ 63 - "term": map[string]interface{}{"did": id.DID.String()}, 64 - }) 81 + if tokParts[0] == "from" { 82 + params.Author = &id.DID 83 + } else { 84 + params.Mentions = &id.DID 85 + } 86 + continue 87 + case "http", "https": 88 + params.URL = p 89 + continue 90 + case "domain": 91 + params.Domain = tokParts[1] 92 + continue 93 + case "lang": 94 + lang, err := syntax.ParseLanguage(tokParts[1]) 95 + if nil == err { 96 + params.Lang = &lang 97 + } 98 + continue 99 + case "since", "until": 100 + // TODO: special-case handle dates? 101 + dt, err := syntax.ParseDatetimeLenient(tokParts[1]) 102 + if err != nil { 103 + continue 104 + } 105 + if tokParts[0] == "since" { 106 + params.Since = &dt 107 + } else { 108 + params.Until = &dt 109 + } 65 110 continue 66 111 } 67 112 ··· 70 115 71 116 out := "" 72 117 for _, p := range keep { 73 - if strings.ContainsRune(p, ' ') { 74 - if out == "" { 75 - out = fmt.Sprintf(`"%s"`, p) 76 - } else { 77 - out += " " + fmt.Sprintf(`"%s"`, p) 78 - } 118 + if out == "" { 119 + out = p 79 120 } else { 80 - if out == "" { 81 - out = p 82 - } else { 83 - out += " " + p 84 - } 121 + out += " " + p 85 122 } 86 123 } 87 - if out == "" && len(filters) >= 1 { 124 + if out == "" { 88 125 out = "*" 89 126 } 90 - return out, filters 127 + params.Query = out 128 + return params 91 129 }
+50 -33
search/parse_query_test.go
··· 19 19 DID: syntax.DID("did:plc:abc222"), 20 20 }) 21 21 22 - var q string 23 - var f []map[string]interface{} 22 + var p PostSearchParams 24 23 25 - q, f = ParseQuery(ctx, &dir, "") 26 - assert.Equal("", q) 27 - assert.Empty(f) 24 + p = ParsePostQuery(ctx, &dir, "") 25 + assert.Equal("*", p.Query) 26 + assert.Empty(p.Filters()) 28 27 29 - p1 := "some +test \"with phrase\" -ok" 30 - q, f = ParseQuery(ctx, &dir, p1) 31 - assert.Equal(p1, q) 32 - assert.Empty(f) 28 + q1 := "some +test \"with phrase\" -ok" 29 + p = ParsePostQuery(ctx, &dir, q1) 30 + assert.Equal(q1, p.Query) 31 + assert.Empty(p.Filters()) 33 32 34 - p2 := "missing from:missing.example.com" 35 - q, f = ParseQuery(ctx, &dir, p2) 36 - assert.Equal("missing", q) 37 - assert.Empty(f) 33 + q2 := "missing from:missing.example.com" 34 + p = ParsePostQuery(ctx, &dir, q2) 35 + assert.Equal("missing", p.Query) 36 + assert.Empty(p.Filters()) 38 37 39 - p3 := "known from:known.example.com" 40 - q, f = ParseQuery(ctx, &dir, p3) 41 - assert.Equal("known", q) 42 - assert.Equal(1, len(f)) 38 + q3 := "known from:known.example.com" 39 + p = ParsePostQuery(ctx, &dir, q3) 40 + assert.Equal("known", p.Query) 41 + assert.NotNil(p.Author) 42 + if p.Author != nil { 43 + assert.Equal("did:plc:abc222", p.Author.String()) 44 + } 43 45 44 - p4 := "from:known.example.com" 45 - q, f = ParseQuery(ctx, &dir, p4) 46 - assert.Equal("*", q) 47 - assert.Equal(1, len(f)) 46 + q4 := "from:known.example.com" 47 + p = ParsePostQuery(ctx, &dir, q4) 48 + assert.Equal("*", p.Query) 49 + assert.Equal(1, len(p.Filters())) 50 + 51 + q5 := `from:known.example.com "multi word phrase" coolio blorg` 52 + p = ParsePostQuery(ctx, &dir, q5) 53 + assert.Equal(`"multi word phrase" coolio blorg`, p.Query) 54 + assert.NotNil(p.Author) 55 + if p.Author != nil { 56 + assert.Equal("did:plc:abc222", p.Author.String()) 57 + } 58 + assert.Equal(1, len(p.Filters())) 48 59 49 - p5 := `from:known.example.com "multi word phrase" coolio blorg` 50 - q, f = ParseQuery(ctx, &dir, p5) 51 - assert.Equal(`"multi word phrase" coolio blorg`, q) 52 - assert.Equal(1, len(f)) 60 + q6 := `from:known.example.com #cool_tag some other stuff` 61 + p = ParsePostQuery(ctx, &dir, q6) 62 + assert.Equal(`some other stuff`, p.Query) 63 + assert.NotNil(p.Author) 64 + if p.Author != nil { 65 + assert.Equal("did:plc:abc222", p.Author.String()) 66 + } 67 + assert.Equal([]string{"cool_tag"}, p.Tags) 68 + assert.Equal(2, len(p.Filters())) 53 69 54 - p6 := `from:known.example.com #cool_tag some other stuff` 55 - q, f = ParseQuery(ctx, &dir, p6) 56 - assert.Equal(`some other stuff`, q) 57 - assert.Equal(2, len(f)) 70 + q7 := "known from:@known.example.com" 71 + p = ParsePostQuery(ctx, &dir, q7) 72 + assert.Equal("known", p.Query) 73 + assert.NotNil(p.Author) 74 + if p.Author != nil { 75 + assert.Equal("did:plc:abc222", p.Author.String()) 76 + } 77 + assert.Equal(1, len(p.Filters())) 58 78 59 - p7 := "known from:@known.example.com" 60 - q, f = ParseQuery(ctx, &dir, p7) 61 - assert.Equal("known", q) 62 - assert.Equal(1, len(f)) 79 + // TODO: more parsing tests: bare handles, to:, since:, until:, URL, domain:, lang 63 80 }
+86 -10
search/query.go
··· 7 7 "fmt" 8 8 "io/ioutil" 9 9 "log/slog" 10 - "time" 11 10 12 11 "github.com/bluesky-social/indigo/atproto/identity" 13 12 "github.com/bluesky-social/indigo/atproto/syntax" ··· 50 49 Post any `json:"post"` 51 50 } 52 51 53 - type PostSearchQuery struct { 52 + type PostSearchParams struct { 54 53 Query string `json:"q"` 55 54 Sort string `json:"sort"` 56 55 Author *syntax.DID `json:"author"` ··· 60 59 Lang *syntax.Language `json:"lang"` 61 60 Domain string `json:"domain"` 62 61 URL string `json:"url"` 63 - Tag string `json:"tag"` 62 + Tags []string `json:"tag"` 64 63 Offset int `json:"offset"` 65 64 Size int `json:"size"` 66 65 } 67 66 68 - type ActorSearchQuery struct { 67 + type ActorSearchParams struct { 69 68 Query string `json:"q"` 70 69 Typeahead bool `json:"typeahead"` 71 70 Account *syntax.DID `json:"account"` ··· 73 72 Size int `json:"size"` 74 73 } 75 74 75 + func (p *PostSearchParams) Filters() []map[string]interface{} { 76 + var filters []map[string]interface{} 77 + 78 + if p.Author != nil { 79 + filters = append(filters, map[string]interface{}{ 80 + "term": map[string]interface{}{"did": p.Author.String()}, 81 + }) 82 + } 83 + 84 + if p.Mentions != nil { 85 + filters = append(filters, map[string]interface{}{ 86 + "term": map[string]interface{}{"mention_did": p.Mentions.String()}, 87 + }) 88 + } 89 + 90 + if p.Lang != nil { 91 + // TODO: extracting just the 2-char code would be good 92 + filters = append(filters, map[string]interface{}{ 93 + "term": map[string]interface{}{"lang_code_iso2": p.Lang.String()}, 94 + }) 95 + } 96 + 97 + if p.Since != nil { 98 + filters = append(filters, map[string]interface{}{ 99 + "range": map[string]interface{}{ 100 + "created_at": map[string]interface{}{ 101 + "gte": p.Since.String(), 102 + }, 103 + }, 104 + }) 105 + } 106 + 107 + if p.Until != nil { 108 + filters = append(filters, map[string]interface{}{ 109 + "range": map[string]interface{}{ 110 + "created_at": map[string]interface{}{ 111 + "lt": p.Until.String(), 112 + }, 113 + }, 114 + }) 115 + } 116 + 117 + if p.Lang != nil { 118 + // TODO: extracting just the 2-char code would be good 119 + filters = append(filters, map[string]interface{}{ 120 + "term": map[string]interface{}{"lang_code_iso2": p.Lang.String()}, 121 + }) 122 + } 123 + 124 + if p.URL != "" { 125 + filters = append(filters, map[string]interface{}{ 126 + "term": map[string]interface{}{"url": p.URL}, 127 + }) 128 + } 129 + 130 + if p.Domain != "" { 131 + filters = append(filters, map[string]interface{}{ 132 + "term": map[string]interface{}{"domain": p.Domain}, 133 + }) 134 + } 135 + 136 + for _, tag := range p.Tags { 137 + filters = append(filters, map[string]interface{}{ 138 + "term": map[string]interface{}{ 139 + "tag": map[string]interface{}{ 140 + "value": tag, 141 + "case_insensitive": true, 142 + }, 143 + }, 144 + }) 145 + } 146 + 147 + return filters 148 + } 149 + 76 150 func checkParams(offset, size int) error { 77 151 if offset+size > 10000 || size > 250 || offset > 10000 || offset < 0 || size < 0 { 78 152 return fmt.Errorf("disallowed size/offset parameters") ··· 87 161 if err := checkParams(offset, size); err != nil { 88 162 return nil, err 89 163 } 90 - queryStr, filters := ParseQuery(ctx, dir, q) 164 + params := ParsePostQuery(ctx, dir, q) 91 165 idx := "everything" 92 - if containsJapanese(queryStr) { 166 + if containsJapanese(params.Query) { 93 167 idx = "everything_ja" 94 168 } 95 169 basic := map[string]interface{}{ 96 170 "simple_query_string": map[string]interface{}{ 97 - "query": queryStr, 171 + "query": params.Query, 98 172 "fields": []string{idx}, 99 173 "flags": "AND|NOT|OR|PHRASE|PRECEDENCE|WHITESPACE", 100 174 "default_operator": "and", ··· 102 176 "analyze_wildcard": false, 103 177 }, 104 178 } 179 + filters := params.Filters() 105 180 // filter out future posts (TODO: temporary hack) 106 181 now := syntax.DatetimeNow() 107 182 filters = append(filters, map[string]interface{}{ ··· 138 213 return nil, err 139 214 } 140 215 141 - queryStr, filters := ParseQuery(ctx, dir, q) 216 + // TODO: have a ParseProfileQuery function? 217 + params := ParsePostQuery(ctx, dir, q) 142 218 basic := map[string]interface{}{ 143 219 "simple_query_string": map[string]interface{}{ 144 - "query": queryStr, 220 + "query": params.Query, 145 221 "fields": []string{"everything"}, 146 222 "flags": "AND|NOT|OR|PHRASE|PRECEDENCE|WHITESPACE", 147 223 "default_operator": "and", ··· 159 235 map[string]interface{}{"term": map[string]interface{}{"has_banner": true}}, 160 236 }, 161 237 "minimum_should_match": 0, 162 - "filter": filters, 238 + "filter": params.Filters(), 163 239 "boost": 0.5, 164 240 }, 165 241 },