1package helpers
2
3import (
4 "fmt"
5
6 appbsky "github.com/bluesky-social/indigo/api/bsky"
7 "github.com/bluesky-social/indigo/atproto/syntax"
8 "github.com/bluesky-social/indigo/automod"
9 "github.com/bluesky-social/indigo/automod/keyword"
10)
11
12func ExtractHashtagsPost(post *appbsky.FeedPost) []string {
13 var tags []string
14 tags = append(tags, post.Tags...)
15 for _, facet := range post.Facets {
16 for _, feat := range facet.Features {
17 if feat.RichtextFacet_Tag != nil {
18 tags = append(tags, feat.RichtextFacet_Tag.Tag)
19 }
20 }
21 }
22 return DedupeStrings(tags)
23}
24
25func NormalizeHashtag(raw string) string {
26 return keyword.Slugify(raw)
27}
28
29type PostFacet struct {
30 Text string
31 URL *string
32 DID *string
33 Tag *string
34}
35
36func ExtractFacets(post *appbsky.FeedPost) ([]PostFacet, error) {
37 var out []PostFacet
38
39 for _, facet := range post.Facets {
40 for _, feat := range facet.Features {
41 if int(facet.Index.ByteEnd) > len([]byte(post.Text)) || facet.Index.ByteStart > facet.Index.ByteEnd {
42 return nil, fmt.Errorf("invalid facet byte range")
43 }
44
45 txt := string([]byte(post.Text)[facet.Index.ByteStart:facet.Index.ByteEnd])
46 if txt == "" {
47 return nil, fmt.Errorf("empty facet text")
48 }
49
50 if feat.RichtextFacet_Link != nil {
51 out = append(out, PostFacet{
52 Text: txt,
53 URL: &feat.RichtextFacet_Link.Uri,
54 })
55 }
56 if feat.RichtextFacet_Tag != nil {
57 out = append(out, PostFacet{
58 Text: txt,
59 Tag: &feat.RichtextFacet_Tag.Tag,
60 })
61 }
62 if feat.RichtextFacet_Mention != nil {
63 out = append(out, PostFacet{
64 Text: txt,
65 DID: &feat.RichtextFacet_Mention.Did,
66 })
67 }
68 }
69 }
70 return out, nil
71}
72
73func ExtractPostBlobCIDsPost(post *appbsky.FeedPost) []string {
74 var out []string
75 if post.Embed.EmbedImages != nil {
76 for _, img := range post.Embed.EmbedImages.Images {
77 out = append(out, img.Image.Ref.String())
78 }
79 }
80 if post.Embed.EmbedRecordWithMedia != nil {
81 media := post.Embed.EmbedRecordWithMedia.Media
82 if media.EmbedImages != nil {
83 for _, img := range media.EmbedImages.Images {
84 out = append(out, img.Image.Ref.String())
85 }
86 }
87 }
88 return DedupeStrings(out)
89}
90
91func ExtractBlobCIDsProfile(profile *appbsky.ActorProfile) []string {
92 var out []string
93 if profile.Avatar != nil {
94 out = append(out, profile.Avatar.Ref.String())
95 }
96 if profile.Banner != nil {
97 out = append(out, profile.Banner.Ref.String())
98 }
99 return DedupeStrings(out)
100}
101
102func ExtractTextTokensPost(post *appbsky.FeedPost) []string {
103 s := post.Text
104 if post.Embed != nil {
105 if post.Embed.EmbedImages != nil {
106 for _, img := range post.Embed.EmbedImages.Images {
107 if img.Alt != "" {
108 s += " " + img.Alt
109 }
110 }
111 }
112 if post.Embed.EmbedRecordWithMedia != nil {
113 media := post.Embed.EmbedRecordWithMedia.Media
114 if media.EmbedImages != nil {
115 for _, img := range media.EmbedImages.Images {
116 if img.Alt != "" {
117 s += " " + img.Alt
118 }
119 }
120 }
121 }
122 }
123 return keyword.TokenizeText(s)
124}
125
126func ExtractTextTokensProfile(profile *appbsky.ActorProfile) []string {
127 s := ""
128 if profile.Description != nil {
129 s += " " + *profile.Description
130 }
131 if profile.DisplayName != nil {
132 s += " " + *profile.DisplayName
133 }
134 return keyword.TokenizeText(s)
135}
136
137func ExtractTextURLsProfile(profile *appbsky.ActorProfile) []string {
138 s := ""
139 if profile.Description != nil {
140 s += " " + *profile.Description
141 }
142 if profile.DisplayName != nil {
143 s += " " + *profile.DisplayName
144 }
145 return ExtractTextURLs(s)
146}
147
148// checks if the post event is a reply post for which the author is replying to themselves, or author is the root author (OP)
149func IsSelfThread(c *automod.RecordContext, post *appbsky.FeedPost) bool {
150 if post.Reply == nil {
151 return false
152 }
153 did := c.Account.Identity.DID.String()
154 parentURI, err := syntax.ParseATURI(post.Reply.Parent.Uri)
155 if err != nil {
156 return false
157 }
158 rootURI, err := syntax.ParseATURI(post.Reply.Root.Uri)
159 if err != nil {
160 return false
161 }
162
163 if parentURI.Authority().String() == did || rootURI.Authority().String() == did {
164 return true
165 }
166 return false
167}
168
169func ParentOrRootIsFollower(c *automod.RecordContext, post *appbsky.FeedPost) bool {
170 if post.Reply == nil || IsSelfThread(c, post) {
171 return false
172 }
173
174 parentURI, err := syntax.ParseATURI(post.Reply.Parent.Uri)
175 if err != nil {
176 c.Logger.Warn("failed to parse reply AT-URI", "uri", post.Reply.Parent.Uri)
177 return false
178 }
179 parentDID, err := parentURI.Authority().AsDID()
180 if err != nil {
181 c.Logger.Warn("reply AT-URI authority not a DID", "uri", post.Reply.Parent.Uri)
182 return false
183 }
184
185 rel := c.GetAccountRelationship(parentDID)
186 if rel.FollowedBy {
187 return true
188 }
189
190 rootURI, err := syntax.ParseATURI(post.Reply.Root.Uri)
191 if err != nil {
192 c.Logger.Warn("failed to parse reply AT-URI", "uri", post.Reply.Root.Uri)
193 return false
194 }
195 rootDID, err := rootURI.Authority().AsDID()
196 if err != nil {
197 c.Logger.Warn("reply AT-URI authority not a DID", "uri", post.Reply.Root.Uri)
198 return false
199 }
200
201 if rootDID == parentDID {
202 return false
203 }
204
205 rel = c.GetAccountRelationship(rootDID)
206 if rel.FollowedBy {
207 return true
208 }
209 return false
210}
211
212func PostParentOrRootIsDid(post *appbsky.FeedPost, did string) bool {
213 if post.Reply == nil {
214 return false
215 }
216
217 rootUri, err := syntax.ParseATURI(post.Reply.Root.Uri)
218 if err != nil || !rootUri.Authority().IsDID() {
219 return false
220 }
221
222 parentUri, err := syntax.ParseATURI(post.Reply.Parent.Uri)
223 if err != nil || !parentUri.Authority().IsDID() {
224 return false
225 }
226
227 return rootUri.Authority().String() == did || parentUri.Authority().String() == did
228}
229
230func PostParentOrRootIsAnyDid(post *appbsky.FeedPost, dids []string) bool {
231 if post.Reply == nil {
232 return false
233 }
234
235 for _, did := range dids {
236 if PostParentOrRootIsDid(post, did) {
237 return true
238 }
239 }
240
241 return false
242}
243
244func PostMentionsDid(post *appbsky.FeedPost, did string) bool {
245 facets, err := ExtractFacets(post)
246 if err != nil {
247 return false
248 }
249
250 for _, facet := range facets {
251 if facet.DID != nil && *facet.DID == did {
252 return true
253 }
254 }
255
256 return false
257}
258
259func PostMentionsAnyDid(post *appbsky.FeedPost, dids []string) bool {
260 for _, did := range dids {
261 if PostMentionsDid(post, did) {
262 return true
263 }
264 }
265
266 return false
267}