Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ logjam.exe
logjam
web-app/node_modules
web-app/dist
go_build_github_com_reiver_logjam
10 changes: 7 additions & 3 deletions cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import (
)

var config libcfg.Model = libcfg.Model{
GoldGorillaBaseURL:flg.GoldGorillaBaseURL,
ProdMode:flg.ProdMode,
WebServerTCPAddress:flg.WebServerTCPAddress,
GoldGorillaBaseURL: flg.GoldGorillaBaseURL,
ProdMode: flg.ProdMode,
WebServerTCPAddress: flg.WebServerTCPAddress,
BlueSkyBaseURL: flg.BlueSkyBaseURL,
NeynarApiKey: flg.NeynarApiKey,
PocketBaseURL: flg.PocketBaseURL,
PocketBaseAuthToken: flg.PocketBaseAuthToken,
}

var Config libcfg.Configurer = libcfg.Wrap(config)
18 changes: 14 additions & 4 deletions flg/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import (
)

var (
GoldGorillaBaseURL string
help bool
PocketBaseURL string
ProdMode bool
GoldGorillaBaseURL string
BlueSkyBaseURL string
NeynarApiKey string
help bool
PocketBaseURL string
PocketBaseAuthToken string
ProdMode bool
WebServerTCPAddress string
)

Expand All @@ -26,8 +29,11 @@ func init() {
var defaultSrc string = fmt.Sprintf(":%s", env.TcpPort)

flag.StringVar(&GoldGorillaBaseURL, "goldgorilla-svc-addr", "http://localhost:8080", "goldgorilla service address baseurl")
flag.StringVar(&BlueSkyBaseURL, "bluesky-base-url", "https://bsky.social", "bluesky node baseurl")
flag.StringVar(&NeynarApiKey, "neynar-api-key", "", "neynar api key")
flag.BoolVar(&help, "h", false, "print help")
flag.StringVar(&PocketBaseURL, pocketBaseURLFlag, env.PocketBaseURL, "pocketbase base API URL")
flag.StringVar(&PocketBaseAuthToken, "pb-auth-token", "", "pocketbase auth token")
flag.BoolVar(&ProdMode, "prod", false, "enable production mode ( its in dev mode by default )")
flag.StringVar(&WebServerTCPAddress, "src", defaultSrc, "source listen address")

Expand All @@ -38,6 +44,10 @@ func init() {
os.Exit(0)
}

if "" == NeynarApiKey {
panic("Neynar Api key cannot be empty.")
}

if "" == PocketBaseURL {
var err error = erorr.Errorf("PocketBase URL cannot be empty. Must be set with %q environment-variable or --%s command-line flag / switch.", env.PocketBaseURLEnvVar, pocketBaseURLFlag)
panic(err)
Expand Down
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/reiver/logjam

go 1.23.4

require github.com/gorilla/websocket v1.5.0
require github.com/gorilla/websocket v1.5.1

require (
github.com/gorilla/mux v1.8.0
Expand All @@ -16,6 +16,11 @@ require (
)

require (
github.com/bluesky-social/indigo v0.0.0-20250408072029-63f3f48e1e16 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/reiver/go-json v0.0.0-20241213105958-0fd2a5bb4dd8 // indirect
github.com/reiver/go-lck v0.0.0-20240808133902-b56df221c39f // indirect
golang.org/x/net v0.23.0 // indirect
)
13 changes: 11 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
github.com/bluesky-social/indigo v0.0.0-20250408072029-63f3f48e1e16/go.mod h1:yjdhLA1LkK8VDS/WPUoYPo25/Hq/8rX38Ftr67EsqKY=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/reiver/go-erorr v0.0.0-20240801233437-8cbde6d1fa3f h1:D1QSxKHm8U73XhjsW3SFLkT0zT5pKJi+1KGboMhY1Rk=
github.com/reiver/go-erorr v0.0.0-20240801233437-8cbde6d1fa3f/go.mod h1:F0HbBf+Ak2ZlE8YkDW4Y+KxaUmT0KaaIJK6CXY3cJxE=
github.com/reiver/go-etag v0.0.0-20241130123934-1a98d29fda6e h1:2FJeuK9Uc6BGLnXz4QOyfPefILfXGoPxiyCpgwz0WMU=
Expand All @@ -16,3 +23,5 @@ github.com/reiver/go-opt v0.0.0-20240809035328-1ff08dec9bc4 h1:KQ/PDBRaJb69hmMmO
github.com/reiver/go-opt v0.0.0-20240809035328-1ff08dec9bc4/go.mod h1:3lRqhDIwZ4OaMNN6Z5rmHCX8up18IkLGuw2rxP9GrYo=
github.com/reiver/go-path v0.0.0-20240327181650-5f2ee05890d8 h1:8JFCnRV4GVjP8XUFuXvCH42o4fXHSkGcUkfsloB+LeY=
github.com/reiver/go-path v0.0.0-20240327181650-5f2ee05890d8/go.mod h1:4H3fxXTRSuvWviraF1tFTVXhRciV8KFGq5tzo1gb2Tk=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
195 changes: 195 additions & 0 deletions lib/bluesky/bluesky_http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package bluesky

import (
"bytes"
"encoding/json"
"errors"
"fmt"
cErrors "github.com/reiver/logjam/lib/errors"
"github.com/reiver/logjam/lib/marshal"
dbsrv "github.com/reiver/logjam/srv/db"
"io"
"net/http"
"sync"
"time"
)

type httpRepository struct {
client *http.Client
svcAddr string
*sync.Mutex
}

const (
sessionsTable = "blueSkySessions"

//keys
accessTokenKey = "accessJwt"
refreshTokenKey = "refreshJwt"
handleKey = "handle"
didKey = "did"
)

func NewHTTPRepository(svcAddr string) IBlueSkyServiceRepository {
return &httpRepository{
client: &http.Client{
Timeout: 8 * time.Second,
},
svcAddr: svcAddr,
Mutex: &sync.Mutex{},
}
}

func (repo *httpRepository) RefreshTokens(did string, accessKey, refreshKey string) (AK, error) {
url := fmt.Sprintf("%s/xrpc/com.atproto.server.refreshSession", repo.svcAddr)

req, err := http.NewRequest("POST", url, nil)
if err != nil {
return AK{}, cErrors.NewErrorFromErr(http.StatusInternalServerError, fmt.Errorf("error creating HTTP request: %w", err))
}
client := &http.Client{Timeout: 16 * time.Second}

filters := map[string]any{didKey: did}
if len(accessKey) > 0 && len(refreshKey) > 0 {
filters[accessTokenKey] = accessKey
filters[refreshTokenKey] = refreshKey
}
rows, err := dbsrv.Repository.GetByFilter(sessionsTable, filters)
if err != nil {
return AK{}, err
}
if len(rows) == 0 {
return AK{}, cErrors.NewErrorWithMsg(http.StatusNotFound, "couldnt find tokens for this account did/at/rt")
}
var ak AK
err = marshal.MapToObj(rows[0], &ak)
if err != nil {
return AK{}, nil
}
req.Header.Set("Authorization", "Bearer "+ak.RefreshToken)
resp, err := client.Do(req)
if err != nil {
return AK{}, cErrors.NewErrorFromErr(http.StatusInternalServerError, fmt.Errorf("error sending HTTP request: %w", err))
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return AK{}, cErrors.NewErrorFromErr(http.StatusInternalServerError, fmt.Errorf("refresh token request failed with status %d: %s", resp.StatusCode, string(body)))
}

// Parse the response.
var res struct {
DID string `json:"did"`
DidDoc map[string]any `json:"didDoc"`
Handle string `json:"handle"`
AccessJWT string `json:"accessJwt"`
RefreshJWT string `json:"refreshJwt"`
Active bool `json:"active"`
}

if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return AK{}, cErrors.NewErrorFromErr(http.StatusInternalServerError, fmt.Errorf("error decoding refresh response: %w", err))
}

// Verify the response contains new tokens.
if res.AccessJWT == "" || res.RefreshJWT == "" {
return AK{}, cErrors.NewErrorFromErr(http.StatusInternalServerError, errors.New("invalid refresh response: missing tokens"))
}

ak = AK{
AccessToken: res.AccessJWT,
RefreshToken: res.RefreshJWT,
Handle: res.Handle,
DID: res.DID,
}
err = dbsrv.Repository.UpdateByFilter(sessionsTable, map[string]any{didKey: ak.DID}, map[string]any{
accessTokenKey: res.AccessJWT,
refreshTokenKey: res.RefreshJWT,
})
return ak, err
}

func (repo *httpRepository) getAK(did string) (*AK, error) {
recs, err := dbsrv.Repository.GetByFilter(sessionsTable, map[string]any{didKey: did})
if err != nil {
return nil, err
}
if len(recs) == 0 {
return nil, nil
}
r0 := recs[0]
return &AK{
AccessToken: r0[accessTokenKey].(string),
RefreshToken: r0[refreshTokenKey].(string),
Handle: r0[handleKey].(string),
DID: r0[didKey].(string),
}, nil
}

func (repo *httpRepository) AccountExists(did string) (bool, error) {
rows, err := dbsrv.Repository.GetByFilter(sessionsTable, map[string]any{
didKey: did,
})
if err != nil {
return false, err
}
if rows != nil && len(rows) == 0 {
return false, nil
}
return true, nil
}

// CreatePost creates a new post by calling the create post endpoint.
func (repo *httpRepository) CreatePost(did, text string) error {
url := fmt.Sprintf("%s/xrpc/com.atproto.repo.createRecord", repo.svcAddr)
ak, err := repo.getAK(did)
if err != nil {
return err
}
if ak == nil {
return cErrors.NewErrorFromErr(http.StatusUnauthorized, errors.New("couldn't find account keys, maybe not submitted yet"))
}
post := TextPostRecord{
Type: "app.bsky.feed.post",
Text: text,
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
payload := struct {
Repo string `json:"repo"`
Collection string `json:"collection"`
Record interface{} `json:"record"`
}{
Repo: ak.DID,
Collection: "app.bsky.feed.post",
Record: post,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return cErrors.NewErrorFromErr(http.StatusInternalServerError, fmt.Errorf("error marshaling payload: %w", err))
}

// Prepare the request.
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes))
if err != nil {
return cErrors.NewErrorFromErr(http.StatusInternalServerError, fmt.Errorf("error creating HTTP request: %w", err))
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+ak.AccessToken)

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return cErrors.NewErrorFromErr(http.StatusInternalServerError, fmt.Errorf("error sending HTTP request: %w", err))
}
defer resp.Body.Close()

// Check for non-200 status codes.
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return cErrors.NewErrorFromErr(resp.StatusCode, fmt.Errorf("create post failed (status %d): %s", resp.StatusCode, string(body)))
}

// Optionally, decode and use the response data here.
return nil
}
20 changes: 20 additions & 0 deletions lib/bluesky/repositories.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package bluesky

// AK Access Keys
type AK struct {
AccessToken string `json:"accessJwt"`
RefreshToken string `json:"refreshJwt"`
Handle string `json:"handle"`
DID string `json:"did"`
}
type TextPostRecord struct {
Type string `json:"$type"`
Text string `json:"text"`
CreatedAt string `json:"createdAt"`
}

type IBlueSkyServiceRepository interface {
RefreshTokens(did string, accessKey, refreshKey string) (AK, error)
CreatePost(did, text string) error
AccountExists(did string) (bool, error)
}
3 changes: 3 additions & 0 deletions lib/cfg/configurer.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package cfg

type Configurer interface {
NeynarApiKey() string
BlueSkyBaseURL() string
GoldGorillaBaseURL() string
PocketBaseURL() string
PocketBaseAuthToken() string
ProdMode() bool
WebServerTCPAddress() string
}
5 changes: 4 additions & 1 deletion lib/cfg/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package cfg

type Model struct {
GoldGorillaBaseURL string
PocketBaseURL string
BlueSkyBaseURL string
NeynarApiKey string
PocketBaseURL string
PocketBaseAuthToken string
ProdMode bool
WebServerTCPAddress string
}
10 changes: 10 additions & 0 deletions lib/cfg/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,20 @@ func (receiver wrapper) GoldGorillaBaseURL() string {
return receiver.internal.GoldGorillaBaseURL
}

func (receiver wrapper) NeynarApiKey() string {
return receiver.internal.NeynarApiKey
}

func (receiver wrapper) BlueSkyBaseURL() string { return receiver.internal.BlueSkyBaseURL }

func (receiver wrapper) PocketBaseURL() string {
return receiver.internal.PocketBaseURL
}

func (receiver wrapper) PocketBaseAuthToken() string {
return receiver.internal.PocketBaseAuthToken
}

func (receiver wrapper) ProdMode() bool {
return receiver.internal.ProdMode
}
Expand Down
Loading