Switch to own Jetstream and PLC endpointsUnverified
016bf11 parent: e230487 modified
.env.example +2 -1 | @@ -1,6 +1,7 @@ | ||
| 1 | 1 | GLEAN_ADDR=:8080 |
| 2 | 2 | GLEAN_DB=glean.db |
| 3 | -GLEAN_JETSTREAM=wss://jetstream2.fr.hose.cam | |
| 3 | +GLEAN_JETSTREAM=wss://jetstream.glean.at | |
| 4 | +GLEAN_PLC_URL=https://didplc.glean.at | |
| 4 | 5 | GLEAN_SYNC_INTERVAL=1h |
| 5 | 6 | GLEAN_CLUSTER_INTERVAL=6h |
| 6 | 7 | # Leave empty for localhost OAuth (development) |
| @@ -1,6 +1,7 @@ | |||
| 1 | GLEAN_ADDR=:8080 | 1 | GLEAN_ADDR=:8080 |
| 2 | GLEAN_DB=glean.db | 2 | GLEAN_DB=glean.db |
| 3 | -GLEAN_JETSTREAM=wss://jetstream2.fr.hose.cam | 3 | +GLEAN_JETSTREAM=wss://jetstream.glean.at |
| 4 | +GLEAN_PLC_URL=https://didplc.glean.at | ||
| 4 | GLEAN_SYNC_INTERVAL=1h | 5 | GLEAN_SYNC_INTERVAL=1h |
| 5 | GLEAN_CLUSTER_INTERVAL=6h | 6 | GLEAN_CLUSTER_INTERVAL=6h |
| 6 | # Leave empty for localhost OAuth (development) | 7 | # Leave empty for localhost OAuth (development) |
modified
docs/specs.md +34 -12 | @@ -10,14 +10,14 @@ The core idea: your RSS subscriptions are a strong signal about your interests. | ||
| 10 | 10 | |
| 11 | 11 | ## 2. Stack |
| 12 | 12 | |
| 13 | -| Layer | Technology | | |
| 14 | -| ---------------- | ------------------------------------ | | |
| 15 | -| Backend | Go | | |
| 16 | -| Database | SQLite (via `mattn/go-sqlite3`) | | |
| 17 | -| Frontend | htmx + TailwindCSS | | |
| 18 | -| Auth | AT Protocol OAuth / DID resolution | | |
| 19 | -| AT Protocol role | AppView for `at.glean.*` lexicons | | |
| 20 | -| Data source | AT Protocol Jetstream → SQLite index | | |
| 13 | +| Layer | Technology | | |
| 14 | +| ---------------- | --------------------------------------------------------------- | | |
| 15 | +| Backend | Go | | |
| 16 | +| Database | SQLite (via `mattn/go-sqlite3`) | | |
| 17 | +| Frontend | htmx + TailwindCSS | | |
| 18 | +| Auth | AT Protocol OAuth / DID resolution (configurable PLC directory) | | |
| 19 | +| AT Protocol role | AppView for `at.glean.*` lexicons | | |
| 20 | +| Data source | AT Protocol Jetstream → SQLite index | | |
| 21 | 21 | |
| 22 | 22 | ## 3. AT Protocol Lexicons |
| 23 | 23 | |
| @@ -137,7 +137,26 @@ The mapping from margin note to glean annotation: | ||
| 137 | 137 | |
| 138 | 138 | When no matching article exists in the local DB, the annotation is stored with an empty `feed_url`. Margin notes are indexed from both Jetstream and PDS sync, same as glean records. |
| 139 | 139 | |
| 140 | -### 3.5 AppView Query Lexicons | |
| 140 | +### 3.5 `app.bsky.graph.follow` (External) | |
| 141 | + | |
| 142 | +Follow relationships are tracked from Bluesky and Tangled follow records. The `FollowRecord` struct is validated against the lexicon at `lexicons/app/bsky/graph/follow.json`. The optional `via` field (a strong ref) is preserved as raw JSON but not used by Glean. | |
| 143 | + | |
| 144 | +### 3.6 Lexicon Constants | |
| 145 | + | |
| 146 | +All collection NSIDs are defined as constants in `lexicon.go` and used throughout the codebase: | |
| 147 | + | |
| 148 | +```go | |
| 149 | +const ( | |
| 150 | + CollectionSubscription = "at.glean.subscription" | |
| 151 | + CollectionAnnotation = "at.glean.annotation" | |
| 152 | + CollectionLike = "at.glean.like" | |
| 153 | + CollectionMarginNote = "at.margin.note" | |
| 154 | + CollectionBskyFollow = "app.bsky.graph.follow" | |
| 155 | + CollectionTangledFollow = "sh.tangled.graph.follow" | |
| 156 | +) | |
| 157 | +``` | |
| 158 | + | |
| 159 | +### 3.7 AppView Query Lexicons | |
| 141 | 160 | |
| 142 | 161 | As an AppView, Glean serves the following XRPC query endpoints. Other AT Protocol applications can call these to access indexed `at.glean.*` data without implementing their own indexer. |
| 143 | 162 | |
| @@ -234,9 +253,9 @@ Output: | ||
| 234 | 253 | people: [{ did, handle, displayName, avatar, jaccard, commonFeeds }] |
| 235 | 254 | ``` |
| 236 | 255 | |
| 237 | -### 3.6 AppView Jetstream Consumption | |
| 256 | +### 3.8 AppView Jetstream Consumption | |
| 238 | 257 | |
| 239 | -Glean subscribes to a Jetstream endpoint (`GLEAN_JETSTREAM`, default `wss://jetstream2.fr.hose.cam`) for all `at.glean.*` records: | |
| 258 | +Glean subscribes to a Jetstream endpoint (`GLEAN_JETSTREAM`, default `wss://jetstream.glean.at`) for all `at.glean.*` records: | |
| 240 | 259 | |
| 241 | 260 | ``` |
| 242 | 261 | SUBSCRIBE collections: ["at.glean.subscription", "at.glean.annotation", "at.glean.like", "app.bsky.graph.follow", "sh.tangled.graph.follow", "at.margin.note"] |
| @@ -774,7 +793,8 @@ glean/ | ||
| 774 | 793 | ├── lexicons/ |
| 775 | 794 | │ └── at/ |
| 776 | 795 | │ ├── glean/ # Glean lexicon JSON schemas (subscription, annotation, like) |
| 777 | -│ └── margin/ # External lexicon schemas (note.json for at.margin.note) | |
| 796 | +│ └── margin/ # External: at.margin.note W3C Web Annotation schema | |
| 797 | +│ └── app/bsky/graph/ # External: app.bsky.graph.follow schema | |
| 778 | 798 | ├── internal/ |
| 779 | 799 | │ ├── atproto/ |
| 780 | 800 | │ │ ├── auth.go # DID resolution, OAuth flow |
| @@ -846,6 +866,8 @@ glean/ | ||
| 846 | 866 | |
| 847 | 867 | ## 10. Auth Flow |
| 848 | 868 | |
| 869 | +DID resolution uses a configurable PLC directory (`GLEAN_PLC_URL`, defaults to `https://didplc.glean.at`). The identity directory is initialized once at startup via `InitIdentity()` with a caching layer (250k entries, 24h TTL). | |
| 870 | + | |
| 849 | 871 | 1. User visits `/`, clicks "Sign in with Bluesky" (or any AT Proto PDS) |
| 850 | 872 | 2. Server redirects to AT Protocol OAuth authorization endpoint |
| 851 | 873 | 3. User authorizes on their PDS |
| @@ -10,14 +10,14 @@ The core idea: your RSS subscriptions are a strong signal about your interests. | |||
| 10 | 10 | ||
| 11 | ## 2. Stack | 11 | ## 2. Stack |
| 12 | 12 | ||
| 13 | -| Layer | Technology | | 13 | +| Layer | Technology | |
| 14 | -| ---------------- | ------------------------------------ | | 14 | +| ---------------- | --------------------------------------------------------------- | |
| 15 | -| Backend | Go | | 15 | +| Backend | Go | |
| 16 | -| Database | SQLite (via `mattn/go-sqlite3`) | | 16 | +| Database | SQLite (via `mattn/go-sqlite3`) | |
| 17 | -| Frontend | htmx + TailwindCSS | | 17 | +| Frontend | htmx + TailwindCSS | |
| 18 | -| Auth | AT Protocol OAuth / DID resolution | | 18 | +| Auth | AT Protocol OAuth / DID resolution (configurable PLC directory) | |
| 19 | -| AT Protocol role | AppView for `at.glean.*` lexicons | | 19 | +| AT Protocol role | AppView for `at.glean.*` lexicons | |
| 20 | -| Data source | AT Protocol Jetstream → SQLite index | | 20 | +| Data source | AT Protocol Jetstream → SQLite index | |
| 21 | 21 | ||
| 22 | ## 3. AT Protocol Lexicons | 22 | ## 3. AT Protocol Lexicons |
| 23 | 23 | ||
| @@ -137,7 +137,26 @@ The mapping from margin note to glean annotation: | |||
| 137 | 137 | ||
| 138 | When no matching article exists in the local DB, the annotation is stored with an empty `feed_url`. Margin notes are indexed from both Jetstream and PDS sync, same as glean records. | 138 | When no matching article exists in the local DB, the annotation is stored with an empty `feed_url`. Margin notes are indexed from both Jetstream and PDS sync, same as glean records. |
| 139 | 139 | ||
| 140 | -### 3.5 AppView Query Lexicons | 140 | +### 3.5 `app.bsky.graph.follow` (External) |
| 141 | + | ||
| 142 | +Follow relationships are tracked from Bluesky and Tangled follow records. The `FollowRecord` struct is validated against the lexicon at `lexicons/app/bsky/graph/follow.json`. The optional `via` field (a strong ref) is preserved as raw JSON but not used by Glean. | ||
| 143 | + | ||
| 144 | +### 3.6 Lexicon Constants | ||
| 145 | + | ||
| 146 | +All collection NSIDs are defined as constants in `lexicon.go` and used throughout the codebase: | ||
| 147 | + | ||
| 148 | +```go | ||
| 149 | +const ( | ||
| 150 | + CollectionSubscription = "at.glean.subscription" | ||
| 151 | + CollectionAnnotation = "at.glean.annotation" | ||
| 152 | + CollectionLike = "at.glean.like" | ||
| 153 | + CollectionMarginNote = "at.margin.note" | ||
| 154 | + CollectionBskyFollow = "app.bsky.graph.follow" | ||
| 155 | + CollectionTangledFollow = "sh.tangled.graph.follow" | ||
| 156 | +) | ||
| 157 | +``` | ||
| 158 | + | ||
| 159 | +### 3.7 AppView Query Lexicons | ||
| 141 | 160 | ||
| 142 | As an AppView, Glean serves the following XRPC query endpoints. Other AT Protocol applications can call these to access indexed `at.glean.*` data without implementing their own indexer. | 161 | As an AppView, Glean serves the following XRPC query endpoints. Other AT Protocol applications can call these to access indexed `at.glean.*` data without implementing their own indexer. |
| 143 | 162 | ||
| @@ -234,9 +253,9 @@ Output: | |||
| 234 | people: [{ did, handle, displayName, avatar, jaccard, commonFeeds }] | 253 | people: [{ did, handle, displayName, avatar, jaccard, commonFeeds }] |
| 235 | ``` | 254 | ``` |
| 236 | 255 | ||
| 237 | -### 3.6 AppView Jetstream Consumption | 256 | +### 3.8 AppView Jetstream Consumption |
| 238 | 257 | ||
| 239 | -Glean subscribes to a Jetstream endpoint (`GLEAN_JETSTREAM`, default `wss://jetstream2.fr.hose.cam`) for all `at.glean.*` records: | 258 | +Glean subscribes to a Jetstream endpoint (`GLEAN_JETSTREAM`, default `wss://jetstream.glean.at`) for all `at.glean.*` records: |
| 240 | 259 | ||
| 241 | ``` | 260 | ``` |
| 242 | SUBSCRIBE collections: ["at.glean.subscription", "at.glean.annotation", "at.glean.like", "app.bsky.graph.follow", "sh.tangled.graph.follow", "at.margin.note"] | 261 | SUBSCRIBE collections: ["at.glean.subscription", "at.glean.annotation", "at.glean.like", "app.bsky.graph.follow", "sh.tangled.graph.follow", "at.margin.note"] |
| @@ -774,7 +793,8 @@ glean/ | |||
| 774 | ├── lexicons/ | 793 | ├── lexicons/ |
| 775 | │ └── at/ | 794 | │ └── at/ |
| 776 | │ ├── glean/ # Glean lexicon JSON schemas (subscription, annotation, like) | 795 | │ ├── glean/ # Glean lexicon JSON schemas (subscription, annotation, like) |
| 777 | -│ └── margin/ # External lexicon schemas (note.json for at.margin.note) | 796 | +│ └── margin/ # External: at.margin.note W3C Web Annotation schema |
| 797 | +│ └── app/bsky/graph/ # External: app.bsky.graph.follow schema | ||
| 778 | ├── internal/ | 798 | ├── internal/ |
| 779 | │ ├── atproto/ | 799 | │ ├── atproto/ |
| 780 | │ │ ├── auth.go # DID resolution, OAuth flow | 800 | │ │ ├── auth.go # DID resolution, OAuth flow |
| @@ -846,6 +866,8 @@ glean/ | |||
| 846 | 866 | ||
| 847 | ## 10. Auth Flow | 867 | ## 10. Auth Flow |
| 848 | 868 | ||
| 869 | +DID resolution uses a configurable PLC directory (`GLEAN_PLC_URL`, defaults to `https://didplc.glean.at`). The identity directory is initialized once at startup via `InitIdentity()` with a caching layer (250k entries, 24h TTL). | ||
| 870 | + | ||
| 849 | 1. User visits `/`, clicks "Sign in with Bluesky" (or any AT Proto PDS) | 871 | 1. User visits `/`, clicks "Sign in with Bluesky" (or any AT Proto PDS) |
| 850 | 2. Server redirects to AT Protocol OAuth authorization endpoint | 872 | 2. Server redirects to AT Protocol OAuth authorization endpoint |
| 851 | 3. User authorizes on their PDS | 873 | 3. User authorizes on their PDS |
modified
internal/atproto/auth.go +47 -6 | @@ -3,6 +3,10 @@ package atproto | ||
| 3 | 3 | import ( |
| 4 | 4 | "context" |
| 5 | 5 | "fmt" |
| 6 | + "net" | |
| 7 | + "net/http" | |
| 8 | + "os" | |
| 9 | + "time" | |
| 6 | 10 | |
| 7 | 11 | "github.com/bluesky-social/indigo/atproto/identity" |
| 8 | 12 | "github.com/bluesky-social/indigo/atproto/syntax" |
| @@ -13,14 +17,53 @@ type ( | ||
| 13 | 17 | Identity = identity.Identity |
| 14 | 18 | ) |
| 15 | 19 | |
| 20 | +var directory identity.Directory | |
| 21 | + | |
| 22 | +func InitIdentity(plcURL string) { | |
| 23 | + if plcURL == "" { | |
| 24 | + plcURL = identity.DefaultPLCURL | |
| 25 | + } | |
| 26 | + base := identity.BaseDirectory{ | |
| 27 | + PLCURL: plcURL, | |
| 28 | + HTTPClient: http.Client{ | |
| 29 | + Timeout: 10 * time.Second, | |
| 30 | + Transport: &http.Transport{ | |
| 31 | + Proxy: http.ProxyFromEnvironment, | |
| 32 | + IdleConnTimeout: 1000 * time.Millisecond, | |
| 33 | + MaxIdleConns: 100, | |
| 34 | + }, | |
| 35 | + }, | |
| 36 | + Resolver: net.Resolver{ | |
| 37 | + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { | |
| 38 | + d := net.Dialer{Timeout: 3 * time.Second} | |
| 39 | + return d.DialContext(ctx, network, address) | |
| 40 | + }, | |
| 41 | + }, | |
| 42 | + TryAuthoritativeDNS: true, | |
| 43 | + SkipDNSDomainSuffixes: []string{".bsky.social"}, | |
| 44 | + UserAgent: "glean/1.0", | |
| 45 | + } | |
| 46 | + directory = identity.NewCacheDirectory(&base, 250_000, 24*time.Hour, 2*time.Minute, 5*time.Minute) | |
| 47 | +} | |
| 48 | + | |
| 49 | +func dir() identity.Directory { | |
| 50 | + if directory == nil { | |
| 51 | + plcURL := os.Getenv("GLEAN_PLC_URL") | |
| 52 | + if plcURL == "" { | |
| 53 | + plcURL = identity.DefaultPLCURL | |
| 54 | + } | |
| 55 | + InitIdentity(plcURL) | |
| 56 | + } | |
| 57 | + return directory | |
| 58 | +} | |
| 59 | + | |
| 16 | 60 | func ResolveHandle(ctx context.Context, handle string) (string, error) { |
| 17 | 61 | h, err := syntax.ParseHandle(handle) |
| 18 | 62 | if err != nil { |
| 19 | 63 | return "", fmt.Errorf("parsing handle: %w", err) |
| 20 | 64 | } |
| 21 | 65 | |
| 22 | - dir := identity.DefaultDirectory() | |
| 23 | - ident, err := dir.LookupHandle(ctx, h) | |
| 66 | + ident, err := dir().LookupHandle(ctx, h) | |
| 24 | 67 | if err != nil { |
| 25 | 68 | return "", fmt.Errorf("resolving handle: %w", err) |
| 26 | 69 | } |
| @@ -34,8 +77,7 @@ func ResolveDID(ctx context.Context, did string) (*DIDDocument, error) { | ||
| 34 | 77 | return nil, fmt.Errorf("parsing DID: %w", err) |
| 35 | 78 | } |
| 36 | 79 | |
| 37 | - dir := identity.DefaultDirectory() | |
| 38 | - ident, err := dir.LookupDID(ctx, d) | |
| 80 | + ident, err := dir().LookupDID(ctx, d) | |
| 39 | 81 | if err != nil { |
| 40 | 82 | return nil, fmt.Errorf("resolving DID: %w", err) |
| 41 | 83 | } |
| @@ -50,8 +92,7 @@ func ResolveIdentity(ctx context.Context, identifier string) (*Identity, error) | ||
| 50 | 92 | return nil, fmt.Errorf("parsing identifier: %w", err) |
| 51 | 93 | } |
| 52 | 94 | |
| 53 | - dir := identity.DefaultDirectory() | |
| 54 | - ident, err := dir.Lookup(ctx, atid) | |
| 95 | + ident, err := dir().Lookup(ctx, atid) | |
| 55 | 96 | if err != nil { |
| 56 | 97 | return nil, fmt.Errorf("resolving identity: %w", err) |
| 57 | 98 | } |
| @@ -3,6 +3,10 @@ package atproto | |||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| 5 | "fmt" | 5 | "fmt" |
| 6 | + "net" | ||
| 7 | + "net/http" | ||
| 8 | + "os" | ||
| 9 | + "time" | ||
| 6 | 10 | ||
| 7 | "github.com/bluesky-social/indigo/atproto/identity" | 11 | "github.com/bluesky-social/indigo/atproto/identity" |
| 8 | "github.com/bluesky-social/indigo/atproto/syntax" | 12 | "github.com/bluesky-social/indigo/atproto/syntax" |
| @@ -13,14 +17,53 @@ type ( | |||
| 13 | Identity = identity.Identity | 17 | Identity = identity.Identity |
| 14 | ) | 18 | ) |
| 15 | 19 | ||
| 20 | +var directory identity.Directory | ||
| 21 | + | ||
| 22 | +func InitIdentity(plcURL string) { | ||
| 23 | + if plcURL == "" { | ||
| 24 | + plcURL = identity.DefaultPLCURL | ||
| 25 | + } | ||
| 26 | + base := identity.BaseDirectory{ | ||
| 27 | + PLCURL: plcURL, | ||
| 28 | + HTTPClient: http.Client{ | ||
| 29 | + Timeout: 10 * time.Second, | ||
| 30 | + Transport: &http.Transport{ | ||
| 31 | + Proxy: http.ProxyFromEnvironment, | ||
| 32 | + IdleConnTimeout: 1000 * time.Millisecond, | ||
| 33 | + MaxIdleConns: 100, | ||
| 34 | + }, | ||
| 35 | + }, | ||
| 36 | + Resolver: net.Resolver{ | ||
| 37 | + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { | ||
| 38 | + d := net.Dialer{Timeout: 3 * time.Second} | ||
| 39 | + return d.DialContext(ctx, network, address) | ||
| 40 | + }, | ||
| 41 | + }, | ||
| 42 | + TryAuthoritativeDNS: true, | ||
| 43 | + SkipDNSDomainSuffixes: []string{".bsky.social"}, | ||
| 44 | + UserAgent: "glean/1.0", | ||
| 45 | + } | ||
| 46 | + directory = identity.NewCacheDirectory(&base, 250_000, 24*time.Hour, 2*time.Minute, 5*time.Minute) | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +func dir() identity.Directory { | ||
| 50 | + if directory == nil { | ||
| 51 | + plcURL := os.Getenv("GLEAN_PLC_URL") | ||
| 52 | + if plcURL == "" { | ||
| 53 | + plcURL = identity.DefaultPLCURL | ||
| 54 | + } | ||
| 55 | + InitIdentity(plcURL) | ||
| 56 | + } | ||
| 57 | + return directory | ||
| 58 | +} | ||
| 59 | + | ||
| 16 | func ResolveHandle(ctx context.Context, handle string) (string, error) { | 60 | func ResolveHandle(ctx context.Context, handle string) (string, error) { |
| 17 | h, err := syntax.ParseHandle(handle) | 61 | h, err := syntax.ParseHandle(handle) |
| 18 | if err != nil { | 62 | if err != nil { |
| 19 | return "", fmt.Errorf("parsing handle: %w", err) | 63 | return "", fmt.Errorf("parsing handle: %w", err) |
| 20 | } | 64 | } |
| 21 | 65 | ||
| 22 | - dir := identity.DefaultDirectory() | 66 | + ident, err := dir().LookupHandle(ctx, h) |
| 23 | - ident, err := dir.LookupHandle(ctx, h) | ||
| 24 | if err != nil { | 67 | if err != nil { |
| 25 | return "", fmt.Errorf("resolving handle: %w", err) | 68 | return "", fmt.Errorf("resolving handle: %w", err) |
| 26 | } | 69 | } |
| @@ -34,8 +77,7 @@ func ResolveDID(ctx context.Context, did string) (*DIDDocument, error) { | |||
| 34 | return nil, fmt.Errorf("parsing DID: %w", err) | 77 | return nil, fmt.Errorf("parsing DID: %w", err) |
| 35 | } | 78 | } |
| 36 | 79 | ||
| 37 | - dir := identity.DefaultDirectory() | 80 | + ident, err := dir().LookupDID(ctx, d) |
| 38 | - ident, err := dir.LookupDID(ctx, d) | ||
| 39 | if err != nil { | 81 | if err != nil { |
| 40 | return nil, fmt.Errorf("resolving DID: %w", err) | 82 | return nil, fmt.Errorf("resolving DID: %w", err) |
| 41 | } | 83 | } |
| @@ -50,8 +92,7 @@ func ResolveIdentity(ctx context.Context, identifier string) (*Identity, error) | |||
| 50 | return nil, fmt.Errorf("parsing identifier: %w", err) | 92 | return nil, fmt.Errorf("parsing identifier: %w", err) |
| 51 | } | 93 | } |
| 52 | 94 | ||
| 53 | - dir := identity.DefaultDirectory() | 95 | + ident, err := dir().Lookup(ctx, atid) |
| 54 | - ident, err := dir.Lookup(ctx, atid) | ||
| 55 | if err != nil { | 96 | if err != nil { |
| 56 | return nil, fmt.Errorf("resolving identity: %w", err) | 97 | return nil, fmt.Errorf("resolving identity: %w", err) |
| 57 | } | 98 | } |
modified
main.go +3 -1 | @@ -21,11 +21,13 @@ import ( | ||
| 21 | 21 | func main() { |
| 22 | 22 | addr := flag.String("addr", envOr("GLEAN_ADDR", ":8080"), "listen address") |
| 23 | 23 | dbPath := flag.String("db", envOr("GLEAN_DB", "glean.db"), "database path") |
| 24 | - jetstreamURL := flag.String("jetstream", envOr("GLEAN_JETSTREAM", "wss://jetstream2.fr.hose.cam"), "Jetstream URL") | |
| 24 | + jetstreamURL := flag.String("jetstream", envOr("GLEAN_JETSTREAM", "wss://jetstream.glean.at"), "Jetstream URL") | |
| 25 | 25 | syncInterval := flag.Duration("sync-interval", envDuration("GLEAN_SYNC_INTERVAL", 1*time.Hour), "PDS sync interval") |
| 26 | 26 | clusterInterval := flag.Duration("cluster-interval", envDuration("GLEAN_CLUSTER_INTERVAL", 6*time.Hour), "cluster recomputation interval") |
| 27 | 27 | flag.Parse() |
| 28 | 28 | |
| 29 | + atproto.InitIdentity(envOr("GLEAN_PLC_URL", "")) | |
| 30 | + | |
| 29 | 31 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) |
| 30 | 32 | |
| 31 | 33 | database, err := db.Open(*dbPath) |
| @@ -21,11 +21,13 @@ import ( | |||
| 21 | func main() { | 21 | func main() { |
| 22 | addr := flag.String("addr", envOr("GLEAN_ADDR", ":8080"), "listen address") | 22 | addr := flag.String("addr", envOr("GLEAN_ADDR", ":8080"), "listen address") |
| 23 | dbPath := flag.String("db", envOr("GLEAN_DB", "glean.db"), "database path") | 23 | dbPath := flag.String("db", envOr("GLEAN_DB", "glean.db"), "database path") |
| 24 | - jetstreamURL := flag.String("jetstream", envOr("GLEAN_JETSTREAM", "wss://jetstream2.fr.hose.cam"), "Jetstream URL") | 24 | + jetstreamURL := flag.String("jetstream", envOr("GLEAN_JETSTREAM", "wss://jetstream.glean.at"), "Jetstream URL") |
| 25 | syncInterval := flag.Duration("sync-interval", envDuration("GLEAN_SYNC_INTERVAL", 1*time.Hour), "PDS sync interval") | 25 | syncInterval := flag.Duration("sync-interval", envDuration("GLEAN_SYNC_INTERVAL", 1*time.Hour), "PDS sync interval") |
| 26 | clusterInterval := flag.Duration("cluster-interval", envDuration("GLEAN_CLUSTER_INTERVAL", 6*time.Hour), "cluster recomputation interval") | 26 | clusterInterval := flag.Duration("cluster-interval", envDuration("GLEAN_CLUSTER_INTERVAL", 6*time.Hour), "cluster recomputation interval") |
| 27 | flag.Parse() | 27 | flag.Parse() |
| 28 | 28 | ||
| 29 | + atproto.InitIdentity(envOr("GLEAN_PLC_URL", "")) | ||
| 30 | + | ||
| 29 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) | 31 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) |
| 30 | 32 | ||
| 31 | database, err := db.Open(*dbPath) | 33 | database, err := db.Open(*dbPath) |
modified
readme.md +11 -9 | @@ -10,6 +10,7 @@ Your subscriptions live as records on your PDS. You own them. If Glean goes away | ||
| 10 | 10 | |
| 11 | 11 | - RSS, Atom, and JSON Feed support with a keyboard-driven reading interface |
| 12 | 12 | - Highlights, notes, tags, and ratings on articles |
| 13 | +- [margin.at](https://margin.at) annotations displayed alongside glean annotations | |
| 13 | 14 | - A trending page showing what's popular across all users |
| 14 | 15 | - Feed and people recommendations based on reading overlap |
| 15 | 16 | - OPML import and export |
| @@ -36,15 +37,16 @@ Then open `http://localhost:8080`. | ||
| 36 | 37 | |
| 37 | 38 | ## Configuration |
| 38 | 39 | |
| 39 | -| Variable | Default | What it does | | |
| 40 | -| -------------------------- | ------------------------------ | --------------------------------------------------------- | | |
| 41 | -| `GLEAN_ADDR` | `:8080` | Listen address | | |
| 42 | -| `GLEAN_DB` | `glean.db` | SQLite database path | | |
| 43 | -| `GLEAN_JETSTREAM` | `wss://jetstream2.fr.hose.cam` | Jetstream WebSocket URL | | |
| 44 | -| `GLEAN_SYNC_INTERVAL` | `1h` | PDS sync interval (Go duration: `30m`, `2h30m`, etc.) | | |
| 45 | -| `GLEAN_CLUSTER_INTERVAL` | `6h` | Cluster recomputation interval (Go duration) | | |
| 46 | -| `GLEAN_OAUTH_CLIENT_ID` | _(empty)_ | OAuth client metadata URL (leave empty for localhost dev) | | |
| 47 | -| `GLEAN_OAUTH_REDIRECT_URL` | _(empty)_ | OAuth redirect URL (leave empty for localhost dev) | | |
| 40 | +| Variable | Default | What it does | | |
| 41 | +| -------------------------- | -------------------------- | --------------------------------------------------------- | | |
| 42 | +| `GLEAN_ADDR` | `:8080` | Listen address | | |
| 43 | +| `GLEAN_DB` | `glean.db` | SQLite database path | | |
| 44 | +| `GLEAN_JETSTREAM` | `wss://jetstream.glean.at` | Jetstream WebSocket URL | | |
| 45 | +| `GLEAN_SYNC_INTERVAL` | `1h` | PDS sync interval (Go duration: `30m`, `2h30m`, etc.) | | |
| 46 | +| `GLEAN_CLUSTER_INTERVAL` | `6h` | Cluster recomputation interval (Go duration) | | |
| 47 | +| `GLEAN_PLC_URL` | `https://plc.directory` | PLC directory URL for DID resolution | | |
| 48 | +| `GLEAN_OAUTH_CLIENT_ID` | _(empty)_ | OAuth client metadata URL (leave empty for localhost dev) | | |
| 49 | +| `GLEAN_OAUTH_REDIRECT_URL` | _(empty)_ | OAuth redirect URL (leave empty for localhost dev) | | |
| 48 | 50 | |
| 49 | 51 | For production: |
| 50 | 52 | |
| @@ -10,6 +10,7 @@ Your subscriptions live as records on your PDS. You own them. If Glean goes away | |||
| 10 | 10 | ||
| 11 | - RSS, Atom, and JSON Feed support with a keyboard-driven reading interface | 11 | - RSS, Atom, and JSON Feed support with a keyboard-driven reading interface |
| 12 | - Highlights, notes, tags, and ratings on articles | 12 | - Highlights, notes, tags, and ratings on articles |
| 13 | +- [margin.at](https://margin.at) annotations displayed alongside glean annotations | ||
| 13 | - A trending page showing what's popular across all users | 14 | - A trending page showing what's popular across all users |
| 14 | - Feed and people recommendations based on reading overlap | 15 | - Feed and people recommendations based on reading overlap |
| 15 | - OPML import and export | 16 | - OPML import and export |
| @@ -36,15 +37,16 @@ Then open `http://localhost:8080`. | |||
| 36 | 37 | ||
| 37 | ## Configuration | 38 | ## Configuration |
| 38 | 39 | ||
| 39 | -| Variable | Default | What it does | | 40 | +| Variable | Default | What it does | |
| 40 | -| -------------------------- | ------------------------------ | --------------------------------------------------------- | | 41 | +| -------------------------- | -------------------------- | --------------------------------------------------------- | |
| 41 | -| `GLEAN_ADDR` | `:8080` | Listen address | | 42 | +| `GLEAN_ADDR` | `:8080` | Listen address | |
| 42 | -| `GLEAN_DB` | `glean.db` | SQLite database path | | 43 | +| `GLEAN_DB` | `glean.db` | SQLite database path | |
| 43 | -| `GLEAN_JETSTREAM` | `wss://jetstream2.fr.hose.cam` | Jetstream WebSocket URL | | 44 | +| `GLEAN_JETSTREAM` | `wss://jetstream.glean.at` | Jetstream WebSocket URL | |
| 44 | -| `GLEAN_SYNC_INTERVAL` | `1h` | PDS sync interval (Go duration: `30m`, `2h30m`, etc.) | | 45 | +| `GLEAN_SYNC_INTERVAL` | `1h` | PDS sync interval (Go duration: `30m`, `2h30m`, etc.) | |
| 45 | -| `GLEAN_CLUSTER_INTERVAL` | `6h` | Cluster recomputation interval (Go duration) | | 46 | +| `GLEAN_CLUSTER_INTERVAL` | `6h` | Cluster recomputation interval (Go duration) | |
| 46 | -| `GLEAN_OAUTH_CLIENT_ID` | _(empty)_ | OAuth client metadata URL (leave empty for localhost dev) | | 47 | +| `GLEAN_PLC_URL` | `https://plc.directory` | PLC directory URL for DID resolution | |
| 47 | -| `GLEAN_OAUTH_REDIRECT_URL` | _(empty)_ | OAuth redirect URL (leave empty for localhost dev) | | 48 | +| `GLEAN_OAUTH_CLIENT_ID` | _(empty)_ | OAuth client metadata URL (leave empty for localhost dev) | |
| 49 | +| `GLEAN_OAUTH_REDIRECT_URL` | _(empty)_ | OAuth redirect URL (leave empty for localhost dev) | | ||
| 48 | 50 | ||
| 49 | For production: | 51 | For production: |
| 50 | 52 | ||