Skip to content

Commit ce3d8c8

Browse files
committed
cmd/bsky-webhook: multiple words, case insenstivity and word boundary
Fixes #18 Signed-off-by: Erisa A <[email protected]>
1 parent cdb9e12 commit ce3d8c8

File tree

1 file changed

+53
-9
lines changed

1 file changed

+53
-9
lines changed

cmd/bsky-webhook/main.go

+53-9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"os"
1717
"os/signal"
1818
"path"
19+
"regexp"
1920
"strconv"
2021
"strings"
2122
"syscall"
@@ -42,7 +43,10 @@ var (
4243
"https://bsky.social"), "bluesky PDS server URL")
4344
watchWord = flag.String("watch-word", envOr("WATCH_WORD", "tailscale"),
4445
"the word to watch out for. may be multiple words in future (required)")
45-
46+
watchWords = flag.String("watch-words", envOr("WATCH_WORDS", ""), // "word1;word2"
47+
"the words to watch out for. if watch-word is also specified, they are added")
48+
delimiter = flag.String("word-delimiter", envOr("WORD_DELIMITER", ";"),
49+
"the character(s) that multi-word options are split by.")
4650
secretsURL = flag.String("secrets-url", envOr("SECRETS_URL", ""),
4751
"the URL of a secrets server (if empty, no server is used)")
4852
secretsPrefix = flag.String("secrets-prefix", envOr("SECRETS_PREFIX", ""),
@@ -51,6 +55,10 @@ var (
5155
"the Tailscale hostname the server should advertise (if empty, runs locally)")
5256
tsStateDir = flag.String("ts-state-dir", envOr("TS_STATE_DIR", ""),
5357
"the Tailscale state directory path (optional)")
58+
caseSensitive = flag.Bool("case-sensitive-words", hasEnv("CASE_SENSITIVE_WORDS"),
59+
"make watch words case-sensitive")
60+
enforceWordBoundary = flag.Bool("enforce-word-boundary", hasEnv("ENFORCE_WORD_BOUNDARY"),
61+
"only match \"whole words\", \\b in regex")
5462
)
5563

5664
// Public addresses of jetstream websocket services.
@@ -92,8 +100,40 @@ func main() {
92100
log.Fatal("Missing Bluesky account handle (BSKY_HANDLE)")
93101
case *bskyAppKey == "" && *secretsURL == "":
94102
log.Fatal("missing Bluesky app secret (BSKY_APP_PASSWORD)")
95-
case *watchWord == "":
96-
log.Fatal("missing watchword")
103+
case *watchWord == "" && *watchWords == "":
104+
log.Fatal("missing watchWord and watchWords (WATCH_WORD / WATCH_WORDS)")
105+
}
106+
107+
words := strings.Split(*watchWords, *delimiter)
108+
words = append(words, *watchWord)
109+
110+
if len(words) == 0 {
111+
log.Fatal("no words! nothing to do!")
112+
}
113+
114+
// build the regular expression for matching words
115+
var rb strings.Builder
116+
if *enforceWordBoundary {
117+
rb.WriteString("\\b(")
118+
}
119+
if !*caseSensitive {
120+
rb.WriteString("(?i)")
121+
}
122+
123+
// prepare the words for being compiled into regex
124+
for i, v := range words {
125+
words[i] = "(" + regexp.QuoteMeta(v) + ")"
126+
}
127+
128+
rb.WriteString(strings.Join(words, "|"))
129+
if *enforceWordBoundary {
130+
rb.WriteString(")\\b")
131+
}
132+
133+
log.Print(rb.String())
134+
wordRx, err := regexp.Compile(rb.String())
135+
if err != nil {
136+
log.Fatalf("compile regex: %v", err)
97137
}
98138

99139
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
@@ -139,14 +179,19 @@ func main() {
139179
}
140180
slog.Info("ws connecting", "url", wsURL.String())
141181

142-
err := websocketConnection(ctx, wsURL)
182+
err := websocketConnection(ctx, wsURL, *wordRx)
143183
slog.Error("ws connection", "url", wsURL, "err", err)
144184

145185
// TODO(erisa): exponential backoff
146186
time.Sleep(2 * time.Second)
147187
}
148188
}
149189

190+
func hasEnv(name string) bool {
191+
_, ok := os.LookupEnv(name)
192+
return ok
193+
}
194+
150195
func envOr(key, defaultVal string) string {
151196
if result, ok := os.LookupEnv(key); ok {
152197
return result
@@ -171,7 +216,7 @@ func nextWSAddress() func() string {
171216
}
172217
}
173218

174-
func websocketConnection(ctx context.Context, wsUrl url.URL) error {
219+
func websocketConnection(ctx context.Context, wsUrl url.URL, wordRx regexp.Regexp) error {
175220
// add compression headers
176221
headers := http.Header{}
177222
headers.Add("Socket-Encoding", "zstd")
@@ -206,7 +251,7 @@ func websocketConnection(ctx context.Context, wsUrl url.URL) error {
206251
return err
207252
}
208253

209-
err = readJetstreamMessage(ctx, jetstreamMessage, bsky)
254+
err = readJetstreamMessage(ctx, jetstreamMessage, bsky, wordRx)
210255
if err != nil {
211256
msg := jetstreamMessage[:min(32, len(jetstreamMessage))]
212257
log.Printf("error reading jetstream message %q: %v", msg, err)
@@ -216,7 +261,7 @@ func websocketConnection(ctx context.Context, wsUrl url.URL) error {
216261
return ctx.Err()
217262
}
218263

219-
func readJetstreamMessage(ctx context.Context, jetstreamMessageEncoded []byte, bsky *bluesky.Client) error {
264+
func readJetstreamMessage(ctx context.Context, jetstreamMessageEncoded []byte, bsky *bluesky.Client, wordRx regexp.Regexp) error {
220265
// Decompress the message
221266
m, err := zstdDecoder.DecodeAll(jetstreamMessageEncoded, nil)
222267
if err != nil {
@@ -247,7 +292,7 @@ func readJetstreamMessage(ctx context.Context, jetstreamMessageEncoded []byte, b
247292
return nil
248293
}
249294

250-
if strings.Contains(strings.ToLower(bskyMessage.Commit.Record.Text), strings.ToLower(*watchWord)) {
295+
if wordRx.MatchString(bskyMessage.Commit.Record.Text) {
251296
jetstreamMessageStr := string(jetstreamMessage)
252297

253298
go func() {
@@ -258,7 +303,6 @@ func readJetstreamMessage(ctx context.Context, jetstreamMessageEncoded []byte, b
258303
}
259304

260305
var imageURL string
261-
262306
if len(bskyMessage.Commit.Record.Embed.Images) != 0 {
263307
imageURL = fmt.Sprintf("https://cdn.bsky.app/img/feed_fullsize/plain/%s/%s", bskyMessage.DID, bskyMessage.Commit.Record.Embed.Images[0].Image.Ref.Link)
264308
}

0 commit comments

Comments
 (0)