+49
lexicons/org/xcvr/lrc/media.json
+49
lexicons/org/xcvr/lrc/media.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "org.xcvr.lrc.media",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A piece of media",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["signetURI", "media"],
12
+
"properties": {
13
+
"signetURI": {
14
+
"type": "string",
15
+
"format": "at-uri"
16
+
},
17
+
"media": {
18
+
"type": "union",
19
+
"required": ["image", "alt"],
20
+
"properties": {
21
+
"image": {
22
+
"type": "blob",
23
+
"accept": ["image/*"],
24
+
"maxSize": 1000000
25
+
},
26
+
"alt": {
27
+
"type": "string",
28
+
"description": "Alt text description of the image, for accessibility."
29
+
}
30
+
}
31
+
},
32
+
"color": {
33
+
"type": "integer",
34
+
"minimum": 0,
35
+
"maximum": 16777215
36
+
},
37
+
"nick": {
38
+
"type": "string",
39
+
"maxLength": 16,
40
+
},
41
+
"postedAt": {
42
+
"type": "string",
43
+
"format": "datetime"
44
+
}
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
+3
-4
lexicons/org/xcvr/lrc/message.json
+3
-4
lexicons/org/xcvr/lrc/message.json
···
14
14
"format": "at-uri"
15
15
},
16
16
"body": {
17
-
"type": "string",
17
+
"type": "string"
18
18
},
19
19
"nick": {
20
20
"type": "string",
21
-
"maxLength": 16,
22
-
"default": "wanderer"
21
+
"maxLength": 16
23
22
},
24
23
"color": {
25
24
"type": "integer",
···
34
33
}
35
34
}
36
35
}
37
-
}
36
+
}
+2
migrations/005_initimages.down.sql
+2
migrations/005_initimages.down.sql
+16
migrations/005_initimages.up.sql
+16
migrations/005_initimages.up.sql
···
1
+
CREATE TABLE medias (
2
+
uri TEXT PRIMARY KEY,
3
+
did TEXT NOT NULL,
4
+
signet_uri TEXT NOT NULL,
5
+
FOREIGN KEY (signet_uri) REFERENCES signets(uri) ON DELETE CASCADE,
6
+
media_cid TEXT,
7
+
media_mime TEXT,
8
+
alt TEXT,
9
+
nick TEXT NOT NULL,
10
+
color INTEGER CHECK (color BETWEEN 0 AND 16777215),
11
+
cid TEXT NOT NULL,
12
+
posted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
13
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT now()
14
+
);
15
+
16
+
CREATE INDEX ON medias (signet_uri);
+99
server/internal/db/lexicon.go
+99
server/internal/db/lexicon.go
···
287
287
`, uri)
288
288
return err
289
289
}
290
+
291
+
func (s *Store) StoreImage(image *types.Image, ctx context.Context) error {
292
+
_, err := s.pool.Exec(ctx, `INSERT INTO medias (
293
+
uri,
294
+
did,
295
+
signet_uri,
296
+
image_cid,
297
+
image_mime,
298
+
alt,
299
+
nick,
300
+
color,
301
+
cid,
302
+
posted_at
303
+
) VALUES (
304
+
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
305
+
) ON CONFLICT (uri) DO NOTHING`,
306
+
image.URI,
307
+
image.DID,
308
+
image.SignetURI,
309
+
image.ImageCID,
310
+
image.ImageMIME,
311
+
image.Alt,
312
+
image.Nick,
313
+
image.Color,
314
+
image.CID,
315
+
image.PostedAt)
316
+
if err != nil {
317
+
return errors.New("effor storing image: " + err.Error())
318
+
}
319
+
return nil
320
+
}
321
+
322
+
func (s *Store) UpdateImage(image *types.Image, ctx context.Context) error {
323
+
_, err := s.pool.Exec(ctx, `INSERT INTO medias (
324
+
uri,
325
+
did,
326
+
signet_uri,
327
+
image_cid,
328
+
image_mime,
329
+
alt,
330
+
nick,
331
+
color,
332
+
cid,
333
+
posted_at
334
+
) VALUES (
335
+
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
336
+
)`,
337
+
image.URI,
338
+
image.DID,
339
+
image.SignetURI,
340
+
image.ImageCID,
341
+
image.ImageMIME,
342
+
image.Alt,
343
+
image.Nick,
344
+
image.Color,
345
+
image.CID,
346
+
image.PostedAt)
347
+
if err != nil {
348
+
return errors.New("effor updating image: " + err.Error())
349
+
}
350
+
return nil
351
+
}
352
+
353
+
func (s *Store) DeleteImage(uri string, ctx context.Context) error {
354
+
_, err := s.pool.Exec(ctx, `DELETE from medias m WHERE m.uri = $1`, uri)
355
+
if err != nil {
356
+
return errors.New("bep bep bop: " + err.Error())
357
+
}
358
+
return nil
359
+
}
360
+
361
+
func (s *Store) GetImage(uri string, ctx context.Context) (*types.Image, error) {
362
+
row := s.pool.QueryRow(ctx, `SELECT FROM medias (
363
+
did,
364
+
signet_uri,
365
+
media_cid,
366
+
media_mime,
367
+
alt,
368
+
nick,
369
+
color,
370
+
cid,
371
+
posted_at
372
+
) WHERE uri = $1`, uri)
373
+
var image types.Image
374
+
err := row.Scan(&image.DID,
375
+
&image.SignetURI,
376
+
&image.ImageCID,
377
+
&image.ImageMIME,
378
+
&image.Alt,
379
+
&image.Nick,
380
+
&image.Color,
381
+
&image.CID,
382
+
&image.PostedAt)
383
+
if err != nil {
384
+
return nil, errors.New("effor storing image: " + err.Error())
385
+
}
386
+
image.URI = uri
387
+
return &image, nil
388
+
}
+4
server/internal/handler/handler.go
+4
server/internal/handler/handler.go
···
31
31
mux.HandleFunc("DELETE /lrc/{user}/{rkey}/ws", h.oauthMiddleware(h.deleteChannel))
32
32
mux.HandleFunc("POST /lrc/channel", h.oauthMiddleware(h.postChannel))
33
33
mux.HandleFunc("POST /lrc/message", h.oauthMiddleware(h.postMessage))
34
+
mux.HandleFunc("POST /lrc/image", h.oauthMiddleware(h.uploadImage))
35
+
mux.HandleFunc("POST /lrc/media", h.oauthMiddleware(h.postMedia))
36
+
// mux.HandleFunc("GET /lrc/image", h.getImage)
37
+
mux.HandleFunc("POST /lrc/imagePub", h.oauthMiddleware(h.postImagePub))
34
38
mux.HandleFunc("POST /lrc/mymessage", h.postMyMessage)
35
39
// xcvr handlers
36
40
mux.HandleFunc("POST /xcvr/profile", h.oauthMiddleware(h.postProfile))
+92
-1
server/internal/handler/lrcHandlers.go
+92
-1
server/internal/handler/lrcHandlers.go
···
5
5
"encoding/json"
6
6
"errors"
7
7
"fmt"
8
-
atoauth "github.com/bluesky-social/indigo/atproto/auth/oauth"
9
8
"net/http"
10
9
"os"
11
10
"rvcx/internal/atputils"
12
11
"rvcx/internal/types"
12
+
"strings"
13
+
14
+
atoauth "github.com/bluesky-social/indigo/atproto/auth/oauth"
13
15
)
14
16
15
17
func (h *Handler) acceptWebsocket(w http.ResponseWriter, r *http.Request) {
···
138
140
}
139
141
f(w, r)
140
142
}
143
+
144
+
func (h *Handler) uploadImage(cs *atoauth.ClientSession, w http.ResponseWriter, r *http.Request) {
145
+
if cs == nil {
146
+
h.badRequest(w, errors.New("must be authorized to post image"))
147
+
return
148
+
}
149
+
err := r.ParseMultipartForm(1 << 20)
150
+
if err != nil {
151
+
h.badRequest(w, errors.New("beep bop bad image: "+err.Error()))
152
+
return
153
+
}
154
+
file, fheader, err := r.FormFile("image")
155
+
if err != nil {
156
+
h.badRequest(w, errors.New("failed to formfile: "+err.Error()))
157
+
return
158
+
}
159
+
defer file.Close()
160
+
ct := fheader.Header.Get("Content-Type")
161
+
if !strings.HasPrefix(ct, "image/") {
162
+
h.badRequest(w, errors.New("must post an image"))
163
+
return
164
+
}
165
+
blob, err := h.rm.PostImage(cs, file, r.Context())
166
+
if err != nil {
167
+
h.serverError(w, errors.New("failed to upload: "+err.Error()))
168
+
return
169
+
}
170
+
w.Header().Set("Content-Type", "application/json")
171
+
encoder := json.NewEncoder(w)
172
+
encoder.Encode(blob)
173
+
}
174
+
175
+
func (h *Handler) postMedia(cs *atoauth.ClientSession, w http.ResponseWriter, r *http.Request) {
176
+
if cs == nil {
177
+
h.badRequest(w, errors.New("must be authorized to post media"))
178
+
}
179
+
mr, err := parseMediaRequest(r)
180
+
if err != nil {
181
+
h.badRequest(w, err)
182
+
return
183
+
}
184
+
h.rm.PostMedia(cs, mr, r.Context())
185
+
}
186
+
187
+
func parseMediaRequest(r *http.Request) (*types.ParseMediaRequest, error) {
188
+
beep := json.NewDecoder(r.Body)
189
+
var mr types.ParseMediaRequest
190
+
err := beep.Decode(&mr)
191
+
if err != nil {
192
+
return nil, errors.New("A aaaaaa : " + err.Error())
193
+
}
194
+
return &mr, nil
195
+
}
196
+
197
+
// func (h *Handler) getImage(w http.ResponseWriter, r *http.Request) {
198
+
// vals := r.URL.Query()
199
+
// uri := vals.Get("uri")
200
+
// if uri == "" {
201
+
// h.badRequest(w, errors.New("must provide a did and cid"))
202
+
// return
203
+
// }
204
+
// image, err := h.db.GetImage(uri, r.Context())
205
+
// if err != nil {
206
+
// h.notFound(w, err)
207
+
// return
208
+
// }
209
+
// uploadDir := fmt.Sprintf("./uploads/%s", image.DID)
210
+
// _, err = os.Stat(uploadDir)
211
+
// if os.IsNotExist(err) {
212
+
// os.Mkdir(uploadDir, 0755)
213
+
// }
214
+
//
215
+
// imgPath := fmt.Sprintf("./uploads/%s", image.ImageCID)
216
+
// _, err = os.Stat(imgPath)
217
+
// if err != nil {
218
+
// syncGetBlob(image.DID, image.ImageCID)
219
+
// }
220
+
//
221
+
// img, err := os.Open(fmt.Sprintf("%s/%s", uploadDir, image.ImageCID))
222
+
// img.WriteTo(w)
223
+
// }
224
+
225
+
// func syncGetBlob(did string, cid *string) {
226
+
// //TODO: impl
227
+
// }
228
+
229
+
func (h *Handler) postImagePub(cs *atoauth.ClientSession, w http.ResponseWriter, r *http.Request) {
230
+
231
+
}
+25
server/internal/lex/types.go
+25
server/internal/lex/types.go
···
44
44
AuthorHandle string `json:"authorHandle" cborgen:"authorHandle"`
45
45
StartedAt *string `json:"startedAt,omitempty" cborgen:"startedAt,omitempty"`
46
46
}
47
+
48
+
type MediaRecord struct {
49
+
LexiconTypeID string `json:"$type,const=org.xcvr.lrc.media" cborgen:"$type,const=org.xcvr.lrc.media"`
50
+
SignetURI string `json:"signetURI" cborgen:"signetURI"`
51
+
Media Media `json:"media" cborgen:"media"`
52
+
Nick *string `json:"nick,omitempty" cborgen:"nick,omitempty"`
53
+
Color *uint64 `json:"color,omitempty" cborgen:"color,omitempty"`
54
+
PostedAt string `json:"postedAt" cborgen:"postedAt"`
55
+
}
56
+
57
+
type Media struct {
58
+
Image *Image
59
+
}
60
+
61
+
type Image struct {
62
+
LexiconTypeID string `json:"$type,const=org.xcvr.lrc.image" cborgen:"$type,const=org.xcvr.lrc.image"`
63
+
Alt string `json:"alt" cborgen:"alt"`
64
+
AspectRatio *AspectRatio `json:"aspectRatio,omitempty" cborgen:"aspectRatio,omitempty"`
65
+
Image *util.BlobSchema `json:"image,omitempty" cborgen:"image,omitempty"`
66
+
}
67
+
68
+
type AspectRatio struct {
69
+
Height int64 `json:"height" cborgen:"height"`
70
+
Width int64 `json:"width" cborgen:"width"`
71
+
}
+43
server/internal/oauth/oauthclient.go
+43
server/internal/oauth/oauthclient.go
···
4
4
"context"
5
5
"encoding/json"
6
6
"errors"
7
+
"fmt"
7
8
8
9
"github.com/bluesky-social/indigo/api/atproto"
9
10
"github.com/bluesky-social/indigo/atproto/auth/oauth"
11
+
atpclient "github.com/bluesky-social/indigo/atproto/client"
10
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
+
lexutil "github.com/bluesky-social/indigo/lex/util"
11
14
15
+
"mime/multipart"
12
16
"rvcx/internal/lex"
13
17
"rvcx/internal/log"
14
18
"rvcx/internal/types"
···
170
174
}
171
175
return profile, nil
172
176
}
177
+
178
+
func UploadBLOB(cs *oauth.ClientSession, file multipart.File, ctx context.Context) (*lexutil.BlobSchema, error) {
179
+
client := cs.APIClient()
180
+
181
+
req := atpclient.NewAPIRequest("POST", "com.atproto.repo.uploadBlob", file)
182
+
resp, err := client.Do(ctx, req)
183
+
if err != nil {
184
+
return nil, err
185
+
}
186
+
defer resp.Body.Close()
187
+
if resp.StatusCode != 200 {
188
+
return nil, fmt.Errorf("upload failed withy status %d", resp.StatusCode)
189
+
}
190
+
var result lexutil.BlobSchema
191
+
decoder := json.NewDecoder(resp.Body)
192
+
err = decoder.Decode(&result)
193
+
if err != nil {
194
+
return nil, errors.New("failed to decode: " + err.Error())
195
+
}
196
+
return &result, nil
197
+
}
198
+
199
+
func CreateXCVRMedia(cs *oauth.ClientSession, imr *lex.MediaRecord, ctx context.Context) (uri string, cid string, err error) {
200
+
c := cs.APIClient()
201
+
body := map[string]any{
202
+
"collection": "org.xcvr.lrc.message",
203
+
"repo": *c.AccountDID,
204
+
"record": imr,
205
+
}
206
+
var out atproto.RepoCreateRecord_Output
207
+
err = c.Post(ctx, "com.atproto.repo.createRecord", body, &out)
208
+
if err != nil {
209
+
err = errors.New("oops! failed to create a media: " + err.Error())
210
+
return
211
+
}
212
+
uri = out.Uri
213
+
cid = out.Cid
214
+
return
215
+
}
+91
server/internal/recordmanager/media.go
+91
server/internal/recordmanager/media.go
···
1
+
package recordmanager
2
+
3
+
import (
4
+
"context"
5
+
"errors"
6
+
atoauth "github.com/bluesky-social/indigo/atproto/auth/oauth"
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
lexutil "github.com/bluesky-social/indigo/lex/util"
9
+
"mime/multipart"
10
+
"rvcx/internal/lex"
11
+
"rvcx/internal/oauth"
12
+
"rvcx/internal/types"
13
+
"time"
14
+
)
15
+
16
+
func (rm *RecordManager) PostImage(cs *atoauth.ClientSession, file multipart.File, ctx context.Context) (*lexutil.BlobSchema, error) {
17
+
return oauth.UploadBLOB(cs, file, ctx)
18
+
}
19
+
20
+
func (rm *RecordManager) PostMedia(cs *atoauth.ClientSession, mr *types.ParseMediaRequest, ctx context.Context) error {
21
+
switch mr.Type {
22
+
case "image":
23
+
return rm.postImageRecord(cs, mr, ctx)
24
+
default:
25
+
return nil
26
+
}
27
+
}
28
+
29
+
func (rm *RecordManager) postImageRecord(cs *atoauth.ClientSession, mr *types.ParseMediaRequest, ctx context.Context) error {
30
+
imr, now, err := rm.validateImageRecord(mr)
31
+
if err != nil {
32
+
return errors.New("coudlnt validate media record: " + err.Error())
33
+
}
34
+
img, err := rm.createImageRecord(cs, imr, now, ctx)
35
+
if err != nil {
36
+
return errors.New("coudlnt validate media record: " + err.Error())
37
+
}
38
+
err = rm.db.StoreImage(img, ctx)
39
+
if err != nil {
40
+
return errors.New("beeped that up!: " + err.Error())
41
+
}
42
+
return nil
43
+
}
44
+
45
+
func (rm *RecordManager) validateImageRecord(mr *types.ParseMediaRequest) (*lex.MediaRecord, *time.Time, error) {
46
+
var imr lex.MediaRecord
47
+
imr.SignetURI = mr.SignetURI
48
+
imr.Nick = mr.Nick
49
+
cptr := mr.Color
50
+
if cptr != nil {
51
+
cnum := uint64(*cptr)
52
+
imr.Color = &cnum
53
+
}
54
+
imr.Media.Image = mr.Image
55
+
nowsyn := syntax.DatetimeNow()
56
+
imr.PostedAt = nowsyn.String()
57
+
nt := nowsyn.Time()
58
+
now := &nt
59
+
return &imr, now, nil
60
+
}
61
+
62
+
func (rm *RecordManager) createImageRecord(cs *atoauth.ClientSession, imr *lex.MediaRecord, now *time.Time, ctx context.Context) (*types.Image, error) {
63
+
uri, cid, err := oauth.CreateXCVRMedia(cs, imr, ctx)
64
+
if err != nil {
65
+
return nil, errors.New("beeped up: " + err.Error())
66
+
}
67
+
var img types.Image
68
+
img.URI = uri
69
+
img.DID = cs.Data.AccountDID.String()
70
+
img.SignetURI = imr.SignetURI
71
+
if imr.Media.Image != nil {
72
+
img.Alt = imr.Media.Image.Alt
73
+
if imr.Media.Image.Image != nil {
74
+
img.ImageMIME = &imr.Media.Image.Image.MimeType
75
+
icid := imr.Media.Image.Image.Ref.String()
76
+
img.ImageCID = &icid
77
+
}
78
+
}
79
+
img.Nick = imr.Nick
80
+
img.CID = cid
81
+
if imr.Color != nil {
82
+
c := uint32(*imr.Color)
83
+
img.Color = &c
84
+
}
85
+
if now != nil {
86
+
img.PostedAt = *now
87
+
} else {
88
+
img.PostedAt = time.Now()
89
+
}
90
+
return &img, nil
91
+
}
+23
server/internal/types/lexicons.go
+23
server/internal/types/lexicons.go
···
2
2
3
3
import (
4
4
"encoding/json"
5
+
"rvcx/internal/lex"
5
6
"time"
6
7
)
7
8
···
209
210
Messages []SignedMessageView `json:"messages"`
210
211
Cursor *string `json:"cursor,omitempty"`
211
212
}
213
+
214
+
type Image struct {
215
+
URI string
216
+
DID string
217
+
SignetURI string
218
+
ImageCID *string
219
+
ImageMIME *string
220
+
Alt string
221
+
Nick *string
222
+
Color *uint32
223
+
CID string
224
+
PostedAt time.Time
225
+
IndexedAt time.Time
226
+
}
227
+
228
+
type ParseMediaRequest struct {
229
+
Nick *string `json:"nick,omitempty"`
230
+
Color *uint32 `json:"color,omitempty"`
231
+
SignetURI string `json:"signetURI"`
232
+
Image *lex.Image `json:"image,omitempty"`
233
+
Type string `json:"type"`
234
+
}