tiny 88x31 lexicon for atproto
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial commit

rachel-mp4 fb67f1d6

+2130
+15
.env
··· 1 + CLIENT_SECRET_KEY=KEY_HERE 2 + CLIENT_SECRET_KEY_ID=KEY_ID_HERE 3 + MY_IDENTITY=MY_ATP_HANDLE_HERE 4 + MY_METADATA_PATH=MY_METADATA_PATH_HERE 5 + MY_NAME=MY_NAME_HERE 6 + MY_LOGO_PATH=MY_LOGO_PATH_HERE 7 + MY_TOS_PATH=MY_TOS_PATH_HERE 8 + MY_POLICY_PATH=MY_POLICY_PATH_HERE 9 + MY_OAUTH_CALLBACK=MY_OAUTH_CALLBACK_HERE 10 + MY_JWKS_PATH=MY_JWKS_PATH_HERE 11 + POSTGRES_USER=POSTGRES_USER_HERE 12 + POSTGRES_PASSWORD=POSTGRES_PASSWORD_HERE 13 + POSTGRES_PORT=POSTGRES_PORT_HERE 14 + POSTGRES_DB=POSTGRES_DB_HERE 15 + SESSION_KEY=SESSION_KEY_HERE
+103
blobs/blobs.go
··· 1 + package blobs 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "github.com/bluesky-social/indigo/api/atproto" 9 + "github.com/bluesky-social/indigo/atproto/atclient" 10 + "io" 11 + "net/http" 12 + "os" 13 + "strings" 14 + ) 15 + 16 + func AddImageToCache(did string, cid string, ctx context.Context) (string, error) { 17 + uploadDir := "./uploads" 18 + _, err := os.Stat(uploadDir) 19 + if os.IsNotExist(err) { 20 + os.Mkdir(uploadDir, 0755) 21 + } 22 + 23 + imgPath := fmt.Sprintf("%s/%s%s", uploadDir, did, cid) 24 + _, err = os.Stat(imgPath) 25 + if err != nil { 26 + blob, err := SyncGetBlob(did, cid, ctx) 27 + if err != nil { 28 + return "", err 29 + } 30 + file, err := os.Create(imgPath) 31 + if err != nil { 32 + return "", err 33 + } 34 + _, err = file.Write(blob) 35 + if err != nil { 36 + return "", err 37 + } 38 + } 39 + if !validateButton(imgPath) { 40 + os.Remove(imgPath) 41 + } 42 + 43 + return imgPath, nil 44 + 45 + } 46 + 47 + func SyncGetBlob(did string, cid string, ctx context.Context) ([]byte, error) { 48 + host, err := GetPDSFromDid(ctx, did, http.DefaultClient) 49 + if err != nil { 50 + return nil, err 51 + } 52 + c := atclient.NewAPIClient(host) 53 + return atproto.SyncGetBlob(ctx, c, cid, did) 54 + } 55 + 56 + func GetPDSFromDid(ctx context.Context, did string, cli *http.Client) (string, error) { 57 + type Identity struct { 58 + Service []struct { 59 + ID string `json:"id"` 60 + Type string `json:"type"` 61 + ServiceEndpoint string `json:"serviceEndpoint"` 62 + } `json:"service"` 63 + } 64 + var url string 65 + if strings.HasPrefix(did, "did:plc:") { 66 + url = fmt.Sprintf("https://plc.directory/%s", did) 67 + } else if strings.HasPrefix(did, "did:web:") { 68 + url = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")) 69 + } else { 70 + return "", errors.New("did type not supported") 71 + } 72 + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 73 + if err != nil { 74 + return "", errors.New("error crafting request:" + err.Error()) 75 + } 76 + resp, err := cli.Do(req) 77 + if err != nil { 78 + return "", errors.New("error evaluating request:" + err.Error()) 79 + } 80 + defer resp.Body.Close() 81 + if resp.StatusCode != 200 { 82 + return "", errors.New("could not resolve did to service") 83 + } 84 + b, err := io.ReadAll(resp.Body) 85 + if err != nil { 86 + return "", errors.New("error reading response body:" + err.Error()) 87 + } 88 + var identity Identity 89 + err = json.Unmarshal(b, &identity) 90 + if err != nil { 91 + return "", errors.New("error unmarshaling to identity:" + err.Error()) 92 + } 93 + var service *string 94 + for _, svc := range identity.Service { 95 + if svc.ID == "#atproto_pds" { 96 + service = &svc.ServiceEndpoint 97 + } 98 + } 99 + if service == nil { 100 + return "", errors.New("could not find atproto_pds service in resolved did's services") 101 + } 102 + return *service, nil 103 + }
+21
blobs/validation.go
··· 1 + package blobs 2 + 3 + import ( 4 + "image" 5 + "os" 6 + ) 7 + 8 + func validateButton(path string) bool { 9 + file, err := os.Open(path) 10 + if err != nil { 11 + return false 12 + } 13 + defer file.Close() 14 + image, _, err := image.Decode(file) 15 + if err != nil { 16 + return false 17 + } 18 + bounds := image.Bounds() 19 + return bounds.Dx() == 88 && bounds.Dy() == 31 20 + 21 + }
+25
cmd/main.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "net/http" 7 + 8 + "tangled.org/moth11.net/88x31/db" 9 + "tangled.org/moth11.net/88x31/handler" 10 + "tangled.org/moth11.net/88x31/oauth" 11 + ) 12 + 13 + func main() { 14 + fmt.Println("running 88x31!") 15 + store, err := db.Init() 16 + if err != nil { 17 + log.Fatal(err) 18 + } 19 + svc, err := oauth.NewService(*store) 20 + if err != nil { 21 + log.Fatal(err) 22 + } 23 + h := handler.MakeHandler(svc) 24 + log.Fatal(http.ListenAndServe(":8080", h.Serve())) 25 + }
+56
db/bans.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "github.com/jackc/pgx/v5" 7 + "tangled.org/moth11.net/88x31/types" 8 + "time" 9 + ) 10 + 11 + func (s *Store) GetBanned(did string, ctx context.Context) (*types.Ban, error) { 12 + row := s.pool.QueryRow(ctx, `SELECT 13 + id, 14 + reason, 15 + till, 16 + banned_at 17 + FROM bans WHERE did = $1 ORDER BY id DESC`, did) 18 + var ban types.Ban 19 + err := row.Scan(&ban.Id, &ban.Reason, &ban.Till, &ban.BannedAt) 20 + if err != nil { 21 + return nil, err 22 + } 23 + ban.Did = did 24 + return &ban, nil 25 + } 26 + 27 + func (s *Store) AddBan(did string, reason *string, till *time.Time, ctx context.Context) error { 28 + _, err := s.pool.Exec(ctx, `INSERT INTO bans ( 29 + did, 30 + reason, 31 + till 32 + ) VALUES ( 33 + $1, $2, $3 34 + ) 35 + `, did, reason, till) 36 + return err 37 + } 38 + 39 + func (s *Store) IsBanned(did string, ctx context.Context) (bool, error) { 40 + ban, err := s.GetBanned(did, ctx) 41 + if ban != nil { 42 + defbanned := false 43 + if ban.Till == nil { 44 + defbanned = true 45 + } else { 46 + defbanned = time.Now().Before(*ban.Till) 47 + } 48 + if defbanned { 49 + return true, nil 50 + } 51 + } 52 + if err != nil && !errors.Is(err, pgx.ErrNoRows) { 53 + return false, err 54 + } 55 + return false, nil 56 + }
+96
db/lexicon.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "github.com/jackc/pgx/v5" 7 + "tangled.org/moth11.net/88x31/types" 8 + "time" 9 + ) 10 + 11 + func (s *Store) GetButtons(limit int, cursor *string, ctx context.Context) ([]types.ButtonView, time.Time, error) { 12 + query := ` 13 + SELECT 14 + uri, 15 + did, 16 + alt, 17 + href, 18 + posted_at 19 + FROM buttons 20 + ORDER BY posted_at DESC 21 + %s 22 + LIMIT $1 23 + ` 24 + if cursor != nil { 25 + query = fmt.Sprintf(query, "WHERE posted_at < $2") 26 + } else { 27 + query = fmt.Sprintf(query, "") 28 + } 29 + rows, err := s.pool.Query(ctx, query, limit, cursor) 30 + if err != nil { 31 + if err == pgx.ErrNoRows { 32 + return nil, time.Time{}, nil 33 + } 34 + return nil, time.Time{}, err 35 + } 36 + defer rows.Close() 37 + var buttons = make([]types.ButtonView, 0) 38 + var postedAt time.Time 39 + for rows.Next() { 40 + var btnView types.ButtonView 41 + err := rows.Scan(&btnView.Src, &btnView.DID, &btnView.Alt, &btnView.Href, &postedAt) 42 + if err != nil { 43 + return nil, time.Time{}, err 44 + } 45 + btnView.Src = fmt.Sprintf("/xrpc/store.88x31.getButton?uri=%s", btnView.Src) 46 + buttons = append(buttons, btnView) 47 + } 48 + return buttons, postedAt, nil 49 + 50 + } 51 + 52 + func (s *Store) GetButton(uri string, ctx context.Context) (*types.Button, error) { 53 + row := s.pool.QueryRow(ctx, ` 54 + SELECT 55 + did, 56 + blob_cid, 57 + blob_mime, 58 + alt, 59 + href, 60 + cid, 61 + posted_at 62 + FROM buttons 63 + WHERE uri = $1 64 + `, uri) 65 + var btn types.Button 66 + err := row.Scan(&btn.DID, 67 + &btn.BlobCID, 68 + &btn.BlobMIME, 69 + &btn.Alt, 70 + &btn.HREF, 71 + &btn.CID, 72 + &btn.PostedAt) 73 + if err != nil { 74 + return nil, err 75 + } 76 + return &btn, nil 77 + } 78 + 79 + func (s *Store) StoreButton(tbr *types.Button, ctx context.Context) error { 80 + _, err := s.pool.Exec(ctx, ` 81 + INSERT INTO buttons ( 82 + uri, 83 + did, 84 + blob_cid, 85 + blob_mime, 86 + alt, 87 + href, 88 + cid, 89 + posted_at, 90 + ) VALUES ( 91 + $1, $2, $3, $4, $5, $6, $7, $8 92 + ) 93 + WHERE uri = $1 94 + `, tbr.URI, tbr.DID, tbr.BlobCID, tbr.BlobMIME, tbr.Alt, tbr.HREF, tbr.CID, tbr.PostedAt) 95 + return err 96 + }
+196
db/oauth.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "strings" 8 + 9 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + ) 12 + 13 + func (s Store) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { 14 + row := s.pool.QueryRow(ctx, ` 15 + SELECT 16 + host_url, 17 + authserver_url, 18 + authserver_token_endpoint, 19 + scopes, 20 + access_token, 21 + refresh_token, 22 + dpop_authserver_nonce, 23 + dpop_host_nonce, 24 + dpop_privatekey_multibase 25 + FROM sessions 26 + WHERE account_did = $1 AND session_id = $2`, did.String(), sessionID) 27 + var scope string 28 + var csd oauth.ClientSessionData 29 + csd.AccountDID = did 30 + csd.SessionID = sessionID 31 + err := row.Scan(&csd.HostURL, 32 + &csd.AuthServerURL, 33 + &csd.AuthServerTokenEndpoint, 34 + &scope, 35 + &csd.AccessToken, 36 + &csd.RefreshToken, 37 + &csd.DPoPAuthServerNonce, 38 + &csd.DPoPHostNonce, 39 + &csd.DPoPPrivateKeyMultibase, 40 + ) 41 + if err != nil { 42 + return nil, errors.New("error scanning: " + err.Error()) 43 + } 44 + scopes := strings.Fields(scope) 45 + csd.Scopes = scopes 46 + return &csd, nil 47 + } 48 + 49 + func (s Store) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error { 50 + scope := strings.Join(sess.Scopes, " ") 51 + _, err := s.pool.Exec(ctx, ` 52 + INSERT INTO sessions ( 53 + session_id, 54 + account_did, 55 + host_url, 56 + authserver_url, 57 + authserver_token_endpoint, 58 + scopes, 59 + access_token, 60 + refresh_token, 61 + dpop_authserver_nonce, 62 + dpop_host_nonce, 63 + dpop_privatekey_multibase 64 + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) 65 + ON CONFLICT (session_id) 66 + DO UPDATE SET 67 + account_did = EXCLUDED.account_did, 68 + host_url = EXCLUDED.host_url, 69 + authserver_url = EXCLUDED.authserver_url, 70 + authserver_token_endpoint = EXCLUDED.authserver_token_endpoint, 71 + scopes = EXCLUDED.scopes, 72 + access_token = EXCLUDED.access_token, 73 + refresh_token = EXCLUDED.refresh_token, 74 + dpop_authserver_nonce = EXCLUDED.dpop_authserver_nonce, 75 + dpop_host_nonce = EXCLUDED.dpop_host_nonce, 76 + dpop_privatekey_multibase = EXCLUDED.dpop_privatekey_multibase 77 + `, 78 + sess.SessionID, 79 + sess.AccountDID.String(), 80 + sess.HostURL, 81 + sess.AuthServerURL, 82 + sess.AuthServerTokenEndpoint, 83 + scope, 84 + sess.AccessToken, 85 + sess.RefreshToken, 86 + sess.DPoPAuthServerNonce, 87 + sess.DPoPHostNonce, 88 + sess.DPoPPrivateKeyMultibase, 89 + ) 90 + if err != nil { 91 + return errors.New("failed to insert: " + err.Error()) 92 + } 93 + return nil 94 + } 95 + 96 + func (s Store) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error { 97 + _, err := s.pool.Exec(ctx, `DELETE FROM sessions WHERE account_did = $1 AND session_id = $2`, did.String(), sessionID) 98 + if err != nil { 99 + return errors.New("failed to delete: " + err.Error()) 100 + } 101 + return nil 102 + } 103 + 104 + func (s Store) DeleteAllSessions(ctx context.Context, did string) error { 105 + _, err := s.pool.Exec(ctx, `DELETE FROM sessions WHERE account_did = $1`, did) 106 + return err 107 + } 108 + 109 + func (s Store) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) { 110 + row := s.pool.QueryRow(ctx, ` 111 + SELECT 112 + authserver_url, 113 + account_did, 114 + scopes, 115 + request_uri, 116 + authserver_token_endpoint, 117 + pkce_verifier, 118 + dpop_authserver_nonce, 119 + dpop_privatekey_multibase 120 + FROM requests 121 + WHERE state = $1 122 + `, state) 123 + var ari oauth.AuthRequestData 124 + ari.State = state 125 + var did string 126 + var scope string 127 + err := row.Scan( 128 + &ari.AuthServerURL, 129 + &did, 130 + &scope, 131 + &ari.RequestURI, 132 + &ari.AuthServerTokenEndpoint, 133 + &ari.PKCEVerifier, 134 + &ari.DPoPAuthServerNonce, 135 + &ari.DPoPPrivateKeyMultibase, 136 + ) 137 + if err != nil { 138 + return nil, errors.New("failed to scan: " + err.Error()) 139 + } 140 + scopes := strings.Fields(scope) 141 + ari.Scopes = scopes 142 + sdid, err := syntax.ParseDID(did) 143 + if err != nil { 144 + return nil, errors.New("failed to parse did: " + err.Error()) 145 + } 146 + ari.AccountDID = &sdid 147 + return &ari, nil 148 + } 149 + 150 + func (s Store) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error { 151 + scope := strings.Join(info.Scopes, " ") 152 + _, err := s.pool.Exec(ctx, ` 153 + INSERT INTO requests ( 154 + state, 155 + authserver_url, 156 + account_did, 157 + scopes, 158 + request_uri, 159 + authserver_token_endpoint, 160 + pkce_verifier, 161 + dpop_authserver_nonce, 162 + dpop_privatekey_multibase) 163 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, 164 + info.State, 165 + info.AuthServerURL, 166 + info.AccountDID, 167 + scope, 168 + info.RequestURI, 169 + info.AuthServerTokenEndpoint, 170 + info.PKCEVerifier, 171 + info.DPoPAuthServerNonce, 172 + info.DPoPPrivateKeyMultibase, 173 + ) 174 + if err != nil { 175 + return errors.New("failed to insert: " + err.Error()) 176 + } 177 + return nil 178 + } 179 + 180 + func (s Store) DeleteAuthRequestInfo(ctx context.Context, state string) error { 181 + _, err := s.pool.Exec(ctx, `DELETE FROM requests WHERE state = $1`, state) 182 + if err != nil { 183 + return errors.New("failed to delete: " + err.Error()) 184 + } 185 + return nil 186 + } 187 + 188 + func (s *Store) SetDpopPdsNonce(id int, dpopnonce string) error { 189 + _, err := s.pool.Exec(context.Background(), ` 190 + UPDATE oauthsessions SET dpop_pds_nonce = $1 WHERE id = $2 191 + `, dpopnonce, id) 192 + if err != nil { 193 + return errors.New(fmt.Sprintf("error updating dpop nonce for id %d: %s", id, err.Error())) 194 + } 195 + return nil 196 + }
+36
db/store.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "github.com/jackc/pgx/v5/pgxpool" 7 + "os" 8 + ) 9 + 10 + type Store struct { 11 + pool *pgxpool.Pool 12 + } 13 + 14 + func Init() (*Store, error) { 15 + pool, err := initialize() 16 + return &Store{pool}, err 17 + } 18 + 19 + func initialize() (*pgxpool.Pool, error) { 20 + dbuser := os.Getenv("POSTGRES_USER") 21 + dbpass := os.Getenv("POSTGRES_PASSWORD") 22 + dbhost := "localhost" 23 + dbport := os.Getenv("POSTGRES_PORT") 24 + dbdb := os.Getenv("POSTGRES_DB") 25 + dburl := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dbuser, dbpass, dbhost, dbport, dbdb) 26 + pool, err := pgxpool.New(context.Background(), dburl) 27 + if err != nil { 28 + return nil, err 29 + } 30 + pingErr := pool.Ping(context.Background()) 31 + if pingErr != nil { 32 + return nil, pingErr 33 + } 34 + fmt.Println("connected!") 35 + return pool, nil 36 + }
+16
gen/main.go
··· 1 + package main 2 + 3 + import ( 4 + cbg "github.com/whyrusleeping/cbor-gen" 5 + "tangled.org/moth11.net/88x31/lex" 6 + ) 7 + 8 + func main() { 9 + if err := cbg.WriteMapEncodersToFile("lex/lexicons_cbor.go", "lex", 10 + lex.ButtonRecord{}, 11 + lex.LikeSubject{}, 12 + lex.LikeRecord{}, 13 + ); err != nil { 14 + panic(err) 15 + } 16 + }
+48
go.mod
··· 1 + module tangled.org/moth11.net/88x31 2 + 3 + go 1.25 4 + 5 + require ( 6 + github.com/bluesky-social/indigo v0.0.0-20251127021457-6f2658724b36 7 + github.com/gorilla/sessions v1.4.0 8 + github.com/ipfs/go-cid v0.4.1 9 + github.com/jackc/pgx/v5 v5.7.6 10 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 11 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 12 + ) 13 + 14 + require ( 15 + github.com/beorn7/perks v1.0.1 // indirect 16 + github.com/cespare/xxhash/v2 v2.2.0 // indirect 17 + github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 18 + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 19 + github.com/google/go-querystring v1.1.0 // indirect 20 + github.com/gorilla/securecookie v1.1.2 // indirect 21 + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 22 + github.com/jackc/pgpassfile v1.0.0 // indirect 23 + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 24 + github.com/jackc/puddle/v2 v2.2.2 // indirect 25 + github.com/klauspost/cpuid/v2 v2.2.7 // indirect 26 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 27 + github.com/minio/sha256-simd v1.0.1 // indirect 28 + github.com/mr-tron/base58 v1.2.0 // indirect 29 + github.com/multiformats/go-base32 v0.1.0 // indirect 30 + github.com/multiformats/go-base36 v0.2.0 // indirect 31 + github.com/multiformats/go-multibase v0.2.0 // indirect 32 + github.com/multiformats/go-multihash v0.2.3 // indirect 33 + github.com/multiformats/go-varint v0.0.7 // indirect 34 + github.com/prometheus/client_golang v1.17.0 // indirect 35 + github.com/prometheus/client_model v0.5.0 // indirect 36 + github.com/prometheus/common v0.45.0 // indirect 37 + github.com/prometheus/procfs v0.12.0 // indirect 38 + github.com/spaolacci/murmur3 v1.1.0 // indirect 39 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 40 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 41 + golang.org/x/crypto v0.37.0 // indirect 42 + golang.org/x/sync v0.13.0 // indirect 43 + golang.org/x/sys v0.32.0 // indirect 44 + golang.org/x/text v0.24.0 // indirect 45 + golang.org/x/time v0.3.0 // indirect 46 + google.golang.org/protobuf v1.33.0 // indirect 47 + lukechampine.com/blake3 v1.2.1 // indirect 48 + )
+99
go.sum
··· 1 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 + github.com/bluesky-social/indigo v0.0.0-20251127021457-6f2658724b36 h1:Vc+l4sltxQfBT8qC3dm87PRYInmxlGyF1dmpjaW0WkU= 4 + github.com/bluesky-social/indigo v0.0.0-20251127021457-6f2658724b36/go.mod h1:Pm2I1+iDXn/hLbF7XCg/DsZi6uDCiOo7hZGWprSM7k0= 5 + github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 6 + github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 + github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 11 + github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 12 + github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 13 + github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 14 + github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 15 + github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 16 + github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 17 + github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 18 + github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 19 + github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 20 + github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 21 + github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 22 + github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 23 + github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 24 + github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 25 + github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 26 + github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 27 + github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 28 + github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 29 + github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 30 + github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 31 + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 32 + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 33 + github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= 34 + github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= 35 + github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 36 + github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 37 + github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 38 + github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 39 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 40 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 41 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 42 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 43 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 44 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 45 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 46 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 47 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 48 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 49 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 50 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 51 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 52 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 53 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 54 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 55 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 + github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 58 + github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 59 + github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 60 + github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 61 + github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 62 + github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 63 + github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 64 + github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 65 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 66 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 67 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 69 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 70 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 71 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 72 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 73 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 74 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 75 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 76 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 77 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 78 + golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 79 + golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 80 + golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 81 + golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 82 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 + golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 84 + golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 85 + golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 86 + golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 87 + golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 88 + golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 89 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 90 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 91 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 92 + google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 93 + google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 94 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 95 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 97 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 98 + lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 99 + lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+53
handler/button.go
··· 1 + package handler 2 + 3 + import ( 4 + "encoding/json" 5 + "log" 6 + "net/http" 7 + "strconv" 8 + "tangled.org/moth11.net/88x31/types" 9 + "time" 10 + ) 11 + 12 + func (h *Handler) getButtons(w http.ResponseWriter, r *http.Request) { 13 + limit := r.URL.Query().Get("limit") 14 + limitI, err := strconv.Atoi(limit) 15 + if err != nil { 16 + limitI = 50 17 + } 18 + if limitI > 100 { 19 + limitI = 100 20 + } 21 + if limitI < 1 { 22 + limitI = 1 23 + } 24 + cursor := r.URL.Query().Get("cursor") 25 + var cursorptr *string 26 + if cursor != "" { 27 + cursorptr = &cursor 28 + } 29 + 30 + btnViews, ncursor, err := h.db.GetButtons(limitI, cursorptr, r.Context()) 31 + if err != nil { 32 + http.Error(w, "error getting buttons!", http.StatusInternalServerError) 33 + return 34 + } 35 + encoder := json.NewEncoder(w) 36 + type Resp struct { 37 + BtnViews []types.ButtonView `json:"button"` 38 + Cursor *time.Time `json:"cursor,omitempty"` 39 + } 40 + myresp := Resp{} 41 + if btnViews == nil { 42 + myresp.BtnViews = make([]types.ButtonView, 0) 43 + myresp.Cursor = nil 44 + } else { 45 + myresp.BtnViews = btnViews 46 + myresp.Cursor = &ncursor 47 + } 48 + err = encoder.Encode(btnViews) 49 + if err != nil { 50 + log.Println(err) 51 + http.Error(w, "error encoding response", http.StatusInternalServerError) 52 + } 53 + }
+152
handler/handler.go
··· 1 + package handler 2 + 3 + import ( 4 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 5 + "github.com/gorilla/sessions" 6 + "html/template" 7 + "net/http" 8 + "os" 9 + "tangled.org/moth11.net/88x31/db" 10 + myoauth "tangled.org/moth11.net/88x31/oauth" 11 + "tangled.org/moth11.net/88x31/types" 12 + ) 13 + 14 + type Handler struct { 15 + db *db.Store 16 + router *http.ServeMux 17 + oauth *myoauth.Service 18 + sessionStore *sessions.CookieStore 19 + } 20 + 21 + var buttonT = template.Must(template.ParseFiles("./tmpl/partial/buttonpart.html", "./tmpl/base.html", "./tmpl/button.html")) 22 + var homeT = template.Must(template.ParseFiles("./tmpl/partial/buttonpart.html", "./tmpl/base.html", "./tmpl/home.html")) 23 + var loginT = template.Must(template.ParseFiles("./tmpl/base.html", "./tmpl/login.html")) 24 + var logoutT = template.Must(template.ParseFiles("./tmpl/base.html", "./tmpl/logout.html")) 25 + var uploadT = template.Must(template.ParseFiles("./tmpl/base.html", "./tmpl/upload.html")) 26 + 27 + func MakeHandler(oauth *myoauth.Service) *Handler { 28 + mux := http.NewServeMux() 29 + sessionStore := sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) 30 + h := &Handler{router: mux, oauth: oauth, sessionStore: sessionStore} 31 + mux.HandleFunc("GET /", h.oauthMiddleware(h.gethome)) 32 + mux.HandleFunc("GET /login", h.oauthMiddleware(getlogin)) 33 + mux.HandleFunc("POST /login", h.login) 34 + mux.HandleFunc("GET /logout", h.oauthMiddleware(getlogout)) 35 + mux.HandleFunc("POST /logout", h.oauthMiddleware(h.logout)) 36 + mux.HandleFunc("GET /upload", h.oauthMiddleware(getupload)) 37 + mux.HandleFunc("POST /upload", h.oauthMiddleware(h.upload)) 38 + mux.HandleFunc("GET /button", h.oauthMiddleware(h.getbutton)) 39 + mux.HandleFunc("GET /xrpc/store.88x31.getButton", h.WithCORS(h.getButton)) 40 + mux.HandleFunc("GET /xrpc/store.88x31.getButtons", h.WithCORS(h.getButtons)) 41 + mux.HandleFunc(oauthCallbackPath(), h.oauthCallback) 42 + return h 43 + } 44 + 45 + type EZData struct { 46 + DID *string 47 + Title string 48 + } 49 + 50 + func getlogin(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 51 + if cs != nil { 52 + http.Redirect(w, r, "/logout", http.StatusSeeOther) 53 + } 54 + var ezd EZData 55 + ezd.Title = "login" 56 + err := loginT.ExecuteTemplate(w, "base.html", ezd) 57 + if err != nil { 58 + http.Error(w, err.Error(), http.StatusInternalServerError) 59 + } 60 + } 61 + 62 + func getlogout(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 63 + if cs == nil { 64 + http.Redirect(w, r, "/login", http.StatusSeeOther) 65 + } 66 + var ezd EZData 67 + did := cs.Data.AccountDID.String() 68 + ezd.DID = &did 69 + ezd.Title = "logout" 70 + err := logoutT.ExecuteTemplate(w, "base.html", ezd) 71 + if err != nil { 72 + http.Error(w, err.Error(), http.StatusInternalServerError) 73 + } 74 + } 75 + 76 + func getupload(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 77 + var ezd EZData 78 + if cs != nil { 79 + did := cs.Data.AccountDID.String() 80 + ezd.DID = &did 81 + } 82 + ezd.Title = "upload" 83 + err := uploadT.ExecuteTemplate(w, "base.html", ezd) 84 + if err != nil { 85 + http.Error(w, err.Error(), http.StatusInternalServerError) 86 + } 87 + } 88 + 89 + func (h *Handler) gethome(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 90 + btnView, _, err := h.db.GetButtons(50, nil, r.Context()) 91 + if err != nil || len(btnView) == 0 { 92 + btnView = nil 93 + } 94 + type homedata struct { 95 + cs *oauth.ClientSession 96 + btnView []types.ButtonView 97 + } 98 + 99 + err = homeT.ExecuteTemplate(w, "base.html", homedata{cs, btnView}) 100 + if err != nil { 101 + http.Error(w, err.Error(), http.StatusInternalServerError) 102 + } 103 + } 104 + 105 + func (h *Handler) getbutton(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 106 + uri := r.URL.Query().Get("uri") 107 + btn, err := h.db.GetButton(uri, r.Context()) 108 + if err != nil || btn == nil { 109 + http.Error(w, "not found", http.StatusNotFound) 110 + return 111 + } 112 + type ButtonData struct { 113 + DID *string 114 + Title string 115 + Button types.Button 116 + } 117 + var btnd ButtonData 118 + if cs != nil { 119 + did := cs.Data.AccountDID.String() 120 + btnd.DID = &did 121 + } 122 + if btn.Alt != nil { 123 + btnd.Title = *btn.Alt 124 + } else { 125 + btnd.Title = uri 126 + } 127 + btnd.Button = *btn 128 + err = buttonT.ExecuteTemplate(w, "base.html", btnd) 129 + if err != nil { 130 + http.Error(w, "error templating", http.StatusInternalServerError) 131 + } 132 + } 133 + 134 + func (h *Handler) Serve() http.Handler { 135 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 136 + h.router.ServeHTTP(w, r) 137 + }) 138 + } 139 + 140 + func (h *Handler) WithCORS(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { 141 + return func(w http.ResponseWriter, r *http.Request) { 142 + w.Header().Set("Access-Control-Allow-Origin", "*") 143 + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 144 + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorizaton, X-Requested-With, Sec-WebSocket-Protocol, Sec-WebSocket-Extensions, Sec-WebSocket-Key, Sec-WebSocket-Version") 145 + w.Header().Set("Access-Control-Allow-Credentials", "true") 146 + if r.Method == "Options" { 147 + w.WriteHeader(http.StatusOK) 148 + return 149 + } 150 + f(w, r) 151 + } 152 + }
+110
handler/oauthhandlers.go
··· 1 + package handler 2 + 3 + import ( 4 + "fmt" 5 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + "github.com/gorilla/sessions" 8 + "log" 9 + "net/http" 10 + "os" 11 + "strings" 12 + ) 13 + 14 + func (h *Handler) logout(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 15 + if cs != nil { 16 + err := h.db.DeleteSession(r.Context(), cs.Data.AccountDID, cs.Data.SessionID) 17 + if err != nil { 18 + log.Println(err) 19 + http.Error(w, "couldn't log out", http.StatusInternalServerError) 20 + return 21 + } 22 + } 23 + s, _ := h.sessionStore.Get(r, "oauthsession") 24 + s.Values = make(map[any]any) 25 + s.Options.MaxAge = -1 26 + err := s.Save(r, w) 27 + if err != nil { 28 + log.Println(err) 29 + return 30 + } 31 + http.Redirect(w, r, "/", http.StatusSeeOther) 32 + } 33 + 34 + func (h *Handler) login(w http.ResponseWriter, r *http.Request) { 35 + err := r.ParseForm() 36 + if err != nil { 37 + http.Error(w, err.Error(), http.StatusBadRequest) 38 + return 39 + } 40 + identifier := strings.TrimSpace(r.FormValue("identifier")) 41 + redirectURL, err := h.oauth.StartAuthFlow(r.Context(), identifier) 42 + if err != nil { 43 + http.Error(w, err.Error(), http.StatusBadRequest) 44 + return 45 + } 46 + http.Redirect(w, r, redirectURL, http.StatusFound) 47 + } 48 + 49 + func oauthCallbackPath() string { 50 + mp := os.Getenv("MY_OAUTH_CALLBACK") 51 + return fmt.Sprintf("GET %s", mp) 52 + } 53 + 54 + func (h *Handler) oauthCallback(w http.ResponseWriter, r *http.Request) { 55 + sessData, err := h.oauth.OauthCallback(r.Context(), r.URL.Query()) 56 + if err != nil { 57 + return 58 + } 59 + isban, err := h.db.IsBanned(sessData.AccountDID.String(), r.Context()) 60 + if err != nil { 61 + http.Error(w, "internal error: ban", http.StatusInternalServerError) 62 + return 63 + } 64 + if isban { 65 + ban, _ := h.db.GetBanned(sessData.AccountDID.String(), r.Context()) 66 + http.Redirect(w, r, fmt.Sprintf("%s%d", os.Getenv("BAN_ENDPOINT"), ban.Id), http.StatusSeeOther) 67 + return 68 + } 69 + 70 + session, _ := h.sessionStore.Get(r, "oauthsession") 71 + 72 + session.Options = &sessions.Options{ 73 + Path: "/", 74 + MaxAge: 86400 * 7, 75 + HttpOnly: true, 76 + } 77 + session.Values = map[any]any{} 78 + session.Values["did"] = sessData.AccountDID.String() 79 + session.Values["id"] = sessData.SessionID 80 + session.Values["scopes"] = strings.Join(sessData.Scopes, " ") 81 + err = session.Save(r, w) 82 + if err != nil { 83 + http.Error(w, "internal error: session", http.StatusInternalServerError) 84 + return 85 + } 86 + http.Redirect(w, r, "/", http.StatusFound) 87 + } 88 + 89 + func (h *Handler) oauthMiddleware(f func(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { 90 + return func(w http.ResponseWriter, r *http.Request) { 91 + s, _ := h.sessionStore.Get(r, "oauthsession") 92 + id, ok := s.Values["id"].(string) 93 + did, bok := s.Values["did"].(string) 94 + if !ok || !bok { 95 + f(nil, w, r) 96 + return 97 + } 98 + sdid, err := syntax.ParseDID(did) 99 + if err != nil { 100 + f(nil, w, r) 101 + return 102 + } 103 + cs, err := h.oauth.ResumeSession(r.Context(), sdid, id) 104 + if err != nil { 105 + f(nil, w, r) 106 + return 107 + } 108 + f(cs, w, r) 109 + } 110 + }
+127
handler/upload.go
··· 1 + package handler 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "net/http" 7 + "os" 8 + "strings" 9 + 10 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + "tangled.org/moth11.net/88x31/blobs" 13 + "tangled.org/moth11.net/88x31/lex" 14 + myoauth "tangled.org/moth11.net/88x31/oauth" 15 + "tangled.org/moth11.net/88x31/types" 16 + ) 17 + 18 + func (h *Handler) upload(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 19 + if cs == nil { 20 + http.Error(w, "upload requires auth", http.StatusUnauthorized) 21 + return 22 + } 23 + err := r.ParseMultipartForm(1 << 21) 24 + if err != nil { 25 + http.Error(w, "form failed to parse", http.StatusBadRequest) 26 + return 27 + } 28 + alt := r.FormValue("alt") 29 + href := r.FormValue("href") 30 + 31 + file, fheader, err := r.FormFile("button") 32 + if err != nil { 33 + http.Error(w, "form lacks button", http.StatusBadRequest) 34 + return 35 + } 36 + defer file.Close() 37 + ct := fheader.Header.Get("Content-Type") 38 + if !strings.HasPrefix(ct, "image/") { 39 + http.Error(w, "button must be image", http.StatusBadRequest) 40 + return 41 + } 42 + blob, err := myoauth.UploadBLOB(cs, file, fheader, r.Context()) 43 + if err != nil { 44 + http.Error(w, "failed to upload blob", http.StatusInternalServerError) 45 + return 46 + } 47 + var lbr lex.ButtonRecord 48 + if blob == nil { 49 + http.Error(w, "recieved nil blob", http.StatusInternalServerError) 50 + return 51 + } 52 + lbr.Blob = *blob 53 + if alt != "" { 54 + lbr.Alt = &alt 55 + } 56 + if href != "" { 57 + lbr.Href = &href 58 + } 59 + nowsyn := syntax.DatetimeNow() 60 + lbr.PostedAt = nowsyn.String() 61 + uri, cid, err := myoauth.CreateButton(cs, &lbr, r.Context()) 62 + if err != nil { 63 + log.Println(err) 64 + http.Error(w, "error creating button", http.StatusInternalServerError) 65 + return 66 + } 67 + var tbr types.Button 68 + tbr.Alt = lbr.Alt 69 + tbr.HREF = lbr.Href 70 + tbr.BlobCID = blob.Ref.String() 71 + tbr.BlobMIME = ct 72 + tbr.URI = uri 73 + tbr.CID = cid 74 + tbr.DID = cs.Data.AccountDID.String() 75 + tbr.PostedAt = nowsyn.Time() 76 + err = h.db.StoreButton(&tbr, r.Context()) 77 + if err != nil { 78 + log.Println(err) 79 + } 80 + http.Redirect(w, r, "/button?uri=%s", http.StatusFound) 81 + } 82 + 83 + func (h *Handler) getButton(w http.ResponseWriter, r *http.Request) { 84 + vals := r.URL.Query() 85 + var did string 86 + var cid string 87 + uri := vals.Get("uri") 88 + var button *types.Button 89 + var err error 90 + if uri != "" { 91 + button, err = h.db.GetButton(uri, r.Context()) 92 + if err == nil { 93 + did = button.DID 94 + cid = button.BlobCID 95 + } 96 + } else { 97 + http.Error(w, "provide a uri!", http.StatusBadRequest) 98 + return 99 + } 100 + ib, _ := h.db.IsBanned(did, r.Context()) 101 + if ib { 102 + http.Error(w, "i don't serve banned content", 404) 103 + return 104 + } 105 + imgPath, err := blobs.AddImageToCache(did, cid, r.Context()) 106 + if err != nil { 107 + http.Error(w, "error adding to cache, likely bad size", http.StatusInternalServerError) 108 + return 109 + } 110 + 111 + stats, err := os.Stat(imgPath) 112 + if err != nil { 113 + http.Error(w, "error recieving from cache, strange...", http.StatusInternalServerError) 114 + return 115 + } 116 + 117 + mime := button.BlobMIME 118 + w.Header().Add("Content-Type", mime) 119 + w.Header().Add("Content-Length", fmt.Sprintf("%d", stats.Size())) 120 + 121 + img, err := os.Open(imgPath) 122 + if err != nil { 123 + http.Error(w, "error reading from cache, strange...", http.StatusInternalServerError) 124 + return 125 + } 126 + img.WriteTo(w) 127 + }
+581
lex/lexicons_cbor.go
··· 1 + // Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. 2 + 3 + package lex 4 + 5 + import ( 6 + "fmt" 7 + "io" 8 + "math" 9 + "sort" 10 + 11 + cid "github.com/ipfs/go-cid" 12 + cbg "github.com/whyrusleeping/cbor-gen" 13 + xerrors "golang.org/x/xerrors" 14 + ) 15 + 16 + var _ = xerrors.Errorf 17 + var _ = cid.Undef 18 + var _ = math.E 19 + var _ = sort.Sort 20 + 21 + func (t *ButtonRecord) MarshalCBOR(w io.Writer) error { 22 + if t == nil { 23 + _, err := w.Write(cbg.CborNull) 24 + return err 25 + } 26 + 27 + cw := cbg.NewCborWriter(w) 28 + fieldCount := 5 29 + 30 + if t.Href == nil { 31 + fieldCount-- 32 + } 33 + 34 + if t.Alt == nil { 35 + fieldCount-- 36 + } 37 + 38 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 39 + return err 40 + } 41 + 42 + // t.Alt (string) (string) 43 + if t.Alt != nil { 44 + 45 + if len("alt") > 8192 { 46 + return xerrors.Errorf("Value in field \"alt\" was too long") 47 + } 48 + 49 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("alt"))); err != nil { 50 + return err 51 + } 52 + if _, err := cw.WriteString(string("alt")); err != nil { 53 + return err 54 + } 55 + 56 + if t.Alt == nil { 57 + if _, err := cw.Write(cbg.CborNull); err != nil { 58 + return err 59 + } 60 + } else { 61 + if len(*t.Alt) > 8192 { 62 + return xerrors.Errorf("Value in field t.Alt was too long") 63 + } 64 + 65 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Alt))); err != nil { 66 + return err 67 + } 68 + if _, err := cw.WriteString(string(*t.Alt)); err != nil { 69 + return err 70 + } 71 + } 72 + } 73 + 74 + // t.Blob (util.BlobSchema) (struct) 75 + if len("blob") > 8192 { 76 + return xerrors.Errorf("Value in field \"blob\" was too long") 77 + } 78 + 79 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { 80 + return err 81 + } 82 + if _, err := cw.WriteString(string("blob")); err != nil { 83 + return err 84 + } 85 + 86 + if err := t.Blob.MarshalCBOR(cw); err != nil { 87 + return err 88 + } 89 + 90 + // t.Href (string) (string) 91 + if t.Href != nil { 92 + 93 + if len("href") > 8192 { 94 + return xerrors.Errorf("Value in field \"href\" was too long") 95 + } 96 + 97 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("href"))); err != nil { 98 + return err 99 + } 100 + if _, err := cw.WriteString(string("href")); err != nil { 101 + return err 102 + } 103 + 104 + if t.Href == nil { 105 + if _, err := cw.Write(cbg.CborNull); err != nil { 106 + return err 107 + } 108 + } else { 109 + if len(*t.Href) > 8192 { 110 + return xerrors.Errorf("Value in field t.Href was too long") 111 + } 112 + 113 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Href))); err != nil { 114 + return err 115 + } 116 + if _, err := cw.WriteString(string(*t.Href)); err != nil { 117 + return err 118 + } 119 + } 120 + } 121 + 122 + // t.LexiconTypeID (string) (string) 123 + if len("$type") > 8192 { 124 + return xerrors.Errorf("Value in field \"$type\" was too long") 125 + } 126 + 127 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 128 + return err 129 + } 130 + if _, err := cw.WriteString(string("$type")); err != nil { 131 + return err 132 + } 133 + 134 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("org.xcvr.actor.profile"))); err != nil { 135 + return err 136 + } 137 + if _, err := cw.WriteString(string("org.xcvr.actor.profile")); err != nil { 138 + return err 139 + } 140 + 141 + // t.PostedAt (string) (string) 142 + if len("omitEmpty") > 8192 { 143 + return xerrors.Errorf("Value in field \"omitEmpty\" was too long") 144 + } 145 + 146 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("omitEmpty"))); err != nil { 147 + return err 148 + } 149 + if _, err := cw.WriteString(string("omitEmpty")); err != nil { 150 + return err 151 + } 152 + 153 + if len(t.PostedAt) > 8192 { 154 + return xerrors.Errorf("Value in field t.PostedAt was too long") 155 + } 156 + 157 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.PostedAt))); err != nil { 158 + return err 159 + } 160 + if _, err := cw.WriteString(string(t.PostedAt)); err != nil { 161 + return err 162 + } 163 + return nil 164 + } 165 + 166 + func (t *ButtonRecord) UnmarshalCBOR(r io.Reader) (err error) { 167 + *t = ButtonRecord{} 168 + 169 + cr := cbg.NewCborReader(r) 170 + 171 + maj, extra, err := cr.ReadHeader() 172 + if err != nil { 173 + return err 174 + } 175 + defer func() { 176 + if err == io.EOF { 177 + err = io.ErrUnexpectedEOF 178 + } 179 + }() 180 + 181 + if maj != cbg.MajMap { 182 + return fmt.Errorf("cbor input should be of type map") 183 + } 184 + 185 + if extra > cbg.MaxLength { 186 + return fmt.Errorf("ButtonRecord: map struct too large (%d)", extra) 187 + } 188 + 189 + n := extra 190 + 191 + nameBuf := make([]byte, 9) 192 + for i := uint64(0); i < n; i++ { 193 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 194 + if err != nil { 195 + return err 196 + } 197 + 198 + if !ok { 199 + // Field doesn't exist on this type, so ignore it 200 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 201 + return err 202 + } 203 + continue 204 + } 205 + 206 + switch string(nameBuf[:nameLen]) { 207 + // t.Alt (string) (string) 208 + case "alt": 209 + 210 + { 211 + b, err := cr.ReadByte() 212 + if err != nil { 213 + return err 214 + } 215 + if b != cbg.CborNull[0] { 216 + if err := cr.UnreadByte(); err != nil { 217 + return err 218 + } 219 + 220 + sval, err := cbg.ReadStringWithMax(cr, 8192) 221 + if err != nil { 222 + return err 223 + } 224 + 225 + t.Alt = (*string)(&sval) 226 + } 227 + } 228 + // t.Blob (util.BlobSchema) (struct) 229 + case "blob": 230 + 231 + { 232 + 233 + if err := t.Blob.UnmarshalCBOR(cr); err != nil { 234 + return xerrors.Errorf("unmarshaling t.Blob: %w", err) 235 + } 236 + 237 + } 238 + // t.Href (string) (string) 239 + case "href": 240 + 241 + { 242 + b, err := cr.ReadByte() 243 + if err != nil { 244 + return err 245 + } 246 + if b != cbg.CborNull[0] { 247 + if err := cr.UnreadByte(); err != nil { 248 + return err 249 + } 250 + 251 + sval, err := cbg.ReadStringWithMax(cr, 8192) 252 + if err != nil { 253 + return err 254 + } 255 + 256 + t.Href = (*string)(&sval) 257 + } 258 + } 259 + // t.LexiconTypeID (string) (string) 260 + case "$type": 261 + 262 + { 263 + sval, err := cbg.ReadStringWithMax(cr, 8192) 264 + if err != nil { 265 + return err 266 + } 267 + 268 + t.LexiconTypeID = string(sval) 269 + } 270 + // t.PostedAt (string) (string) 271 + case "omitEmpty": 272 + 273 + { 274 + sval, err := cbg.ReadStringWithMax(cr, 8192) 275 + if err != nil { 276 + return err 277 + } 278 + 279 + t.PostedAt = string(sval) 280 + } 281 + 282 + default: 283 + // Field doesn't exist on this type, so ignore it 284 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 285 + return err 286 + } 287 + } 288 + } 289 + 290 + return nil 291 + } 292 + func (t *LikeSubject) MarshalCBOR(w io.Writer) error { 293 + if t == nil { 294 + _, err := w.Write(cbg.CborNull) 295 + return err 296 + } 297 + 298 + cw := cbg.NewCborWriter(w) 299 + 300 + if _, err := cw.Write([]byte{162}); err != nil { 301 + return err 302 + } 303 + 304 + // t.CID (string) (string) 305 + if len("cid") > 8192 { 306 + return xerrors.Errorf("Value in field \"cid\" was too long") 307 + } 308 + 309 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cid"))); err != nil { 310 + return err 311 + } 312 + if _, err := cw.WriteString(string("cid")); err != nil { 313 + return err 314 + } 315 + 316 + if len(t.CID) > 8192 { 317 + return xerrors.Errorf("Value in field t.CID was too long") 318 + } 319 + 320 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CID))); err != nil { 321 + return err 322 + } 323 + if _, err := cw.WriteString(string(t.CID)); err != nil { 324 + return err 325 + } 326 + 327 + // t.URI (string) (string) 328 + if len("uri") > 8192 { 329 + return xerrors.Errorf("Value in field \"uri\" was too long") 330 + } 331 + 332 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("uri"))); err != nil { 333 + return err 334 + } 335 + if _, err := cw.WriteString(string("uri")); err != nil { 336 + return err 337 + } 338 + 339 + if len(t.URI) > 8192 { 340 + return xerrors.Errorf("Value in field t.URI was too long") 341 + } 342 + 343 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.URI))); err != nil { 344 + return err 345 + } 346 + if _, err := cw.WriteString(string(t.URI)); err != nil { 347 + return err 348 + } 349 + return nil 350 + } 351 + 352 + func (t *LikeSubject) UnmarshalCBOR(r io.Reader) (err error) { 353 + *t = LikeSubject{} 354 + 355 + cr := cbg.NewCborReader(r) 356 + 357 + maj, extra, err := cr.ReadHeader() 358 + if err != nil { 359 + return err 360 + } 361 + defer func() { 362 + if err == io.EOF { 363 + err = io.ErrUnexpectedEOF 364 + } 365 + }() 366 + 367 + if maj != cbg.MajMap { 368 + return fmt.Errorf("cbor input should be of type map") 369 + } 370 + 371 + if extra > cbg.MaxLength { 372 + return fmt.Errorf("LikeSubject: map struct too large (%d)", extra) 373 + } 374 + 375 + n := extra 376 + 377 + nameBuf := make([]byte, 3) 378 + for i := uint64(0); i < n; i++ { 379 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 380 + if err != nil { 381 + return err 382 + } 383 + 384 + if !ok { 385 + // Field doesn't exist on this type, so ignore it 386 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 387 + return err 388 + } 389 + continue 390 + } 391 + 392 + switch string(nameBuf[:nameLen]) { 393 + // t.CID (string) (string) 394 + case "cid": 395 + 396 + { 397 + sval, err := cbg.ReadStringWithMax(cr, 8192) 398 + if err != nil { 399 + return err 400 + } 401 + 402 + t.CID = string(sval) 403 + } 404 + // t.URI (string) (string) 405 + case "uri": 406 + 407 + { 408 + sval, err := cbg.ReadStringWithMax(cr, 8192) 409 + if err != nil { 410 + return err 411 + } 412 + 413 + t.URI = string(sval) 414 + } 415 + 416 + default: 417 + // Field doesn't exist on this type, so ignore it 418 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 419 + return err 420 + } 421 + } 422 + } 423 + 424 + return nil 425 + } 426 + func (t *LikeRecord) MarshalCBOR(w io.Writer) error { 427 + if t == nil { 428 + _, err := w.Write(cbg.CborNull) 429 + return err 430 + } 431 + 432 + cw := cbg.NewCborWriter(w) 433 + 434 + if _, err := cw.Write([]byte{163}); err != nil { 435 + return err 436 + } 437 + 438 + // t.LexiconTypeID (string) (string) 439 + if len("$type") > 8192 { 440 + return xerrors.Errorf("Value in field \"$type\" was too long") 441 + } 442 + 443 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 444 + return err 445 + } 446 + if _, err := cw.WriteString(string("$type")); err != nil { 447 + return err 448 + } 449 + 450 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("org.xcvr.feed.channel"))); err != nil { 451 + return err 452 + } 453 + if _, err := cw.WriteString(string("org.xcvr.feed.channel")); err != nil { 454 + return err 455 + } 456 + 457 + // t.Subject (lex.LikeSubject) (struct) 458 + if len("subject") > 8192 { 459 + return xerrors.Errorf("Value in field \"subject\" was too long") 460 + } 461 + 462 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 463 + return err 464 + } 465 + if _, err := cw.WriteString(string("subject")); err != nil { 466 + return err 467 + } 468 + 469 + if err := t.Subject.MarshalCBOR(cw); err != nil { 470 + return err 471 + } 472 + 473 + // t.CreatedAt (string) (string) 474 + if len("omitEmpty") > 8192 { 475 + return xerrors.Errorf("Value in field \"omitEmpty\" was too long") 476 + } 477 + 478 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("omitEmpty"))); err != nil { 479 + return err 480 + } 481 + if _, err := cw.WriteString(string("omitEmpty")); err != nil { 482 + return err 483 + } 484 + 485 + if len(t.CreatedAt) > 8192 { 486 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 487 + } 488 + 489 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 490 + return err 491 + } 492 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 493 + return err 494 + } 495 + return nil 496 + } 497 + 498 + func (t *LikeRecord) UnmarshalCBOR(r io.Reader) (err error) { 499 + *t = LikeRecord{} 500 + 501 + cr := cbg.NewCborReader(r) 502 + 503 + maj, extra, err := cr.ReadHeader() 504 + if err != nil { 505 + return err 506 + } 507 + defer func() { 508 + if err == io.EOF { 509 + err = io.ErrUnexpectedEOF 510 + } 511 + }() 512 + 513 + if maj != cbg.MajMap { 514 + return fmt.Errorf("cbor input should be of type map") 515 + } 516 + 517 + if extra > cbg.MaxLength { 518 + return fmt.Errorf("LikeRecord: map struct too large (%d)", extra) 519 + } 520 + 521 + n := extra 522 + 523 + nameBuf := make([]byte, 9) 524 + for i := uint64(0); i < n; i++ { 525 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 526 + if err != nil { 527 + return err 528 + } 529 + 530 + if !ok { 531 + // Field doesn't exist on this type, so ignore it 532 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 533 + return err 534 + } 535 + continue 536 + } 537 + 538 + switch string(nameBuf[:nameLen]) { 539 + // t.LexiconTypeID (string) (string) 540 + case "$type": 541 + 542 + { 543 + sval, err := cbg.ReadStringWithMax(cr, 8192) 544 + if err != nil { 545 + return err 546 + } 547 + 548 + t.LexiconTypeID = string(sval) 549 + } 550 + // t.Subject (lex.LikeSubject) (struct) 551 + case "subject": 552 + 553 + { 554 + 555 + if err := t.Subject.UnmarshalCBOR(cr); err != nil { 556 + return xerrors.Errorf("unmarshaling t.Subject: %w", err) 557 + } 558 + 559 + } 560 + // t.CreatedAt (string) (string) 561 + case "omitEmpty": 562 + 563 + { 564 + sval, err := cbg.ReadStringWithMax(cr, 8192) 565 + if err != nil { 566 + return err 567 + } 568 + 569 + t.CreatedAt = string(sval) 570 + } 571 + 572 + default: 573 + // Field doesn't exist on this type, so ignore it 574 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 575 + return err 576 + } 577 + } 578 + } 579 + 580 + return nil 581 + }
+27
lex/types.go
··· 1 + package lex 2 + 3 + import ( 4 + "github.com/bluesky-social/indigo/lex/util" 5 + ) 6 + 7 + func init() { 8 + } 9 + 10 + type ButtonRecord struct { 11 + LexiconTypeID string `json:"$type,const=org.xcvr.actor.profile" cborgen:"$type,const=org.xcvr.actor.profile"` 12 + Href *string `json:"href,omitempty" cborgen:"href,omitempty"` 13 + Alt *string `json:"alt,omitempty" cborgen:"alt,omitempty"` 14 + Blob util.BlobSchema `json:"blob" cborgen:"blob"` 15 + PostedAt string `json:"postedAt,omitempty" cborgen:"postedAt,omitEmpty" ` 16 + } 17 + 18 + type LikeSubject struct { 19 + URI string `json:"uri" cborgen:"uri"` 20 + CID string `json:"cid" cborgen:"cid"` 21 + } 22 + 23 + type LikeRecord struct { 24 + LexiconTypeID string `json:"$type,const=org.xcvr.feed.channel" cborgen:"$type,const=org.xcvr.feed.channel"` 25 + Subject LikeSubject `json:"subject" cborgen:"subject"` 26 + CreatedAt string `json:"postedAt,omitempty" cborgen:"postedAt,omitEmpty" ` 27 + }
+25
migrations/001_oauth.up.sql
··· 1 + CREATE TABLE requests ( 2 + state TEXT NOT NULL PRIMARY KEY, 3 + authserver_url TEXT NOT NULL, 4 + account_did TEXT, 5 + scopes TEXT NOT NULL, 6 + request_uri TEXT NOT NULL, 7 + authserver_token_endpoint TEXT NOT NULL, 8 + pkce_verifier TEXT NOT NULL, 9 + dpop_authserver_nonce TEXT NOT NULL, 10 + dpop_privatekey_multibase TEXT NOT NULL 11 + ); 12 + 13 + CREATE TABLE sessions ( 14 + session_id TEXT NOT NULL PRIMARY KEY, 15 + account_did TEXT NOT NULL, 16 + host_url TEXT NOT NULL, 17 + authserver_url TEXT NOT NULL, 18 + authserver_token_endpoint TEXT NOT NULL, 19 + scopes TEXT NOT NULL, 20 + access_token TEXT NOT NULL, 21 + refresh_token TEXT NOT NULL, 22 + dpop_authserver_nonce TEXT NOT NULL, 23 + dpop_host_nonce TEXT NOT NULL, 24 + dpop_privatekey_multibase TEXT NOT NULL 25 + );
+2
migrations/001_oauth_down.sql
··· 1 + DROP TABLE IF EXISTS sessions; 2 + DROP TABLE IF EXISTS requests;
+2
migrations/002_buttons_down.sql
··· 1 + DELETE INDEX IF EXISTS buttons_posted_at_idx; 2 + DROP TABLE IF EXISTS buttons;
+13
migrations/002_buttons_up.sql
··· 1 + CREATE TABLE buttons ( 2 + uri TEXT PRIMARY KEY, 3 + did TEXT NOT NULL, 4 + blob_cid TEXT NOT NULL, 5 + blob_mime TEXT NOT NULL, 6 + alt TEXT, 7 + href TEXT, 8 + cid TEXT NOT NULL, 9 + posted_at TIMESTAMPTZ NOT NULL DEFAULT now(), 10 + indexed_at TIMESTAMPTZ NOT NULL DEFAULT now() 11 + ); 12 + 13 + CREATE INDEX buttons (posted_at);
+1
migrations/003_likes_down.sql
··· 1 + DROP TABLE IF EXISTS likes;
+9
migrations/003_likes_up.sql
··· 1 + CREATE TABLE likes ( 2 + uri TEXT PRIMARY KEY, 3 + subject_uri TEXT NOT NULL, 4 + subject_cid TEXT NOT NULL, 5 + did TEXT NOT NULL, 6 + cid TEXT NOT NULL, 7 + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), 8 + indexed_at TIMESTAMPTZ NOT NULL DEFAULT now() 9 + );
+1
migrations/004_bans_down.sql
··· 1 + DROP TABLE IF EXISTS bans;
+7
migrations/004_bans_up.sql
··· 1 + CREATE TABLE bans ( 2 + id SERIAL PRIMARY KEY, 3 + did TEXT NOT NULL, 4 + reason TEXT, 5 + till TIMESTAMPTZ, 6 + banned_at TIMESTAMPTZ NOT NULL DEFAULT now() 7 + );
+68
oauth/client.go
··· 1 + package oauth 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "errors" 8 + "fmt" 9 + "github.com/bluesky-social/indigo/api/atproto" 10 + "github.com/bluesky-social/indigo/atproto/atclient" 11 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 12 + lexutil "github.com/bluesky-social/indigo/lex/util" 13 + "io" 14 + "mime/multipart" 15 + "tangled.org/moth11.net/88x31/lex" 16 + ) 17 + 18 + func UploadBLOB(cs *oauth.ClientSession, file multipart.File, fileHeader *multipart.FileHeader, ctx context.Context) (*lexutil.BlobSchema, error) { 19 + client := cs.APIClient() 20 + fileBytes, err := io.ReadAll(file) 21 + if err != nil { 22 + return nil, errors.New("failed to readall: " + err.Error()) 23 + } 24 + fileReader := bytes.NewReader(fileBytes) 25 + req := atclient.NewAPIRequest("POST", "com.atproto.repo.uploadBlob", fileReader) 26 + contentType := fileHeader.Header.Get("Content-Type") 27 + if contentType == "" { 28 + contentType = "application/octet-stream" 29 + } 30 + req.Headers.Add("Content-Type", contentType) 31 + req.Headers.Add("Content-Length", fmt.Sprintf("%d", len(fileBytes))) 32 + resp, err := client.Do(ctx, req) 33 + if err != nil { 34 + return nil, err 35 + } 36 + defer resp.Body.Close() 37 + if resp.StatusCode != 200 { 38 + body, _ := io.ReadAll(resp.Body) 39 + return nil, fmt.Errorf("upload failed withy status %d: %s", resp.StatusCode, body) 40 + } 41 + var uploadResp struct { 42 + Blob *lexutil.BlobSchema `json:"blob"` 43 + } 44 + decoder := json.NewDecoder(resp.Body) 45 + err = decoder.Decode(&uploadResp) 46 + if err != nil { 47 + return nil, errors.New("failed to decode: " + err.Error()) 48 + } 49 + return uploadResp.Blob, nil 50 + } 51 + 52 + func CreateButton(cs *oauth.ClientSession, channel *lex.ButtonRecord, ctx context.Context) (uri string, cid string, err error) { 53 + c := cs.APIClient() 54 + body := map[string]any{ 55 + "collection": "org.xcvr.feed.channel", 56 + "repo": *c.AccountDID, 57 + "record": channel, 58 + } 59 + var out atproto.RepoCreateRecord_Output 60 + err = c.Post(ctx, "com.atproto.repo.createRecord", body, &out) 61 + if err != nil { 62 + err = errors.New("oops! failed to create a channel: " + err.Error()) 63 + return 64 + } 65 + uri = out.Uri 66 + cid = out.Cid 67 + return 68 + }
+15
oauth/jwks.go
··· 1 + package oauth 2 + 3 + import ( 4 + "github.com/bluesky-social/indigo/atproto/atcrypto" 5 + "os" 6 + ) 7 + 8 + func GetPrivateKey() (atcrypto.PrivateKeyExportable, error) { 9 + csk := os.Getenv("CLIENT_SECRET_KEY") 10 + key, err := atcrypto.ParsePrivateMultibase(csk) 11 + if err != nil { 12 + return nil, err 13 + } 14 + return key, nil 15 + }
+89
oauth/metadata.go
··· 1 + package oauth 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + ) 7 + 8 + var ( 9 + mi string 10 + mp string 11 + clientMetadata *ClientMetadata 12 + ) 13 + 14 + type ClientMetadata struct { 15 + ClientId string `json:"client_id"` 16 + ClientName string `json:"client_name"` 17 + ClientUri string `json:"client_uri"` 18 + LogoUri string `json:"logo_uri"` 19 + TosUri string `json:"tos_uri"` 20 + PolicyUrl string `json:"policy_url"` 21 + RedirectUris []string `json:"redirect_uris"` 22 + GrantTypes []string `json:"grant_types"` 23 + ResponseTypes []string `json:"response_types"` 24 + ApplicationType string `json:"application_type"` 25 + DPOPBoundAccessTokens bool `json:"dpop_bound_access_tokens"` 26 + JWKSUri string `json:"jwks_uri"` 27 + Scope string `json:"scope"` 28 + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` 29 + TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"` 30 + } 31 + 32 + func GetClientMetadata() ClientMetadata { 33 + if clientMetadata == nil { 34 + mi = os.Getenv("MY_IDENTITY") 35 + mp = os.Getenv("MY_METADATA_PATH") 36 + clientMetadata = &ClientMetadata{ 37 + ClientId: getClientId(), 38 + ClientName: getClientName(), 39 + ClientUri: getClientUri(), 40 + LogoUri: getLogoUri(), 41 + TosUri: getTOSUri(), 42 + PolicyUrl: getPolicyUri(), 43 + RedirectUris: []string{getOauthCallback()}, 44 + GrantTypes: []string{"authorization_code", "refresh_token"}, 45 + ResponseTypes: []string{"code"}, 46 + ApplicationType: "web", 47 + DPOPBoundAccessTokens: true, 48 + JWKSUri: getJWKSUri(), 49 + Scope: "atproto transition:generic", 50 + TokenEndpointAuthMethod: "private_key_jwt", 51 + TokenEndpointAuthSigningAlg: "ES256", 52 + } 53 + } 54 + return *clientMetadata 55 + } 56 + 57 + func getClientId() string { 58 + mi = os.Getenv("MY_IDENTITY") 59 + mp = os.Getenv("MY_METADATA_PATH") 60 + return fmt.Sprintf("https://%s%s", mi, mp) 61 + } 62 + 63 + func getClientName() string { 64 + return os.Getenv("MY_NAME") 65 + } 66 + 67 + func getClientUri() string { 68 + return fmt.Sprintf("https://%s", mi) 69 + } 70 + 71 + func getLogoUri() string { 72 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_LOGO_PATH")) 73 + } 74 + 75 + func getTOSUri() string { 76 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_TOS_PATH")) 77 + } 78 + 79 + func getPolicyUri() string { 80 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_POLICY_PATH")) 81 + } 82 + 83 + func getOauthCallback() string { 84 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_OAUTH_CALLBACK")) 85 + } 86 + 87 + func getJWKSUri() string { 88 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_JWKS_PATH")) 89 + }
+37
oauth/service.go
··· 1 + package oauth 2 + 3 + import ( 4 + "context" 5 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + "net/url" 8 + "os" 9 + "tangled.org/moth11.net/88x31/db" 10 + ) 11 + 12 + type Service struct { 13 + App *oauth.ClientApp 14 + } 15 + 16 + func NewService(store db.Store) (*Service, error) { 17 + config := oauth.NewPublicConfig(getClientId(), getOauthCallback(), []string{"atproto", "transition:generic"}) 18 + key, err := GetPrivateKey() 19 + if err != nil { 20 + return nil, err 21 + } 22 + err = config.SetClientSecret(key, os.Getenv("CLIENT_SECRET_KEY_ID")) 23 + app := oauth.NewClientApp(&config, store) 24 + return &Service{app}, nil 25 + } 26 + 27 + func (s *Service) StartAuthFlow(ctx context.Context, identifier string) (redirectURL string, err error) { 28 + return s.App.StartAuthFlow(ctx, identifier) 29 + } 30 + 31 + func (s *Service) OauthCallback(ctx context.Context, params url.Values) (sessdata *oauth.ClientSessionData, err error) { 32 + return s.App.ProcessCallback(ctx, params) 33 + } 34 + 35 + func (s *Service) ResumeSession(ctx context.Context, did syntax.DID, sessionId string) (sess *oauth.ClientSession, err error) { 36 + return s.App.ResumeSession(ctx, did, sessionId) 37 + }
+31
tmpl/base.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>{{ .Title }}</title> 7 + <style> 8 + .special { 9 + background: linear-gradient(88deg, #f00, #ff0, #0f0, #0ff, #00f, #f0f); 10 + } 11 + </style> 12 + </head> 13 + <body> 14 + <h1> 15 + Welcome to the <a href="/">88x31.store</a> 16 + </h1> 17 + <h2> 18 + TODAYS <span class="special">special~<span> EVERYTHING IS FREE 19 + </h2> 20 + <header> 21 + {{if .DID}} 22 + hi {{.DID}}!<a href="/logout">logout</a> 23 + {{else}} 24 + <a href="/login">login with oauth</a> 25 + {{end}} 26 + </header> 27 + <main> 28 + {{template "content" .}} 29 + </main> 30 + </body> 31 + </html>
+3
tmpl/button.html
··· 1 + {{define "content"}} 2 + {{template "buttonpart" .Button}} 3 + {{end}}
+7
tmpl/home.html
··· 1 + {{define "content"}} 2 + {{range .Buttons}} 3 + {{template "buttonpart" .}} 4 + {{else}} 5 + no buttons yet, <a href="/upload">post the first!</a> 6 + {{end}} 7 + {{end}}
+6
tmpl/login.html
··· 1 + {{define "content"}} 2 + <form action="/login" method="POST"> 3 + <div><input type="text" name="identifier" placeholder="alice.com" value=""/></div> 4 + <div><input type="submit" value="login with atproto oauth"/></div> 5 + </form> 6 + {{end}}
+5
tmpl/logout.html
··· 1 + {{define "content"}} 2 + <form action="/logout" method="POST"> 3 + <input type="submit" value="logout"/> 4 + </form> 5 + {{end}}
+5
tmpl/partial/buttonpart.html
··· 1 + {{define "buttonpart"}} 2 + <a href="{{.HREF}}"> 3 + <img src="/xrpc/store.88x31.getButton?uri={{.URI}}" alt="{{.Alt}}"/> 4 + </a> 5 + {{end}}
+6
tmpl/upload.html
··· 1 + {{define "content"}} 2 + <form action="/upload" method="POST"> 3 + <div><input type="file" value="select file" accept="image/png, image/jpg, image/gif"/></div> 4 + <div><input type="submit" value="upload"/></div> 5 + </form> 6 + {{end}}
+13
types/ban.go
··· 1 + package types 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + type Ban struct { 8 + Id int `json:"id"` 9 + Did string `json:"did"` 10 + Reason *string `json:"reason,omitempty"` 11 + Till *time.Time `json:"till,omitempty"` 12 + BannedAt time.Time `json:"bannedAt"` 13 + }
+24
types/lexicon.go
··· 1 + package types 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + type Button struct { 8 + URI string 9 + DID string 10 + BlobCID string 11 + BlobMIME string 12 + Alt *string 13 + HREF *string 14 + CID string 15 + PostedAt time.Time 16 + IndexedAt time.Time 17 + } 18 + 19 + type ButtonView struct { 20 + DID string `json:"did"` 21 + Src string `json:"src"` 22 + Alt *string `json:"alt,omitempty"` 23 + Href *string `json:"href,omitempty"` 24 + }