+5
-110
appview/db/strings.go
+5
-110
appview/db/strings.go
···
1
1
package db
2
2
3
3
import (
4
-
"bytes"
5
4
"database/sql"
6
5
"errors"
7
6
"fmt"
8
-
"io"
9
7
"strings"
10
8
"time"
11
-
"unicode/utf8"
12
9
13
-
"github.com/bluesky-social/indigo/atproto/syntax"
14
-
"tangled.org/core/api/tangled"
10
+
"tangled.org/core/appview/models"
15
11
)
16
12
17
-
type String struct {
18
-
Did syntax.DID
19
-
Rkey string
20
-
21
-
Filename string
22
-
Description string
23
-
Contents string
24
-
Created time.Time
25
-
Edited *time.Time
26
-
}
27
-
28
-
func (s *String) StringAt() syntax.ATURI {
29
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", s.Did, tangled.StringNSID, s.Rkey))
30
-
}
31
-
32
-
type StringStats struct {
33
-
LineCount uint64
34
-
ByteCount uint64
35
-
}
36
-
37
-
func (s String) Stats() StringStats {
38
-
lineCount, err := countLines(strings.NewReader(s.Contents))
39
-
if err != nil {
40
-
// non-fatal
41
-
// TODO: log this?
42
-
}
43
-
44
-
return StringStats{
45
-
LineCount: uint64(lineCount),
46
-
ByteCount: uint64(len(s.Contents)),
47
-
}
48
-
}
49
-
50
-
func (s String) Validate() error {
51
-
var err error
52
-
53
-
if utf8.RuneCountInString(s.Filename) > 140 {
54
-
err = errors.Join(err, fmt.Errorf("filename too long"))
55
-
}
56
-
57
-
if utf8.RuneCountInString(s.Description) > 280 {
58
-
err = errors.Join(err, fmt.Errorf("description too long"))
59
-
}
60
-
61
-
if len(s.Contents) == 0 {
62
-
err = errors.Join(err, fmt.Errorf("contents is empty"))
63
-
}
64
-
65
-
return err
66
-
}
67
-
68
-
func (s *String) AsRecord() tangled.String {
69
-
return tangled.String{
70
-
Filename: s.Filename,
71
-
Description: s.Description,
72
-
Contents: s.Contents,
73
-
CreatedAt: s.Created.Format(time.RFC3339),
74
-
}
75
-
}
76
-
77
-
func StringFromRecord(did, rkey string, record tangled.String) String {
78
-
created, err := time.Parse(record.CreatedAt, time.RFC3339)
79
-
if err != nil {
80
-
created = time.Now()
81
-
}
82
-
return String{
83
-
Did: syntax.DID(did),
84
-
Rkey: rkey,
85
-
Filename: record.Filename,
86
-
Description: record.Description,
87
-
Contents: record.Contents,
88
-
Created: created,
89
-
}
90
-
}
91
-
92
-
func AddString(e Execer, s String) error {
13
+
func AddString(e Execer, s models.String) error {
93
14
_, err := e.Exec(
94
15
`insert into strings (
95
16
did,
···
123
44
return err
124
45
}
125
46
126
-
func GetStrings(e Execer, limit int, filters ...filter) ([]String, error) {
127
-
var all []String
47
+
func GetStrings(e Execer, limit int, filters ...filter) ([]models.String, error) {
48
+
var all []models.String
128
49
129
50
var conditions []string
130
51
var args []any
···
167
88
defer rows.Close()
168
89
169
90
for rows.Next() {
170
-
var s String
91
+
var s models.String
171
92
var createdAt string
172
93
var editedAt sql.NullString
173
94
···
248
169
_, err := e.Exec(query, args...)
249
170
return err
250
171
}
251
-
252
-
func countLines(r io.Reader) (int, error) {
253
-
buf := make([]byte, 32*1024)
254
-
bufLen := 0
255
-
count := 0
256
-
nl := []byte{'\n'}
257
-
258
-
for {
259
-
c, err := r.Read(buf)
260
-
if c > 0 {
261
-
bufLen += c
262
-
}
263
-
count += bytes.Count(buf[:c], nl)
264
-
265
-
switch {
266
-
case err == io.EOF:
267
-
/* handle last line not having a newline at the end */
268
-
if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
269
-
count++
270
-
}
271
-
return count, nil
272
-
case err != nil:
273
-
return 0, err
274
-
}
275
-
}
276
-
}
+2
-2
appview/ingester.go
+2
-2
appview/ingester.go
···
594
594
return err
595
595
}
596
596
597
-
string := db.StringFromRecord(did, rkey, record)
597
+
string := models.StringFromRecord(did, rkey, record)
598
598
599
-
if err = string.Validate(); err != nil {
599
+
if err = i.Validator.ValidateString(&string); err != nil {
600
600
l.Error("invalid record", "err", err)
601
601
return err
602
602
}
+95
appview/models/string.go
+95
appview/models/string.go
···
1
+
package models
2
+
3
+
import (
4
+
"bytes"
5
+
"fmt"
6
+
"io"
7
+
"strings"
8
+
"time"
9
+
10
+
"github.com/bluesky-social/indigo/atproto/syntax"
11
+
"tangled.org/core/api/tangled"
12
+
)
13
+
14
+
type String struct {
15
+
Did syntax.DID
16
+
Rkey string
17
+
18
+
Filename string
19
+
Description string
20
+
Contents string
21
+
Created time.Time
22
+
Edited *time.Time
23
+
}
24
+
25
+
func (s *String) StringAt() syntax.ATURI {
26
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", s.Did, tangled.StringNSID, s.Rkey))
27
+
}
28
+
29
+
func (s *String) AsRecord() tangled.String {
30
+
return tangled.String{
31
+
Filename: s.Filename,
32
+
Description: s.Description,
33
+
Contents: s.Contents,
34
+
CreatedAt: s.Created.Format(time.RFC3339),
35
+
}
36
+
}
37
+
38
+
func StringFromRecord(did, rkey string, record tangled.String) String {
39
+
created, err := time.Parse(record.CreatedAt, time.RFC3339)
40
+
if err != nil {
41
+
created = time.Now()
42
+
}
43
+
return String{
44
+
Did: syntax.DID(did),
45
+
Rkey: rkey,
46
+
Filename: record.Filename,
47
+
Description: record.Description,
48
+
Contents: record.Contents,
49
+
Created: created,
50
+
}
51
+
}
52
+
53
+
type StringStats struct {
54
+
LineCount uint64
55
+
ByteCount uint64
56
+
}
57
+
58
+
func (s String) Stats() StringStats {
59
+
lineCount, err := countLines(strings.NewReader(s.Contents))
60
+
if err != nil {
61
+
// non-fatal
62
+
// TODO: log this?
63
+
}
64
+
65
+
return StringStats{
66
+
LineCount: uint64(lineCount),
67
+
ByteCount: uint64(len(s.Contents)),
68
+
}
69
+
}
70
+
71
+
func countLines(r io.Reader) (int, error) {
72
+
buf := make([]byte, 32*1024)
73
+
bufLen := 0
74
+
count := 0
75
+
nl := []byte{'\n'}
76
+
77
+
for {
78
+
c, err := r.Read(buf)
79
+
if c > 0 {
80
+
bufLen += c
81
+
}
82
+
count += bytes.Count(buf[:c], nl)
83
+
84
+
switch {
85
+
case err == io.EOF:
86
+
/* handle last line not having a newline at the end */
87
+
if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
88
+
count++
89
+
}
90
+
return count, nil
91
+
case err != nil:
92
+
return 0, err
93
+
}
94
+
}
95
+
}
+2
-3
appview/notify/merged_notifier.go
+2
-3
appview/notify/merged_notifier.go
···
3
3
import (
4
4
"context"
5
5
6
-
"tangled.org/core/appview/db"
7
6
"tangled.org/core/appview/models"
8
7
)
9
8
···
68
67
}
69
68
}
70
69
71
-
func (m *mergedNotifier) NewString(ctx context.Context, string *db.String) {
70
+
func (m *mergedNotifier) NewString(ctx context.Context, string *models.String) {
72
71
for _, notifier := range m.notifiers {
73
72
notifier.NewString(ctx, string)
74
73
}
75
74
}
76
75
77
-
func (m *mergedNotifier) EditString(ctx context.Context, string *db.String) {
76
+
func (m *mergedNotifier) EditString(ctx context.Context, string *models.String) {
78
77
for _, notifier := range m.notifiers {
79
78
notifier.EditString(ctx, string)
80
79
}
+4
-5
appview/notify/notifier.go
+4
-5
appview/notify/notifier.go
···
3
3
import (
4
4
"context"
5
5
6
-
"tangled.org/core/appview/db"
7
6
"tangled.org/core/appview/models"
8
7
)
9
8
···
23
22
24
23
UpdateProfile(ctx context.Context, profile *models.Profile)
25
24
26
-
NewString(ctx context.Context, s *db.String)
27
-
EditString(ctx context.Context, s *db.String)
25
+
NewString(ctx context.Context, s *models.String)
26
+
EditString(ctx context.Context, s *models.String)
28
27
DeleteString(ctx context.Context, did, rkey string)
29
28
}
30
29
···
48
47
49
48
func (m *BaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {}
50
49
51
-
func (m *BaseNotifier) NewString(ctx context.Context, s *db.String) {}
52
-
func (m *BaseNotifier) EditString(ctx context.Context, s *db.String) {}
50
+
func (m *BaseNotifier) NewString(ctx context.Context, s *models.String) {}
51
+
func (m *BaseNotifier) EditString(ctx context.Context, s *models.String) {}
53
52
func (m *BaseNotifier) DeleteString(ctx context.Context, did, rkey string) {}
+6
-6
appview/pages/pages.go
+6
-6
appview/pages/pages.go
···
477
477
478
478
type ProfileStringsParams struct {
479
479
LoggedInUser *oauth.User
480
-
Strings []db.String
480
+
Strings []models.String
481
481
Card *ProfileCard
482
482
Active string
483
483
}
···
1308
1308
Action string
1309
1309
1310
1310
// this is supplied in the case of editing an existing string
1311
-
String db.String
1311
+
String models.String
1312
1312
}
1313
1313
1314
1314
func (p *Pages) PutString(w io.Writer, params PutStringParams) error {
···
1318
1318
type StringsDashboardParams struct {
1319
1319
LoggedInUser *oauth.User
1320
1320
Card ProfileCard
1321
-
Strings []db.String
1321
+
Strings []models.String
1322
1322
}
1323
1323
1324
1324
func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error {
···
1327
1327
1328
1328
type StringTimelineParams struct {
1329
1329
LoggedInUser *oauth.User
1330
-
Strings []db.String
1330
+
Strings []models.String
1331
1331
}
1332
1332
1333
1333
func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error {
···
1339
1339
ShowRendered bool
1340
1340
RenderToggle bool
1341
1341
RenderedContents template.HTML
1342
-
String db.String
1343
-
Stats db.StringStats
1342
+
String models.String
1343
+
Stats models.StringStats
1344
1344
Owner identity.Identity
1345
1345
}
1346
1346
+2
-3
appview/posthog/notifier.go
+2
-3
appview/posthog/notifier.go
···
5
5
"log"
6
6
7
7
"github.com/posthog/posthog-go"
8
-
"tangled.org/core/appview/db"
9
8
"tangled.org/core/appview/models"
10
9
"tangled.org/core/appview/notify"
11
10
)
···
142
141
}
143
142
}
144
143
145
-
func (n *posthogNotifier) EditString(ctx context.Context, string *db.String) {
144
+
func (n *posthogNotifier) EditString(ctx context.Context, string *models.String) {
146
145
err := n.client.Enqueue(posthog.Capture{
147
146
DistinctId: string.Did.String(),
148
147
Event: "edit_string",
···
153
152
}
154
153
}
155
154
156
-
func (n *posthogNotifier) CreateString(ctx context.Context, string *db.String) {
155
+
func (n *posthogNotifier) CreateString(ctx context.Context, string models.String) {
157
156
err := n.client.Enqueue(posthog.Capture{
158
157
DistinctId: string.Did.String(),
159
158
Event: "create_string",
+3
-2
appview/strings/strings.go
+3
-2
appview/strings/strings.go
···
11
11
"tangled.org/core/api/tangled"
12
12
"tangled.org/core/appview/db"
13
13
"tangled.org/core/appview/middleware"
14
+
"tangled.org/core/appview/models"
14
15
"tangled.org/core/appview/notify"
15
16
"tangled.org/core/appview/oauth"
16
17
"tangled.org/core/appview/pages"
···
235
236
description := r.FormValue("description")
236
237
237
238
// construct new string from form values
238
-
entry := db.String{
239
+
entry := models.String{
239
240
Did: first.Did,
240
241
Rkey: first.Rkey,
241
242
Filename: filename,
···
318
319
319
320
description := r.FormValue("description")
320
321
321
-
string := db.String{
322
+
string := models.String{
322
323
Did: syntax.DID(user.Did),
323
324
Rkey: tid.TID(),
324
325
Filename: filename,
+27
appview/validator/string.go
+27
appview/validator/string.go
···
1
+
package validator
2
+
3
+
import (
4
+
"errors"
5
+
"fmt"
6
+
"unicode/utf8"
7
+
8
+
"tangled.org/core/appview/models"
9
+
)
10
+
11
+
func (v *Validator) ValidateString(s *models.String) error {
12
+
var err error
13
+
14
+
if utf8.RuneCountInString(s.Filename) > 140 {
15
+
err = errors.Join(err, fmt.Errorf("filename too long"))
16
+
}
17
+
18
+
if utf8.RuneCountInString(s.Description) > 280 {
19
+
err = errors.Join(err, fmt.Errorf("description too long"))
20
+
}
21
+
22
+
if len(s.Contents) == 0 {
23
+
err = errors.Join(err, fmt.Errorf("contents is empty"))
24
+
}
25
+
26
+
return err
27
+
}