backend for xcvr appview

refactor towards atproto appview

+558 -164
+5 -1
lexicons/org/xcvr/feed/defs.json
··· 4 4 "defs": { 5 5 "channelView": { 6 6 "type": "object", 7 - "required": ["uri", "host", "creator", "title", "connectedCount"], 7 + "required": ["uri", "host", "creator", "title"], 8 8 "properties": { 9 9 "uri": { 10 10 "type": "string", ··· 30 30 "connectedCount": { 31 31 "type": "integer", 32 32 "minimum": 0 33 + }, 34 + "createdAt": { 35 + "type": "string", 36 + "format": "datetime" 33 37 } 34 38 } 35 39 }
+1 -1
migrations/001_init.up.sql
··· 11 11 indexed_at TIMESTAMPTZ NOT NULL DEFAULT now() 12 12 ); 13 13 14 - CREATE TABLE did_handle ( 14 + CREATE TABLE did_handles ( 15 15 handle TEXT PRIMARY KEY, 16 16 did TEXT NOT NULL UNIQUE, 17 17 indexed_at TIMESTAMPTZ NOT NULL DEFAULT now()
+1 -1
migrations/002_populate.down.sql
··· 1 1 DELETE FROM messages; 2 2 DELETE FROM signets; 3 3 DELETE FROM channels; 4 - DELETE FROM did_handle; 4 + DELETE FROM did_handles; 5 5 DELETE FROM profiles;
+1 -1
migrations/002_populate.up.sql
··· 3 3 ('did:example:alice', 'Alice Example', 'alice', 'Chilling', 'bafybeib6...', 'image/png', 16711680, 'at://did:example:alice/app.bsky.actor.profile/self', 'cid1'), 4 4 ('did:example:bob', 'Bob Example', 'bobby', 'Working hard', 'bafybeib7...', 'image/jpeg', 65280, 'at://did:example:bob/app.bsky.actor.profile/self', 'cid2'); 5 5 6 - INSERT INTO did_handle (handle, did) 6 + INSERT INTO did_handles (handle, did) 7 7 VALUES 8 8 ('alice.com', 'did:example:alice'), 9 9 ('bob.net', 'did:example:bob');
+73 -160
server/cmd/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "encoding/json" 5 - "fmt" 6 4 "net/http" 7 5 "os" 8 - "slices" 9 - "sync" 10 - "time" 11 - "unicode/utf8" 6 + "context" 7 + "xcvr-backend/internal/db" 8 + "xcvr-backend/internal/handler" 9 + "xcvr-backend/internal/log" 12 10 13 - "github.com/rachel-mp4/lrc/lrcd/pkg/lrcd" 14 - ) 15 - 16 - var ( 17 - channelsMu sync.Mutex 18 - bandToServer map[string]*lrcd.Server 19 - channels []channel 11 + "github.com/joho/godotenv" 20 12 ) 21 13 22 - type channel struct { 23 - Band string `json:"band"` 24 - Sign string `json:"sign"` 25 - } 26 14 27 15 func main() { 28 - bandToServer = make(map[string]*lrcd.Server) 29 - channels = make([]channel, 0) 30 - createChannel(channel{Band: "general", Sign: "hi"}, false) 31 - createChannel(channel{Band: "sneep", Sign: "snirp"}, false) 32 - fmt.Println("hello world") 33 - mux := http.NewServeMux() 34 - mux.HandleFunc("GET /{band}/ws", acceptWebsocket) 35 - mux.HandleFunc("GET /xrpc/getChannels", getChannels) 36 - mux.HandleFunc("POST /xrpc/initChannel", initChannel) 16 + logger := log.New(os.Stdout, true) 37 17 38 - http.ListenAndServe(":8080", withCORSAll(mux)) 39 - } 40 - 41 - func acceptWebsocket(w http.ResponseWriter, r *http.Request) { 42 - band := r.PathValue("band") 43 - server, ok := bandToServer[band] 44 - if !ok { 45 - http.NotFound(w, r) 46 - return 18 + gdeerr := godotenv.Load("../.env") 19 + if gdeerr != nil { 20 + logger.Println("i think you should make a .env file in the xcvr-backend directory !\n\nExample contents:\n-------------------------------------------------------------------\nPOSTGRES_USER=xcvr\nPOSTGRES_PASSWORD=secret\nPOSTGRES_DB=xcvrdb\nPOSTGRES_PORT=15432\n-------------------------------------------------------------------\n\nGood luck !\n\n") 21 + panic(gdeerr) 47 22 } 48 - h := server.WSHandler() 49 - h(w, r) 50 - } 51 - 52 - func initChannel(w http.ResponseWriter, r *http.Request) { 53 - decoder := json.NewDecoder(r.Body) 54 - var c channel 55 - err := decoder.Decode(&c) 23 + conn, err := db.Init() 24 + defer conn.Close(context.Background()) 56 25 if err != nil { 57 - http.Error(w, "invalid json", http.StatusBadRequest) 58 - } 59 - switch isValidInit(c) { 60 - case ieNoBand: 61 - http.Error(w, "must give a band", http.StatusBadRequest) 62 - return 63 - case ieLongBand: 64 - http.Error(w, "band must be shorter than 32 bytes", http.StatusBadRequest) 65 - return 66 - case ieCollision: 67 - http.Error(w, "band must be unique", http.StatusBadRequest) 68 - return 69 - case ieLongSign: 70 - http.Error(w, "sign must be shorter than 51 code points", http.StatusBadRequest) 71 - return 72 - case ieOK: 73 - c, err = createChannel(c, false) 74 - } 75 - if err != nil { 76 - http.Error(w, "uh oh", http.StatusTeapot) 77 - } 78 - fmt.Printf("created a channel on band: %s and call sign: %s\n", c.Band, c.Sign) 79 - encoder := json.NewEncoder(w) 80 - encoder.Encode(c) 81 - } 82 - 83 - type initError = int 84 - 85 - const ( 86 - ieOK initError = iota 87 - ieNoBand 88 - ieLongBand 89 - ieCollision 90 - ieLongSign 91 - ) 92 - 93 - // TODO: can changes to bandToServer after unlock create data race? 94 - func isValidInit(c channel) initError { 95 - if c.Band == "" { 96 - return ieNoBand 97 - } 98 - if len(c.Band) > 31 { 99 - return ieLongBand 100 - } 101 - channelsMu.Lock() 102 - _, ok := bandToServer[c.Band] 103 - channelsMu.Unlock() 104 - if ok { 105 - return ieCollision 106 - } 107 - if utf8.RuneCountInString(c.Sign) > 50 { 108 - return ieLongSign 109 - } 110 - return ieOK 111 - } 112 - 113 - func getChannels(w http.ResponseWriter, r *http.Request) { 114 - encoder := json.NewEncoder(w) 115 - err := encoder.Encode(channels) 116 - if err != nil { 26 + logger.Println("failed to init db") 117 27 panic(err) 118 28 } 29 + h := handler.New(conn, logger) 30 + http.ListenAndServe(":8080", h.WithCORSAll()) 31 + 119 32 } 120 33 121 - func createChannel(c channel, withDelete bool) (channel, error) { 122 - options := []lrcd.Option{ 123 - lrcd.WithWelcome(c.Sign), 124 - lrcd.WithLogging(os.Stdout, true), 125 - } 126 - ec := make(chan struct{}) 34 + // func initChannel(w http.ResponseWriter, r *http.Request) { 35 + // decoder := json.NewDecoder(r.Body) 36 + // var c channel 37 + // err := decoder.Decode(&c) 38 + // if err != nil { 39 + // http.Error(w, "invalid json", http.StatusBadRequest) 40 + // } 41 + // switch isValidInit(c) { 42 + // case ieNoBand: 43 + // http.Error(w, "must give a band", http.StatusBadRequest) 44 + // return 45 + // case ieLongBand: 46 + // http.Error(w, "band must be shorter than 32 bytes", http.StatusBadRequest) 47 + // return 48 + // case ieCollision: 49 + // http.Error(w, "band must be unique", http.StatusBadRequest) 50 + // return 51 + // case ieLongSign: 52 + // http.Error(w, "sign must be shorter than 51 code points", http.StatusBadRequest) 53 + // return 54 + // case ieOK: 55 + // c, err = createChannel(c, false) 56 + // } 57 + // if err != nil { 58 + // http.Error(w, "uh oh", http.StatusTeapot) 59 + // } 60 + // fmt.Printf("created a channel on band: %s and call sign: %s\n", c.Band, c.Sign) 61 + // encoder := json.NewEncoder(w) 62 + // encoder.Encode(c) 63 + // } 127 64 128 - if withDelete { 129 - options = append(options, lrcd.WithEmptyChannel(ec)) 130 - after := 10 * time.Second 131 - options = append(options, lrcd.WithEmptySignalAfter(after)) 132 - } 133 - server, err := lrcd.NewServer(options...) 65 + // type initError = int 134 66 135 - if err != nil { 136 - fmt.Println(err.Error()) 137 - return channel{}, err 138 - } 139 - fmt.Println("created", c.Band) 67 + // const ( 68 + // ieOK initError = iota 69 + // ieNoBand 70 + // ieLongBand 71 + // ieCollision 72 + // ieLongSign 73 + // ) 140 74 141 - err = server.Start() 142 - if err != nil { 143 - fmt.Println(err.Error()) 144 - return channel{}, err 145 - } 146 - fmt.Println("started", c.Band) 75 + // // TODO: can changes to bandToServer after unlock create data race? 76 + // func isValidInit(c channel) initError { 77 + // if c.Band == "" { 78 + // return ieNoBand 79 + // } 80 + // if len(c.Band) > 31 { 81 + // return ieLongBand 82 + // } 83 + // channelsMu.Lock() 84 + // _, ok := bandToServer[c.Band] 85 + // channelsMu.Unlock() 86 + // if ok { 87 + // return ieCollision 88 + // } 89 + // if utf8.RuneCountInString(c.Sign) > 50 { 90 + // return ieLongSign 91 + // } 92 + // return ieOK 93 + // } 147 94 148 - channelsMu.Lock() 149 - defer channelsMu.Unlock() 150 - bandToServer[c.Band] = server 151 - channels = append(channels, c) 152 - if withDelete { 153 - go func() { 154 - <-ec 155 - channelsMu.Lock() 156 - idx := slices.Index(channels, c) 157 - channels = slices.Delete(channels, idx, idx+1) 158 - err = bandToServer[c.Band].Stop() 159 - if err != nil { 160 - fmt.Println(err.Error()) 161 - } 162 - delete(bandToServer, c.Band) 163 - channelsMu.Unlock() 164 - fmt.Println("deleted", c.Band) 165 - }() 166 - } 167 - return c, nil 168 - } 169 95 170 - func withCORSAll(h http.Handler) http.Handler { 171 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 172 - fmt.Println("incoming request:", r.Method, r.URL.Path) 173 - w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173") 174 - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 175 - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 176 - if r.Method == "OPTIONS" { 177 - w.WriteHeader(http.StatusNoContent) 178 - return 179 - } 180 - h.ServeHTTP(w, r) 181 - }) 182 - }
+6
server/go.mod
··· 6 6 7 7 require ( 8 8 github.com/gorilla/websocket v1.5.3 // indirect 9 + github.com/jackc/pgpassfile v1.0.0 // indirect 10 + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 11 + github.com/jackc/pgx/v5 v5.7.4 12 + github.com/joho/godotenv v1.5.1 9 13 github.com/rachel-mp4/lrc/lrc v0.0.0-20250408013928-75dc71a6060f // indirect 14 + golang.org/x/crypto v0.31.0 // indirect 15 + golang.org/x/text v0.21.0 // indirect 10 16 ) 11 17 12 18 replace github.com/rachel-mp4/lrc/lrcd => ../../lrc/lrcd
+30
server/go.sum
··· 1 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 1 4 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 2 5 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 6 + github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 7 + github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 8 + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 9 + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 10 + github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= 11 + github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 12 + github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 13 + github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 14 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 15 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 16 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 20 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 + github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 22 + github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 23 + golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 24 + golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 25 + golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 26 + golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 27 + golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 28 + golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 29 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
server/internal/db/.DS_Store

This is a binary file and will not be displayed.

+115
server/internal/db/db.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "xcvr-backend/internal/types" 7 + "os" 8 + 9 + "github.com/jackc/pgx/v5" 10 + ) 11 + 12 + func Init() (*pgx.Conn, error) { 13 + dbuser := os.Getenv("POSTGRES_USER") 14 + dbpass := os.Getenv("POSTGRES_PASSWORD") 15 + dbhost := "localhost" 16 + dbport := os.Getenv("POSTGRES_PORT") 17 + dbdb := os.Getenv("POSTGRES_DB") 18 + dburl := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dbuser, dbpass, dbhost, dbport, dbdb) 19 + conn, err := pgx.Connect(context.Background(), dburl) 20 + if err != nil { 21 + return conn, err 22 + } 23 + pingErr := conn.Ping(context.Background()) 24 + if pingErr != nil { 25 + return conn, pingErr 26 + } 27 + fmt.Println("connected!") 28 + return conn, nil 29 + } 30 + 31 + func GetMessages(channelURI string, limit int,ctx context.Context, db *pgx.Conn) ([]types.Message, error) { 32 + rows, err := db.Query(ctx, ` 33 + SELECT 34 + m.uri, m.did, m.signet_uri, m.body, m.nick, m.color, m.posted_at 35 + FROM messages m 36 + JOIN signets s ON m.signet_uri = s.uri 37 + WHERE s.channel_uri = $1 38 + ORDER BY s.message_id DESC 39 + LIMIT $2 40 + `, channelURI, limit) 41 + if err != nil { 42 + return nil, err 43 + } 44 + var msgs = make([]types.Message, 0, limit) 45 + for rows.Next() { 46 + var msg types.Message 47 + err := rows.Scan(&msg.URI, &msg.DID, &msg.SignetURI, &msg.Body, &msg.Nick, &msg.PostedAt) 48 + if err != nil { 49 + return nil, err 50 + } 51 + msgs = append(msgs, msg) 52 + } 53 + return msgs, nil 54 + } 55 + 56 + func GetChannels(limit int, ctx context.Context, db *pgx.Conn) ([]types.Channel, error) { 57 + rows, err := db.Query(ctx, ` 58 + SELECT 59 + c.uri, c.did, c.host, c.title, c.topic, c.created_at 60 + FROM channels c 61 + ORDER BY s.message_id DESC 62 + LIMIT $2 63 + `, limit) 64 + if err != nil { 65 + return nil, err 66 + } 67 + var chans = make([]types.Channel, 0, limit) 68 + for rows.Next() { 69 + var c types.Channel 70 + err := rows.Scan(&c.URI, &c.DID, &c.Host, &c.Title, &c.Topic, &c.CreatedAt) 71 + if err != nil { 72 + return nil, err 73 + } 74 + chans = append(chans, c) 75 + } 76 + return chans, nil 77 + } 78 + 79 + func GetChannelViews(limit int, ctx context.Context, db *pgx.Conn) ([]types.ChannelView, error) { 80 + rows, err := db.Query(ctx, ` 81 + SELECT 82 + channels.uri, 83 + channels.host, 84 + channels.title, 85 + channels.topic, 86 + channels.created_at, 87 + did_handles.did, 88 + did_handles.handle, 89 + profiles.display_name, 90 + profiles.status, 91 + profiles.color, 92 + profiles.avatar_cid 93 + FROM channels 94 + LEFT JOIN profiles ON channels.did = profiles.did 95 + LEFT JOIN did_handles ON profiles.did = did_handles.did 96 + ORDER BY channels.created_at DESC 97 + LIMIT $1 98 + `, limit) 99 + if err != nil { 100 + return nil, err 101 + } 102 + var chans = make([]types.ChannelView, 0, limit) 103 + for rows.Next() { 104 + var c types.ChannelView 105 + var p types.ProfileView 106 + err := rows.Scan(&c.URI, &c.Host, &c.Title, &c.Topic, &c.CreatedAt, &p.DID, &p.Handle, &p.DisplayName, &p.Status, &p.Color, &p.Avatar) 107 + if err != nil { 108 + return nil, err 109 + } 110 + c.Creator = p 111 + chans = append(chans, c) 112 + } 113 + return chans, nil 114 + } 115 +
server/internal/handler/.DS_Store

This is a binary file and will not be displayed.

+107
server/internal/handler/handler.go
··· 1 + package handler 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + "strconv" 7 + "xcvr-backend/internal/db" 8 + "xcvr-backend/internal/log" 9 + "xcvr-backend/internal/model" 10 + 11 + "github.com/jackc/pgx/v5" 12 + ) 13 + 14 + type Handler struct { 15 + db *pgx.Conn 16 + router *http.ServeMux 17 + logger log.Logger 18 + } 19 + 20 + func New(conn *pgx.Conn, logger log.Logger) *Handler { 21 + mux := http.NewServeMux() 22 + h := &Handler{conn, mux, logger} 23 + mux.HandleFunc("GET /lrc/{title}/ws", h.acceptWebsocket) 24 + mux.HandleFunc("GET /lrc/{user}/{title}/ws", h.acceptWebsocketUser) 25 + mux.HandleFunc("GET /xrpc/org.xcvr.feed.getChannels", h.getChannels) 26 + mux.HandleFunc("GET /xrpc/org.xcvr.lrc.getMessages", h.getMessages) 27 + mux.HandleFunc("POST /lrc/channel", postChannel) 28 + mux.HandleFunc("POST /lrc/message", postMessage) 29 + return h 30 + } 31 + 32 + func (h *Handler) acceptWebsocket(w http.ResponseWriter, r *http.Request) { 33 + title := r.PathValue("title") 34 + f, err := model.GetWSHandlerFrom(title, h.db) 35 + if err != nil { 36 + http.NotFound(w, r) 37 + h.logger.Deprintf("couldn't find server %s", title) 38 + return 39 + } 40 + f(w, r) 41 + } 42 + 43 + func (h *Handler) acceptWebsocketUser(w http.ResponseWriter, r *http.Request) { 44 + title := r.PathValue("title") 45 + user := r.PathValue("user") 46 + f, err := model.GetWSHandlerFrom(title, h.db) 47 + if err != nil { 48 + http.NotFound(w, r) 49 + h.logger.Deprintf("couldn't find user %s's server %s", user, title) 50 + return 51 + } 52 + f(w, r) 53 + } 54 + 55 + 56 + func (h *Handler) getMessages(w http.ResponseWriter, r *http.Request) { 57 + 58 + } 59 + 60 + func (h *Handler) getChannels(w http.ResponseWriter, r *http.Request) { 61 + limitstr := r.URL.Query().Get("limit") 62 + limit := 50 63 + if limitstr != "" { 64 + l, err := strconv.Atoi(limitstr) 65 + if err == nil { 66 + limit = max(min(l, 100),1) 67 + } 68 + } 69 + cvs, err := db.GetChannelViews(limit, r.Context(), h.db) 70 + if err != nil { 71 + serverError(w) 72 + h.logger.Printf("db.GetChannels failed! %s", err.Error()) 73 + return 74 + } 75 + encoder := json.NewEncoder(w) 76 + encoder.Encode(cvs) 77 + } 78 + 79 + func postChannel(w http.ResponseWriter, r *http.Request) { 80 + 81 + } 82 + 83 + func postMessage(w http.ResponseWriter, r *http.Request) { 84 + 85 + } 86 + 87 + func badRequest(w http.ResponseWriter) { 88 + http.Error(w, `{"error":"Invalid JSON","message":"Could not parse request body"}`,http.StatusBadRequest) 89 + } 90 + 91 + func serverError(w http.ResponseWriter) { 92 + http.Error(w, `{"error":"Internal server error","message":"Something went wrong"}`,http.StatusInternalServerError) 93 + } 94 + 95 + func (h *Handler) WithCORSAll() http.Handler { 96 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 + h.logger.Deprintf("incoming request: %s %s", r.Method, r.URL.Path) 98 + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173") 99 + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 100 + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 101 + if r.Method == "OPTIONS" { 102 + w.WriteHeader(http.StatusNoContent) 103 + return 104 + } 105 + h.router.ServeHTTP(w, r) 106 + }) 107 + }
+41
server/internal/log/log.go
··· 1 + package log 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "log" 7 + ) 8 + 9 + type Logger struct { 10 + debugLogger *log.Logger 11 + prodLogger *log.Logger 12 + } 13 + 14 + func New(w io.Writer, verbose bool) Logger { 15 + l := Logger{} 16 + l.prodLogger = log.New(w, "[log]", log.Ldate|log.Ltime) 17 + if verbose { 18 + l.debugLogger = log.New(w, "[debug]", log.Ldate|log.Ltime) 19 + } 20 + return l 21 + } 22 + 23 + func (l *Logger) Deprintln(s string) { 24 + if l.debugLogger != nil { 25 + l.debugLogger.Println(s) 26 + } 27 + } 28 + 29 + func (l *Logger) Deprintf(format string, args ...any) { 30 + l.Deprintln(fmt.Sprintf(format, args...)) 31 + } 32 + 33 + func (l *Logger) Println(s string) { 34 + if l.prodLogger != nil { 35 + l.prodLogger.Println(s) 36 + } 37 + } 38 + 39 + func (l *Logger) Printf(format string, args ...any) { 40 + l.Println(fmt.Sprintf(format, args...)) 41 + }
server/internal/model/.DS_Store

This is a binary file and will not be displayed.

+89
server/internal/model/channel.go
··· 1 + package model 2 + 3 + import ( 4 + "errors" 5 + "net/http" 6 + "sync" 7 + "time" 8 + "xcvr-backend/internal/types" 9 + 10 + "github.com/jackc/pgx/v5" 11 + "github.com/rachel-mp4/lrc/lrcd/pkg/lrcd" 12 + ) 13 + 14 + var ( 15 + channelsMu sync.Mutex 16 + channels = make([]channel, 0) 17 + uriToServer = make(map[string]*lrcd.Server) 18 + didToPView = make(map[string]*pView) 19 + ) 20 + 21 + type pView struct { 22 + profileView types.ProfileView 23 + lastUpdated time.Time 24 + } 25 + 26 + type channel struct { 27 + Title string `json:"title"` 28 + Topic string `json:"topic"` 29 + CreatedAt string `json:"createdAt"` 30 + Host string `json:"host"` 31 + } 32 + 33 + func GetWSHandlerFrom(uri string, db *pgx.Conn) (http.HandlerFunc, error) { 34 + server, ok := uriToServer[uri] 35 + if !ok { 36 + return nil, errors.New("channel does not exist") 37 + } 38 + return server.WSHandler(), nil 39 + } 40 + 41 + // func CreateChannel(title string, topic string) error { 42 + // c := channel{Title: title, Topic: topic} 43 + // _, err := createChannel(c) 44 + // return err 45 + // } 46 + 47 + // func createChannel(c channel) (channel, error) { 48 + // options := []lrcd.Option{ 49 + // lrcd.WithWelcome(c.Title), 50 + // lrcd.WithLogging(os.Stdout, true), 51 + // } 52 + // ec := make(chan struct{}) 53 + 54 + // server, err := lrcd.NewServer(options...) 55 + 56 + // if err != nil { 57 + // fmt.Println(err.Error()) 58 + // return channel{}, err 59 + // } 60 + // fmt.Println("created", c.Title) 61 + 62 + // err = server.Start() 63 + // if err != nil { 64 + // fmt.Println(err.Error()) 65 + // return channel{}, err 66 + // } 67 + // fmt.Println("started", c.Title) 68 + 69 + // channelsMu.Lock() 70 + // defer channelsMu.Unlock() 71 + // uriToServer[c.Band] = server 72 + // channels = append(channels, c) 73 + // if withDelete { 74 + // go func() { 75 + // <-ec 76 + // channelsMu.Lock() 77 + // idx := slices.Index(channels, c) 78 + // channels = slices.Delete(channels, idx, idx+1) 79 + // err = bandToServer[c.Band].Stop() 80 + // if err != nil { 81 + // fmt.Println(err.Error()) 82 + // } 83 + // delete(bandToServer, c.Band) 84 + // channelsMu.Unlock() 85 + // fmt.Println("deleted", c.Band) 86 + // }() 87 + // } 88 + // return c, nil 89 + // }
+89
server/internal/types/lexicons.go
··· 1 + package types 2 + 3 + import "time" 4 + 5 + type Profile struct { 6 + DID string 7 + DisplayName string 8 + DefaultNick string 9 + Status *string 10 + AvatarCID *string 11 + AvatarMIME *string 12 + Color uint32 13 + URI string 14 + CID string 15 + IndexedAt time.Time 16 + } 17 + 18 + type ProfileView struct { 19 + DID string `json:"did"` 20 + Handle string `json:"handle"` 21 + DisplayName *string `json:"displayName,omitempty"` 22 + Status *string `json:"status,omitempty"` 23 + Color *uint32 `json:"color,omitempty"` 24 + Avatar *string `json:"avatar,omitempty"` 25 + } 26 + 27 + type DIDHandle struct { 28 + Handle string 29 + DID string 30 + IndexedAt time.Time 31 + } 32 + 33 + type Channel struct { 34 + URI string 35 + CID string 36 + DID string 37 + Host string 38 + Title string 39 + Topic *string 40 + CreatedAt time.Time 41 + IndexedAt time.Time 42 + } 43 + 44 + type GetChannelRequest struct { 45 + Limit *int `json:"limit,omitempty"` 46 + Cursor *string `json:"cursor,omitempty"` 47 + } 48 + 49 + type ChannelView struct { 50 + URI string `json:"uri"` 51 + Host string `json:"host"` 52 + Creator ProfileView `json:"creator"` 53 + Title string `json:"title"` 54 + ConnectedCount *int `json:"int"` 55 + Topic *string `json:"topic"` 56 + CreatedAt time.Time `json:"createdAt"` 57 + } 58 + 59 + type Signet struct { 60 + URI string 61 + DID string 62 + ChannelURI string 63 + MessageID uint32 64 + CID string 65 + StartedAt time.Time 66 + IndexedAt time.Time 67 + } 68 + 69 + type Message struct { 70 + URI string 71 + DID string 72 + SignetURI string 73 + Body string 74 + Nick string 75 + Color uint32 76 + CID string 77 + PostedAt time.Time 78 + IndexedAt time.Time 79 + } 80 + 81 + type MessageView struct { 82 + URI string `json:"uri"` 83 + Author ProfileView `json:"author"` 84 + Body string `json:"body"` 85 + Nick string `json:"nick,omitempty"` 86 + Color int `json:"color"` 87 + StartedAt time.Time `json:"startedAt"` 88 + PostedAt time.Time `json:"postedAt"` 89 + }