tangled
alpha
login
or
join now
stream.place
/
streamplace
77
fork
atom
Live video on the AT Protocol
77
fork
atom
overview
issues
1
pulls
pipelines
moderation: implement labeler subscriptions
Eli Mallon
7 months ago
897a85d5
daf4d973
+153
3 changed files
expand all
collapse all
unified
split
pkg
atproto
labeler_firehose.go
cmd
streamplace.go
config
config.go
+146
pkg/atproto/labeler_firehose.go
···
1
1
+
package atproto
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"fmt"
6
6
+
"net/http"
7
7
+
"net/url"
8
8
+
"time"
9
9
+
10
10
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
11
11
+
"github.com/bluesky-social/indigo/atproto/label"
12
12
+
"github.com/bluesky-social/indigo/events"
13
13
+
"github.com/bluesky-social/indigo/events/schedulers/parallel"
14
14
+
"github.com/gorilla/websocket"
15
15
+
"golang.org/x/sync/errgroup"
16
16
+
"stream.place/streamplace/pkg/aqhttp"
17
17
+
"stream.place/streamplace/pkg/log"
18
18
+
)
19
19
+
20
20
+
func (atsync *ATProtoSynchronizer) StartLabelerFirehose(ctx context.Context, did string) error {
21
21
+
retryCount := 0
22
22
+
retryWindow := time.Now()
23
23
+
24
24
+
for {
25
25
+
if ctx.Err() != nil {
26
26
+
return nil
27
27
+
}
28
28
+
err := atsync.StartLabelerFirehoseRetry(ctx, did)
29
29
+
if err != nil {
30
30
+
log.Error(ctx, "firehose error", "err", err)
31
31
+
32
32
+
// Check if we're within the 1-minute window
33
33
+
now := time.Now()
34
34
+
if now.Sub(retryWindow) > time.Minute {
35
35
+
// Reset the counter if more than a minute has passed
36
36
+
retryCount = 1
37
37
+
retryWindow = now
38
38
+
} else {
39
39
+
// Increment retry count if within the window
40
40
+
retryCount++
41
41
+
if retryCount >= 3 {
42
42
+
log.Error(ctx, "firehose failed 3 times within a minute, crashing", "err", err)
43
43
+
return fmt.Errorf("firehose failed 3 times within a minute: %w", err)
44
44
+
}
45
45
+
}
46
46
+
}
47
47
+
}
48
48
+
}
49
49
+
50
50
+
func (atsync *ATProtoSynchronizer) StartLabelerFirehoseRetry(ctx context.Context, did string) error {
51
51
+
ctx = log.WithLogValues(ctx, "func", "StartLabelerFirehose")
52
52
+
53
53
+
ident, err := ResolveIdent(ctx, did)
54
54
+
if err != nil {
55
55
+
return fmt.Errorf("failed to resolve DID %s: %w", did, err)
56
56
+
}
57
57
+
58
58
+
ctx = log.WithLogValues(ctx, "labelerDID", ident.DID.String(), "labelerHandle", ident.Handle.String())
59
59
+
60
60
+
pub, err := ident.GetPublicKey("atproto_label")
61
61
+
if err != nil {
62
62
+
return fmt.Errorf("failed to get public key for labeler %s: %w", did, err)
63
63
+
}
64
64
+
65
65
+
labeler, ok := ident.Services["atproto_labeler"]
66
66
+
if !ok {
67
67
+
return fmt.Errorf("labeler %s does not have a atproto_labeler service", did)
68
68
+
}
69
69
+
70
70
+
ctx = log.WithLogValues(ctx, "func", "StartFirehose")
71
71
+
ctx, cancel := context.WithCancel(ctx)
72
72
+
defer cancel()
73
73
+
dialer := websocket.DefaultDialer
74
74
+
u, err := url.Parse(labeler.URL)
75
75
+
if err != nil {
76
76
+
return fmt.Errorf("invalid labeler URI: %w", err)
77
77
+
}
78
78
+
u.Path = "xrpc/com.atproto.label.subscribeLabels"
79
79
+
if u.Scheme == "http" {
80
80
+
u.Scheme = "ws"
81
81
+
} else if u.Scheme == "https" {
82
82
+
u.Scheme = "wss"
83
83
+
} else {
84
84
+
return fmt.Errorf("invalid labeler URI scheme: %s", labeler.URL)
85
85
+
}
86
86
+
query := u.Query()
87
87
+
query.Set("cursor", "0")
88
88
+
u.RawQuery = query.Encode()
89
89
+
90
90
+
con, _, err := dialer.Dial(u.String(), http.Header{
91
91
+
"User-Agent": []string{aqhttp.UserAgent},
92
92
+
})
93
93
+
if err != nil {
94
94
+
return fmt.Errorf("subscribing to firehose failed (dialing): %w", err)
95
95
+
}
96
96
+
97
97
+
rsc := &events.RepoStreamCallbacks{
98
98
+
LabelLabels: func(evt *comatproto.LabelSubscribeLabels_Labels) error {
99
99
+
log.Log(ctx, "labeler labels", "labels", evt.Labels, "seq", evt.Seq)
100
100
+
for _, labelLex := range evt.Labels {
101
101
+
l := label.FromLexicon(labelLex)
102
102
+
err = l.VerifySignature(pub)
103
103
+
if err != nil {
104
104
+
log.Error(ctx, "failed to verify label signature", "err", err)
105
105
+
continue
106
106
+
}
107
107
+
err = l.VerifySyntax()
108
108
+
if err != nil {
109
109
+
log.Error(ctx, "failed to verify label syntax", "err", err)
110
110
+
continue
111
111
+
}
112
112
+
log.Log(ctx, "labeler label", "cid", l.CID, "createdAt", l.CreatedAt, "expiresAt", l.ExpiresAt, "negated", l.Negated, "sourceDID", l.SourceDID, "uri", l.URI, "val", l.Val, "version", l.Version)
113
113
+
}
114
114
+
return nil
115
115
+
},
116
116
+
LabelInfo: func(evt *comatproto.LabelSubscribeLabels_Info) error {
117
117
+
log.Log(ctx, "labeler info", "name", evt.Name, "message", evt.Message)
118
118
+
return nil
119
119
+
},
120
120
+
Error: func(evt *events.ErrorFrame) error {
121
121
+
log.Error(ctx, "firehose error", "err", evt.Error, "message", evt.Message)
122
122
+
cancel()
123
123
+
return fmt.Errorf("firehose error: %s", evt.Error)
124
124
+
},
125
125
+
}
126
126
+
127
127
+
scheduler := parallel.NewScheduler(
128
128
+
10,
129
129
+
100,
130
130
+
did,
131
131
+
rsc.EventHandler,
132
132
+
)
133
133
+
134
134
+
log.Log(ctx, "starting labeler firehose consumer", "labelerDID", did)
135
135
+
136
136
+
g, ctx := errgroup.WithContext(ctx)
137
137
+
138
138
+
g.Go(func() error {
139
139
+
return events.HandleRepoStream(ctx, con, scheduler, nil)
140
140
+
})
141
141
+
142
142
+
<-ctx.Done()
143
143
+
144
144
+
return nil
145
145
+
146
146
+
}
+5
pkg/cmd/streamplace.go
···
365
365
group.Go(func() error {
366
366
return atsync.StartFirehose(ctx)
367
367
})
368
368
+
for _, labeler := range cli.Labelers {
369
369
+
group.Go(func() error {
370
370
+
return atsync.StartLabelerFirehose(ctx, labeler)
371
371
+
})
372
372
+
}
368
373
}
369
374
370
375
group.Go(func() error {
+2
pkg/config/config.go
···
107
107
NewWebRTCPlayback bool
108
108
AppleTeamID string
109
109
AndroidCertFingerprint string
110
110
+
Labelers []string
110
111
}
111
112
112
113
func (cli *CLI) NewFlagSet(name string) *flag.FlagSet {
···
164
165
fs.BoolVar(&cli.NewWebRTCPlayback, "new-webrtc-playback", true, "enable new webrtc playback")
165
166
fs.StringVar(&cli.AppleTeamID, "apple-team-id", "", "apple team id for deep linking")
166
167
fs.StringVar(&cli.AndroidCertFingerprint, "android-cert-fingerprint", "", "android cert fingerprint for deep linking")
168
168
+
cli.StringSliceFlag(fs, &cli.Labelers, "labelers", "", "did of labelers that this instance should subscribe to")
167
169
168
170
if runtime.GOOS == "linux" {
169
171
fs.BoolVar(&cli.NoMist, "no-mist", true, "Disable MistServer")