1package search
2
3import (
4 "context"
5 "log/slog"
6 "strings"
7 "time"
8
9 "github.com/bluesky-social/indigo/atproto/identity"
10 "github.com/bluesky-social/indigo/atproto/syntax"
11)
12
13// ParsePostQuery takes a query string and pulls out some facet patterns ("from:handle.net") as filters
14func ParsePostQuery(ctx context.Context, dir identity.Directory, raw string, viewer *syntax.DID) PostSearchParams {
15 quoted := false
16 parts := strings.FieldsFunc(raw, func(r rune) bool {
17 if r == '"' {
18 quoted = !quoted
19 }
20 return r == ' ' && !quoted
21 })
22
23 params := PostSearchParams{}
24
25 keep := make([]string, 0, len(parts))
26 for _, p := range parts {
27 // pass-through quoted, either phrase or single token
28 if strings.HasPrefix(p, "\"") {
29 keep = append(keep, p)
30 continue
31 }
32
33 // tags (array)
34 if strings.HasPrefix(p, "#") && len(p) > 1 {
35 params.Tags = append(params.Tags, p[1:])
36 continue
37 }
38
39 // handle (mention)
40 if strings.HasPrefix(p, "@") && len(p) > 1 {
41 handle, err := syntax.ParseHandle(p[1:])
42 if err != nil {
43 keep = append(keep, p)
44 continue
45 }
46 id, err := dir.LookupHandle(ctx, handle)
47 if err != nil {
48 if err != identity.ErrHandleNotFound {
49 slog.Error("failed to resolve handle", "err", err)
50 }
51 continue
52 }
53 params.Mentions = &id.DID
54 continue
55 }
56
57 tokParts := strings.SplitN(p, ":", 2)
58 if len(tokParts) == 1 {
59 keep = append(keep, p)
60 continue
61 }
62
63 switch tokParts[0] {
64 case "did":
65 // Used as a hack for `from:me` when supplied by the client
66 did, err := syntax.ParseDID(p)
67 if err != nil {
68 continue
69 }
70 params.Author = &did
71 continue
72 case "from", "to", "mentions":
73 raw := tokParts[1]
74 if raw == "me" {
75 if viewer != nil && tokParts[0] == "from" {
76 params.Author = viewer
77 } else if viewer != nil {
78 params.Mentions = viewer
79 }
80 continue
81 }
82 if strings.HasPrefix(raw, "@") && len(raw) > 1 {
83 raw = raw[1:]
84 }
85 handle, err := syntax.ParseHandle(raw)
86 if err != nil {
87 continue
88 }
89 id, err := dir.LookupHandle(ctx, handle)
90 if err != nil {
91 if err != identity.ErrHandleNotFound {
92 slog.Error("failed to resolve handle", "err", err)
93 }
94 continue
95 }
96 if tokParts[0] == "from" {
97 params.Author = &id.DID
98 } else {
99 params.Mentions = &id.DID
100 }
101 continue
102 case "http", "https":
103 params.URL = p
104 continue
105 case "domain":
106 params.Domain = tokParts[1]
107 continue
108 case "lang":
109 lang, err := syntax.ParseLanguage(tokParts[1])
110 if nil == err {
111 params.Lang = &lang
112 }
113 continue
114 case "since", "until":
115 var dt syntax.Datetime
116 // first try just date
117 date, err := time.Parse(time.DateOnly, tokParts[1])
118 if nil == err {
119 dt = syntax.Datetime(date.Format(syntax.AtprotoDatetimeLayout))
120 } else {
121 // fallback to formal atproto datetime format
122 dt, err = syntax.ParseDatetimeLenient(tokParts[1])
123 if err != nil {
124 continue
125 }
126 }
127 if tokParts[0] == "since" {
128 params.Since = &dt
129 } else {
130 params.Until = &dt
131 }
132 continue
133 }
134
135 keep = append(keep, p)
136 }
137
138 out := ""
139 for _, p := range keep {
140 if out == "" {
141 out = p
142 } else {
143 out += " " + p
144 }
145 }
146 if out == "" {
147 out = "*"
148 }
149 params.Query = out
150 return params
151}