@awiki/cli 0.0.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +44 -0
- package/.goreleaser.yml +44 -0
- package/AGENTS.md +60 -0
- package/CLAUDE.md +192 -0
- package/README.md +2 -0
- package/docs/architecture/awiki-command-v2.md +955 -0
- package/docs/architecture/awiki-skill-architecture.md +475 -0
- package/docs/architecture/awiki-v2-architecture.md +1063 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/cli-init.md +1008 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/output-format.md +407 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/overall-init.md +741 -0
- package/docs/harness/review-spec.md +474 -0
- package/docs/installation.md +372 -0
- package/docs/plan/awiki-v2-implementation-plan.md +903 -0
- package/docs/plan/phase-0/adr-index.md +56 -0
- package/docs/plan/phase-0/audit-findings.md +251 -0
- package/docs/plan/phase-0/capability-mapping.md +108 -0
- package/docs/plan/phase-0/implementation-constraints.md +363 -0
- package/docs/publish.md +169 -0
- package/go.mod +29 -0
- package/go.sum +73 -0
- package/internal/anpsdk/registry.go +63 -0
- package/internal/authsdk/session.go +351 -0
- package/internal/buildinfo/buildinfo.go +34 -0
- package/internal/cli/app.go +136 -0
- package/internal/cli/app_test.go +88 -0
- package/internal/cli/debug.go +104 -0
- package/internal/cli/group.go +263 -0
- package/internal/cli/id.go +473 -0
- package/internal/cli/init.go +134 -0
- package/internal/cli/msg.go +228 -0
- package/internal/cli/page.go +267 -0
- package/internal/cli/root.go +499 -0
- package/internal/cli/runtime.go +232 -0
- package/internal/cli/upgrade.go +60 -0
- package/internal/cmdmeta/catalog.go +203 -0
- package/internal/cmdmeta/catalog_test.go +21 -0
- package/internal/config/config.go +399 -0
- package/internal/config/config_test.go +104 -0
- package/internal/config/write.go +37 -0
- package/internal/content/service.go +314 -0
- package/internal/content/service_test.go +165 -0
- package/internal/content/types.go +44 -0
- package/internal/docs/topics.go +110 -0
- package/internal/doctor/doctor.go +306 -0
- package/internal/identity/client.go +267 -0
- package/internal/identity/did.go +85 -0
- package/internal/identity/did_test.go +50 -0
- package/internal/identity/layout.go +206 -0
- package/internal/identity/legacy.go +378 -0
- package/internal/identity/public.go +70 -0
- package/internal/identity/public_test.go +73 -0
- package/internal/identity/readiness.go +74 -0
- package/internal/identity/service.go +826 -0
- package/internal/identity/store.go +385 -0
- package/internal/identity/store_test.go +180 -0
- package/internal/identity/types.go +204 -0
- package/internal/message/auth.go +167 -0
- package/internal/message/group_service.go +838 -0
- package/internal/message/group_wire.go +350 -0
- package/internal/message/group_wire_test.go +67 -0
- package/internal/message/helpers.go +61 -0
- package/internal/message/http_client.go +334 -0
- package/internal/message/proof.go +156 -0
- package/internal/message/proof_test.go +61 -0
- package/internal/message/service.go +696 -0
- package/internal/message/service_test.go +97 -0
- package/internal/message/types.go +155 -0
- package/internal/message/wire.go +100 -0
- package/internal/message/wire_test.go +49 -0
- package/internal/message/ws_proxy_client.go +151 -0
- package/internal/output/output.go +350 -0
- package/internal/output/output_test.go +48 -0
- package/internal/runtime/config.go +117 -0
- package/internal/runtime/config_test.go +46 -0
- package/internal/runtime/listener/files.go +65 -0
- package/internal/runtime/listener/manager.go +142 -0
- package/internal/runtime/listener/server.go +983 -0
- package/internal/runtime/listener/server_test.go +319 -0
- package/internal/runtime/listener/sysproc_unix.go +17 -0
- package/internal/runtime/listener/sysproc_windows.go +13 -0
- package/internal/runtime/listener/types.go +21 -0
- package/internal/runtime/listener/wsclient.go +299 -0
- package/internal/runtime/listener/wsclient_test.go +41 -0
- package/internal/store/dao.go +632 -0
- package/internal/store/dao_test.go +87 -0
- package/internal/store/helpers.go +197 -0
- package/internal/store/import.go +499 -0
- package/internal/store/import_test.go +103 -0
- package/internal/store/open.go +71 -0
- package/internal/store/query.go +151 -0
- package/internal/store/schema.go +277 -0
- package/internal/store/schema_test.go +56 -0
- package/internal/store/types.go +177 -0
- package/internal/update/update.go +368 -0
- package/package.json +17 -0
- package/scripts/install.js +171 -0
- package/scripts/release/release-prerelease.sh +86 -0
- package/scripts/release/tag-release.sh +66 -0
- package/scripts/release/withdraw-release.sh +78 -0
- package/scripts/run.js +69 -0
- package/skills/README.md +32 -0
- package/skills/awiki-bundle/SKILL.md +76 -0
- package/skills/awiki-debug/SKILL.md +80 -0
- package/skills/awiki-group/SKILL.md +111 -0
- package/skills/awiki-id/SKILL.md +123 -0
- package/skills/awiki-msg/SKILL.md +131 -0
- package/skills/awiki-page/SKILL.md +93 -0
- package/skills/awiki-people/SKILL.md +66 -0
- package/skills/awiki-runtime/SKILL.md +137 -0
- package/skills/awiki-shared/SKILL.md +124 -0
- package/skills/awiki-workflow-discovery/SKILL.md +93 -0
- package/skills/awiki-workflow-onboarding/SKILL.md +119 -0
- package/skills/manifests/skills.yaml +260 -0
- package/skills/templates/bundle-skill-template.md +42 -0
- package/skills/templates/debug-skill-template.md +44 -0
- package/skills/templates/domain-skill-template.md +56 -0
- package/skills/templates/shared-skill-template.md +46 -0
- package/skills/templates/workflow-skill-template.md +46 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package listener
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"github.com/agentconnect/awiki-cli/internal/authsdk"
|
|
7
|
+
appconfig "github.com/agentconnect/awiki-cli/internal/config"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestNewWSClientDoesNotDoubleAppendWSEndpoint(t *testing.T) {
|
|
11
|
+
t.Parallel()
|
|
12
|
+
|
|
13
|
+
client, err := NewWSClient(&appconfig.Resolved{
|
|
14
|
+
MessageServiceURL: "http://127.0.0.1:18080",
|
|
15
|
+
MessageServiceWSURL: "ws://127.0.0.1:18080/ws",
|
|
16
|
+
}, dummyAuthSession())
|
|
17
|
+
if err != nil {
|
|
18
|
+
t.Fatalf("NewWSClient() error = %v", err)
|
|
19
|
+
}
|
|
20
|
+
if client.websocketURL != "ws://127.0.0.1:18080/ws" {
|
|
21
|
+
t.Fatalf("client.websocketURL = %q, want ws://127.0.0.1:18080/ws", client.websocketURL)
|
|
22
|
+
}
|
|
23
|
+
if client.requestURL != "http://127.0.0.1:18080/ws" {
|
|
24
|
+
t.Fatalf("client.requestURL = %q, want http://127.0.0.1:18080/ws", client.requestURL)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func TestAppendEndpointIfMissing(t *testing.T) {
|
|
29
|
+
t.Parallel()
|
|
30
|
+
|
|
31
|
+
if got := appendEndpointIfMissing("ws://127.0.0.1:18080", "/ws"); got != "ws://127.0.0.1:18080/ws" {
|
|
32
|
+
t.Fatalf("appendEndpointIfMissing() = %q", got)
|
|
33
|
+
}
|
|
34
|
+
if got := appendEndpointIfMissing("ws://127.0.0.1:18080/ws", "/ws"); got != "ws://127.0.0.1:18080/ws" {
|
|
35
|
+
t.Fatalf("appendEndpointIfMissing() = %q", got)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func dummyAuthSession() *authsdk.Session {
|
|
40
|
+
return authsdk.NewSession("", "", "alice", "did:wba:example.com:user:alice", "token", nil)
|
|
41
|
+
}
|
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
package store
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"database/sql"
|
|
6
|
+
"fmt"
|
|
7
|
+
"regexp"
|
|
8
|
+
"strings"
|
|
9
|
+
"time"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func StoreMessage(ctx context.Context, db *sql.DB, record MessageRecord) error {
|
|
13
|
+
if strings.TrimSpace(record.MsgID) == "" {
|
|
14
|
+
return fmt.Errorf("msg_id is required")
|
|
15
|
+
}
|
|
16
|
+
if strings.TrimSpace(record.ThreadID) == "" {
|
|
17
|
+
return fmt.Errorf("thread_id is required")
|
|
18
|
+
}
|
|
19
|
+
now := nowUTC()
|
|
20
|
+
_, err := db.ExecContext(ctx, `
|
|
21
|
+
INSERT OR IGNORE INTO messages
|
|
22
|
+
(msg_id, owner_did, thread_id, direction, sender_did, receiver_did, group_id, group_did,
|
|
23
|
+
content_type, content, title, server_seq, sent_at, stored_at, is_e2ee, is_read,
|
|
24
|
+
sender_name, metadata, credential_name)
|
|
25
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
26
|
+
record.MsgID,
|
|
27
|
+
normalizeOwnerDID(record.OwnerDID),
|
|
28
|
+
record.ThreadID,
|
|
29
|
+
record.Direction,
|
|
30
|
+
normalizeOptionalString(record.SenderDID),
|
|
31
|
+
normalizeOptionalString(record.ReceiverDID),
|
|
32
|
+
normalizeOptionalString(record.GroupID),
|
|
33
|
+
normalizeOptionalString(record.GroupDID),
|
|
34
|
+
defaultString(record.ContentType, "text"),
|
|
35
|
+
record.Content,
|
|
36
|
+
normalizeOptionalString(record.Title),
|
|
37
|
+
normalizeOptionalInt64(record.ServerSeq),
|
|
38
|
+
normalizeOptionalString(record.SentAt),
|
|
39
|
+
now,
|
|
40
|
+
boolToInt(record.IsE2EE),
|
|
41
|
+
boolToInt(record.IsRead),
|
|
42
|
+
normalizeOptionalString(record.SenderName),
|
|
43
|
+
normalizeMetadata(record.Metadata),
|
|
44
|
+
normalizeCredentialName(record.CredentialName),
|
|
45
|
+
)
|
|
46
|
+
return err
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func StoreMessagesBatch(ctx context.Context, db *sql.DB, batch []MessageRecord) error {
|
|
50
|
+
if len(batch) == 0 {
|
|
51
|
+
return nil
|
|
52
|
+
}
|
|
53
|
+
tx, err := db.BeginTx(ctx, nil)
|
|
54
|
+
if err != nil {
|
|
55
|
+
return err
|
|
56
|
+
}
|
|
57
|
+
stmt, err := tx.PrepareContext(ctx, `
|
|
58
|
+
INSERT OR IGNORE INTO messages
|
|
59
|
+
(msg_id, owner_did, thread_id, direction, sender_did, receiver_did, group_id, group_did,
|
|
60
|
+
content_type, content, title, server_seq, sent_at, stored_at, is_e2ee, is_read,
|
|
61
|
+
sender_name, metadata, credential_name)
|
|
62
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
63
|
+
if err != nil {
|
|
64
|
+
_ = tx.Rollback()
|
|
65
|
+
return err
|
|
66
|
+
}
|
|
67
|
+
defer stmt.Close()
|
|
68
|
+
now := nowUTC()
|
|
69
|
+
for _, record := range batch {
|
|
70
|
+
if _, err := stmt.ExecContext(ctx,
|
|
71
|
+
record.MsgID,
|
|
72
|
+
normalizeOwnerDID(record.OwnerDID),
|
|
73
|
+
record.ThreadID,
|
|
74
|
+
record.Direction,
|
|
75
|
+
normalizeOptionalString(record.SenderDID),
|
|
76
|
+
normalizeOptionalString(record.ReceiverDID),
|
|
77
|
+
normalizeOptionalString(record.GroupID),
|
|
78
|
+
normalizeOptionalString(record.GroupDID),
|
|
79
|
+
defaultString(record.ContentType, "text"),
|
|
80
|
+
record.Content,
|
|
81
|
+
normalizeOptionalString(record.Title),
|
|
82
|
+
normalizeOptionalInt64(record.ServerSeq),
|
|
83
|
+
normalizeOptionalString(record.SentAt),
|
|
84
|
+
now,
|
|
85
|
+
boolToInt(record.IsE2EE),
|
|
86
|
+
boolToInt(record.IsRead),
|
|
87
|
+
normalizeOptionalString(record.SenderName),
|
|
88
|
+
normalizeMetadata(record.Metadata),
|
|
89
|
+
normalizeCredentialName(record.CredentialName),
|
|
90
|
+
); err != nil {
|
|
91
|
+
_ = tx.Rollback()
|
|
92
|
+
return err
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return tx.Commit()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func QueueE2EEOutbox(ctx context.Context, db *sql.DB, record E2EEOutboxRecord) (string, error) {
|
|
99
|
+
outboxID := defaultString(record.OutboxID, generateID())
|
|
100
|
+
now := nowUTC()
|
|
101
|
+
_, err := db.ExecContext(ctx, `
|
|
102
|
+
INSERT INTO e2ee_outbox
|
|
103
|
+
(outbox_id, owner_did, peer_did, session_id, original_type, plaintext, local_status,
|
|
104
|
+
attempt_count, sent_msg_id, sent_server_seq, last_error_code, retry_hint, failed_msg_id,
|
|
105
|
+
failed_server_seq, metadata, last_attempt_at, created_at, updated_at, credential_name)
|
|
106
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
107
|
+
outboxID,
|
|
108
|
+
normalizeOwnerDID(record.OwnerDID),
|
|
109
|
+
record.PeerDID,
|
|
110
|
+
normalizeOptionalString(record.SessionID),
|
|
111
|
+
defaultString(record.OriginalType, "text"),
|
|
112
|
+
record.Plaintext,
|
|
113
|
+
defaultString(record.LocalStatus, "queued"),
|
|
114
|
+
record.AttemptCount,
|
|
115
|
+
normalizeOptionalString(record.SentMsgID),
|
|
116
|
+
normalizeOptionalInt64(record.SentServerSeq),
|
|
117
|
+
normalizeOptionalString(record.LastErrorCode),
|
|
118
|
+
normalizeOptionalString(record.RetryHint),
|
|
119
|
+
normalizeOptionalString(record.FailedMsgID),
|
|
120
|
+
normalizeOptionalInt64(record.FailedServerSeq),
|
|
121
|
+
normalizeMetadata(record.Metadata),
|
|
122
|
+
normalizeOptionalString(record.LastAttemptAt),
|
|
123
|
+
defaultString(record.CreatedAt, now),
|
|
124
|
+
defaultString(record.UpdatedAt, now),
|
|
125
|
+
normalizeCredentialName(record.CredentialName),
|
|
126
|
+
)
|
|
127
|
+
return outboxID, err
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func MarkE2EEOutboxSent(ctx context.Context, db *sql.DB, outboxID string, ownerDID string, sessionID string, sentMsgID string, sentServerSeq *int64, metadata string) error {
|
|
131
|
+
_, err := db.ExecContext(ctx, `
|
|
132
|
+
UPDATE e2ee_outbox
|
|
133
|
+
SET session_id = COALESCE(?, session_id),
|
|
134
|
+
local_status = 'sent',
|
|
135
|
+
attempt_count = attempt_count + 1,
|
|
136
|
+
sent_msg_id = COALESCE(?, sent_msg_id),
|
|
137
|
+
sent_server_seq = COALESCE(?, sent_server_seq),
|
|
138
|
+
metadata = COALESCE(?, metadata),
|
|
139
|
+
last_attempt_at = ?,
|
|
140
|
+
updated_at = ?,
|
|
141
|
+
last_error_code = NULL,
|
|
142
|
+
retry_hint = NULL,
|
|
143
|
+
failed_msg_id = NULL,
|
|
144
|
+
failed_server_seq = NULL
|
|
145
|
+
WHERE outbox_id = ? AND owner_did = ?`,
|
|
146
|
+
normalizeOptionalString(sessionID),
|
|
147
|
+
normalizeOptionalString(sentMsgID),
|
|
148
|
+
normalizeOptionalInt64(sentServerSeq),
|
|
149
|
+
normalizeMetadata(metadata),
|
|
150
|
+
nowUTC(),
|
|
151
|
+
nowUTC(),
|
|
152
|
+
outboxID,
|
|
153
|
+
normalizeOwnerDID(ownerDID),
|
|
154
|
+
)
|
|
155
|
+
return err
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func MarkE2EEOutboxFailed(ctx context.Context, db *sql.DB, outboxID string, ownerDID string, errorCode string, retryHint string, failedMsgID string, failedServerSeq *int64, metadata string) error {
|
|
159
|
+
_, err := db.ExecContext(ctx, `
|
|
160
|
+
UPDATE e2ee_outbox
|
|
161
|
+
SET local_status = 'failed',
|
|
162
|
+
last_error_code = ?,
|
|
163
|
+
retry_hint = COALESCE(?, retry_hint),
|
|
164
|
+
failed_msg_id = COALESCE(?, failed_msg_id),
|
|
165
|
+
failed_server_seq = COALESCE(?, failed_server_seq),
|
|
166
|
+
metadata = COALESCE(?, metadata),
|
|
167
|
+
updated_at = ?
|
|
168
|
+
WHERE outbox_id = ? AND owner_did = ?`,
|
|
169
|
+
errorCode,
|
|
170
|
+
normalizeOptionalString(retryHint),
|
|
171
|
+
normalizeOptionalString(failedMsgID),
|
|
172
|
+
normalizeOptionalInt64(failedServerSeq),
|
|
173
|
+
normalizeMetadata(metadata),
|
|
174
|
+
nowUTC(),
|
|
175
|
+
outboxID,
|
|
176
|
+
normalizeOwnerDID(ownerDID),
|
|
177
|
+
)
|
|
178
|
+
return err
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func UpdateE2EEOutboxStatus(ctx context.Context, db *sql.DB, outboxID string, ownerDID string, credentialName string, status string) error {
|
|
182
|
+
if strings.TrimSpace(ownerDID) != "" {
|
|
183
|
+
_, err := db.ExecContext(ctx, `UPDATE e2ee_outbox SET local_status = ?, updated_at = ? WHERE outbox_id = ? AND owner_did = ?`,
|
|
184
|
+
status, nowUTC(), outboxID, normalizeOwnerDID(ownerDID))
|
|
185
|
+
return err
|
|
186
|
+
}
|
|
187
|
+
_, err := db.ExecContext(ctx, `UPDATE e2ee_outbox SET local_status = ?, updated_at = ? WHERE outbox_id = ? AND credential_name = ?`,
|
|
188
|
+
status, nowUTC(), outboxID, normalizeCredentialName(credentialName))
|
|
189
|
+
return err
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func SetE2EEOutboxFailureByID(ctx context.Context, db *sql.DB, outboxID string, ownerDID string, credentialName string, errorCode string, retryHint string, metadata string) error {
|
|
193
|
+
if strings.TrimSpace(ownerDID) != "" {
|
|
194
|
+
_, err := db.ExecContext(ctx, `
|
|
195
|
+
UPDATE e2ee_outbox
|
|
196
|
+
SET local_status = 'failed',
|
|
197
|
+
last_error_code = ?,
|
|
198
|
+
retry_hint = COALESCE(?, retry_hint),
|
|
199
|
+
metadata = COALESCE(?, metadata),
|
|
200
|
+
updated_at = ?
|
|
201
|
+
WHERE outbox_id = ? AND owner_did = ?`,
|
|
202
|
+
errorCode, normalizeOptionalString(retryHint), normalizeMetadata(metadata), nowUTC(), outboxID, normalizeOwnerDID(ownerDID))
|
|
203
|
+
return err
|
|
204
|
+
}
|
|
205
|
+
_, err := db.ExecContext(ctx, `
|
|
206
|
+
UPDATE e2ee_outbox
|
|
207
|
+
SET local_status = 'failed',
|
|
208
|
+
last_error_code = ?,
|
|
209
|
+
retry_hint = COALESCE(?, retry_hint),
|
|
210
|
+
metadata = COALESCE(?, metadata),
|
|
211
|
+
updated_at = ?
|
|
212
|
+
WHERE outbox_id = ? AND credential_name = ?`,
|
|
213
|
+
errorCode, normalizeOptionalString(retryHint), normalizeMetadata(metadata), nowUTC(), outboxID, normalizeCredentialName(credentialName))
|
|
214
|
+
return err
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func GetE2EEOutbox(ctx context.Context, db *sql.DB, outboxID string, ownerDID string, credentialName string) (map[string]any, error) {
|
|
218
|
+
if strings.TrimSpace(ownerDID) != "" {
|
|
219
|
+
return queryOneMap(ctx, db, `SELECT * FROM e2ee_outbox WHERE outbox_id = ? AND owner_did = ?`, outboxID, normalizeOwnerDID(ownerDID))
|
|
220
|
+
}
|
|
221
|
+
return queryOneMap(ctx, db, `SELECT * FROM e2ee_outbox WHERE outbox_id = ? AND credential_name = ?`, outboxID, normalizeCredentialName(credentialName))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
func ListE2EEOutbox(ctx context.Context, db *sql.DB, ownerDID string, credentialName string, localStatus string) ([]map[string]any, error) {
|
|
225
|
+
switch {
|
|
226
|
+
case strings.TrimSpace(ownerDID) != "" && strings.TrimSpace(localStatus) != "":
|
|
227
|
+
return queryMaps(ctx, db, `SELECT * FROM e2ee_outbox WHERE owner_did = ? AND local_status = ? ORDER BY updated_at DESC`, normalizeOwnerDID(ownerDID), localStatus)
|
|
228
|
+
case strings.TrimSpace(ownerDID) != "":
|
|
229
|
+
return queryMaps(ctx, db, `SELECT * FROM e2ee_outbox WHERE owner_did = ? ORDER BY updated_at DESC`, normalizeOwnerDID(ownerDID))
|
|
230
|
+
case strings.TrimSpace(localStatus) != "":
|
|
231
|
+
return queryMaps(ctx, db, `SELECT * FROM e2ee_outbox WHERE credential_name = ? AND local_status = ? ORDER BY updated_at DESC`, normalizeCredentialName(credentialName), localStatus)
|
|
232
|
+
default:
|
|
233
|
+
return queryMaps(ctx, db, `SELECT * FROM e2ee_outbox WHERE credential_name = ? ORDER BY updated_at DESC`, normalizeCredentialName(credentialName))
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func GetMessageByID(ctx context.Context, db *sql.DB, msgID string, ownerDID string, credentialName string) (map[string]any, error) {
|
|
238
|
+
if strings.TrimSpace(ownerDID) != "" {
|
|
239
|
+
return queryOneMap(ctx, db, `SELECT * FROM messages WHERE msg_id = ? AND owner_did = ?`, msgID, normalizeOwnerDID(ownerDID))
|
|
240
|
+
}
|
|
241
|
+
return queryOneMap(ctx, db, `SELECT * FROM messages WHERE msg_id = ? AND credential_name = ?`, msgID, normalizeCredentialName(credentialName))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
func UpsertContact(ctx context.Context, db *sql.DB, record ContactRecord) error {
|
|
245
|
+
if strings.TrimSpace(record.DID) == "" {
|
|
246
|
+
return fmt.Errorf("contact did is required")
|
|
247
|
+
}
|
|
248
|
+
ownerDID := normalizeOwnerDID(record.OwnerDID)
|
|
249
|
+
existing, err := queryMaps(ctx, db, `SELECT did FROM contacts WHERE owner_did = ? AND did = ?`, ownerDID, record.DID)
|
|
250
|
+
if err != nil {
|
|
251
|
+
return err
|
|
252
|
+
}
|
|
253
|
+
now := nowUTC()
|
|
254
|
+
if len(existing) > 0 {
|
|
255
|
+
_, err = db.ExecContext(ctx, `
|
|
256
|
+
UPDATE contacts
|
|
257
|
+
SET name = COALESCE(?, name),
|
|
258
|
+
handle = COALESCE(?, handle),
|
|
259
|
+
nick_name = COALESCE(?, nick_name),
|
|
260
|
+
bio = COALESCE(?, bio),
|
|
261
|
+
profile_md = COALESCE(?, profile_md),
|
|
262
|
+
tags = COALESCE(?, tags),
|
|
263
|
+
relationship = COALESCE(?, relationship),
|
|
264
|
+
source_type = COALESCE(?, source_type),
|
|
265
|
+
source_name = COALESCE(?, source_name),
|
|
266
|
+
source_group_id = COALESCE(?, source_group_id),
|
|
267
|
+
connected_at = COALESCE(?, connected_at),
|
|
268
|
+
recommended_reason = COALESCE(?, recommended_reason),
|
|
269
|
+
followed = COALESCE(?, followed),
|
|
270
|
+
messaged = COALESCE(?, messaged),
|
|
271
|
+
note = COALESCE(?, note),
|
|
272
|
+
first_seen_at = COALESCE(?, first_seen_at),
|
|
273
|
+
last_seen_at = ?,
|
|
274
|
+
metadata = COALESCE(?, metadata)
|
|
275
|
+
WHERE owner_did = ? AND did = ?`,
|
|
276
|
+
normalizeOptionalString(record.Name),
|
|
277
|
+
normalizeOptionalString(record.Handle),
|
|
278
|
+
normalizeOptionalString(record.NickName),
|
|
279
|
+
normalizeOptionalString(record.Bio),
|
|
280
|
+
normalizeOptionalString(record.ProfileMD),
|
|
281
|
+
normalizeOptionalString(record.Tags),
|
|
282
|
+
normalizeOptionalString(record.Relationship),
|
|
283
|
+
normalizeOptionalString(record.SourceType),
|
|
284
|
+
normalizeOptionalString(record.SourceName),
|
|
285
|
+
normalizeOptionalString(record.SourceGroupID),
|
|
286
|
+
normalizeOptionalString(record.ConnectedAt),
|
|
287
|
+
normalizeOptionalString(record.RecommendedReason),
|
|
288
|
+
normalizeOptionalBool(record.Followed),
|
|
289
|
+
normalizeOptionalBool(record.Messaged),
|
|
290
|
+
normalizeOptionalString(record.Note),
|
|
291
|
+
normalizeOptionalString(record.FirstSeenAt),
|
|
292
|
+
now,
|
|
293
|
+
normalizeMetadata(record.Metadata),
|
|
294
|
+
ownerDID,
|
|
295
|
+
record.DID,
|
|
296
|
+
)
|
|
297
|
+
return err
|
|
298
|
+
}
|
|
299
|
+
_, err = db.ExecContext(ctx, `
|
|
300
|
+
INSERT INTO contacts
|
|
301
|
+
(owner_did, did, name, handle, nick_name, bio, profile_md, tags, relationship, source_type, source_name,
|
|
302
|
+
source_group_id, connected_at, recommended_reason, followed, messaged, note, first_seen_at, last_seen_at, metadata)
|
|
303
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
304
|
+
ownerDID, record.DID,
|
|
305
|
+
normalizeOptionalString(record.Name),
|
|
306
|
+
normalizeOptionalString(record.Handle),
|
|
307
|
+
normalizeOptionalString(record.NickName),
|
|
308
|
+
normalizeOptionalString(record.Bio),
|
|
309
|
+
normalizeOptionalString(record.ProfileMD),
|
|
310
|
+
normalizeOptionalString(record.Tags),
|
|
311
|
+
normalizeOptionalString(record.Relationship),
|
|
312
|
+
normalizeOptionalString(record.SourceType),
|
|
313
|
+
normalizeOptionalString(record.SourceName),
|
|
314
|
+
normalizeOptionalString(record.SourceGroupID),
|
|
315
|
+
normalizeOptionalString(record.ConnectedAt),
|
|
316
|
+
normalizeOptionalString(record.RecommendedReason),
|
|
317
|
+
defaultBoolValue(record.Followed),
|
|
318
|
+
defaultBoolValue(record.Messaged),
|
|
319
|
+
normalizeOptionalString(record.Note),
|
|
320
|
+
defaultString(record.FirstSeenAt, now),
|
|
321
|
+
defaultString(record.LastSeenAt, now),
|
|
322
|
+
normalizeMetadata(record.Metadata),
|
|
323
|
+
)
|
|
324
|
+
return err
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
func AppendRelationshipEvent(ctx context.Context, db *sql.DB, record RelationshipEventRecord) (string, error) {
|
|
328
|
+
eventID := defaultString(record.EventID, generateID())
|
|
329
|
+
now := nowUTC()
|
|
330
|
+
_, err := db.ExecContext(ctx, `
|
|
331
|
+
INSERT INTO relationship_events
|
|
332
|
+
(event_id, owner_did, target_did, target_handle, event_type, source_type, source_name, source_group_id,
|
|
333
|
+
reason, score, status, created_at, updated_at, metadata, credential_name)
|
|
334
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
335
|
+
eventID,
|
|
336
|
+
normalizeOwnerDID(record.OwnerDID),
|
|
337
|
+
record.TargetDID,
|
|
338
|
+
normalizeOptionalString(record.TargetHandle),
|
|
339
|
+
record.EventType,
|
|
340
|
+
normalizeOptionalString(record.SourceType),
|
|
341
|
+
normalizeOptionalString(record.SourceName),
|
|
342
|
+
normalizeOptionalString(record.SourceGroupID),
|
|
343
|
+
normalizeOptionalString(record.Reason),
|
|
344
|
+
normalizeOptionalFloat64(record.Score),
|
|
345
|
+
defaultString(record.Status, "pending"),
|
|
346
|
+
defaultString(record.CreatedAt, now),
|
|
347
|
+
defaultString(record.UpdatedAt, now),
|
|
348
|
+
normalizeMetadata(record.Metadata),
|
|
349
|
+
normalizeCredentialName(record.CredentialName),
|
|
350
|
+
)
|
|
351
|
+
return eventID, err
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
func UpsertGroup(ctx context.Context, db *sql.DB, record GroupRecord) error {
|
|
355
|
+
ownerDID := normalizeOwnerDID(record.OwnerDID)
|
|
356
|
+
if ownerDID == "" || strings.TrimSpace(record.GroupID) == "" {
|
|
357
|
+
return fmt.Errorf("owner_did and group_id are required")
|
|
358
|
+
}
|
|
359
|
+
now := nowUTC()
|
|
360
|
+
_, err := db.ExecContext(ctx, `
|
|
361
|
+
INSERT OR REPLACE INTO groups
|
|
362
|
+
(owner_did, group_id, group_did, name, group_mode, slug, description, goal, rules, message_prompt,
|
|
363
|
+
doc_url, group_owner_did, group_owner_handle, my_role, membership_status, join_enabled, join_code,
|
|
364
|
+
join_code_expires_at, member_count, last_synced_seq, last_read_seq, last_message_at, remote_created_at,
|
|
365
|
+
remote_updated_at, stored_at, metadata, credential_name)
|
|
366
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
367
|
+
ownerDID,
|
|
368
|
+
record.GroupID,
|
|
369
|
+
normalizeOptionalString(record.GroupDID),
|
|
370
|
+
normalizeOptionalString(record.Name),
|
|
371
|
+
defaultString(record.GroupMode, "general"),
|
|
372
|
+
normalizeOptionalString(record.Slug),
|
|
373
|
+
normalizeOptionalString(record.Description),
|
|
374
|
+
normalizeOptionalString(record.Goal),
|
|
375
|
+
normalizeOptionalString(record.Rules),
|
|
376
|
+
normalizeOptionalString(record.MessagePrompt),
|
|
377
|
+
normalizeOptionalString(record.DocURL),
|
|
378
|
+
normalizeOptionalString(record.GroupOwnerDID),
|
|
379
|
+
normalizeOptionalString(record.GroupOwnerHandle),
|
|
380
|
+
normalizeOptionalString(record.MyRole),
|
|
381
|
+
defaultString(record.MembershipStatus, "active"),
|
|
382
|
+
normalizeOptionalBool(record.JoinEnabled),
|
|
383
|
+
normalizeOptionalString(record.JoinCode),
|
|
384
|
+
normalizeOptionalString(record.JoinCodeExpiresAt),
|
|
385
|
+
normalizeOptionalInt64(record.MemberCount),
|
|
386
|
+
normalizeOptionalInt64(record.LastSyncedSeq),
|
|
387
|
+
normalizeOptionalInt64(record.LastReadSeq),
|
|
388
|
+
normalizeOptionalString(record.LastMessageAt),
|
|
389
|
+
normalizeOptionalString(record.RemoteCreatedAt),
|
|
390
|
+
normalizeOptionalString(record.RemoteUpdatedAt),
|
|
391
|
+
now,
|
|
392
|
+
normalizeMetadata(record.Metadata),
|
|
393
|
+
normalizeCredentialName(record.CredentialName),
|
|
394
|
+
)
|
|
395
|
+
return err
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
func ReplaceGroupMembers(ctx context.Context, db *sql.DB, ownerDID string, groupID string, members []GroupMemberRecord, credentialName string) error {
|
|
399
|
+
tx, err := db.BeginTx(ctx, nil)
|
|
400
|
+
if err != nil {
|
|
401
|
+
return err
|
|
402
|
+
}
|
|
403
|
+
if _, err := tx.ExecContext(ctx, `DELETE FROM group_members WHERE owner_did = ? AND group_id = ?`, normalizeOwnerDID(ownerDID), groupID); err != nil {
|
|
404
|
+
_ = tx.Rollback()
|
|
405
|
+
return err
|
|
406
|
+
}
|
|
407
|
+
stmt, err := tx.PrepareContext(ctx, `
|
|
408
|
+
INSERT INTO group_members
|
|
409
|
+
(owner_did, group_id, user_id, member_did, member_handle, profile_url, role, status,
|
|
410
|
+
joined_at, sent_message_count, last_synced_at, metadata, credential_name)
|
|
411
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
412
|
+
if err != nil {
|
|
413
|
+
_ = tx.Rollback()
|
|
414
|
+
return err
|
|
415
|
+
}
|
|
416
|
+
defer stmt.Close()
|
|
417
|
+
now := nowUTC()
|
|
418
|
+
zero := int64(0)
|
|
419
|
+
for _, member := range members {
|
|
420
|
+
if strings.TrimSpace(member.UserID) == "" {
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
if _, err := stmt.ExecContext(ctx,
|
|
424
|
+
normalizeOwnerDID(ownerDID),
|
|
425
|
+
groupID,
|
|
426
|
+
member.UserID,
|
|
427
|
+
normalizeOptionalString(member.MemberDID),
|
|
428
|
+
normalizeOptionalString(member.MemberHandle),
|
|
429
|
+
normalizeOptionalString(member.ProfileURL),
|
|
430
|
+
normalizeOptionalString(member.Role),
|
|
431
|
+
defaultString(member.Status, "active"),
|
|
432
|
+
normalizeOptionalString(member.JoinedAt),
|
|
433
|
+
normalizeOptionalInt64(defaultInt64Ptr(member.SentMessageCount, &zero)),
|
|
434
|
+
now,
|
|
435
|
+
normalizeMetadata(member.Metadata),
|
|
436
|
+
normalizeCredentialName(defaultString(member.CredentialName, credentialName)),
|
|
437
|
+
); err != nil {
|
|
438
|
+
_ = tx.Rollback()
|
|
439
|
+
return err
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return tx.Commit()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func UpsertGroupMember(ctx context.Context, db *sql.DB, record GroupMemberRecord) error {
|
|
446
|
+
zero := int64(0)
|
|
447
|
+
_, err := db.ExecContext(ctx, `
|
|
448
|
+
INSERT OR REPLACE INTO group_members
|
|
449
|
+
(owner_did, group_id, user_id, member_did, member_handle, profile_url, role, status,
|
|
450
|
+
joined_at, sent_message_count, last_synced_at, metadata, credential_name)
|
|
451
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
452
|
+
normalizeOwnerDID(record.OwnerDID),
|
|
453
|
+
record.GroupID,
|
|
454
|
+
record.UserID,
|
|
455
|
+
normalizeOptionalString(record.MemberDID),
|
|
456
|
+
normalizeOptionalString(record.MemberHandle),
|
|
457
|
+
normalizeOptionalString(record.ProfileURL),
|
|
458
|
+
normalizeOptionalString(record.Role),
|
|
459
|
+
defaultString(record.Status, "active"),
|
|
460
|
+
normalizeOptionalString(record.JoinedAt),
|
|
461
|
+
normalizeOptionalInt64(defaultInt64Ptr(record.SentMessageCount, &zero)),
|
|
462
|
+
nowUTC(),
|
|
463
|
+
normalizeMetadata(record.Metadata),
|
|
464
|
+
normalizeCredentialName(record.CredentialName),
|
|
465
|
+
)
|
|
466
|
+
return err
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
func SyncGroupMemberFromSystemEvent(ctx context.Context, db *sql.DB, ownerDID string, groupID string, systemEvent map[string]any, credentialName string) (bool, error) {
|
|
470
|
+
subject, ok := systemEvent["subject"].(map[string]any)
|
|
471
|
+
if !ok {
|
|
472
|
+
return false, nil
|
|
473
|
+
}
|
|
474
|
+
userID, _ := subject["id"].(string)
|
|
475
|
+
if userID == "" {
|
|
476
|
+
return false, nil
|
|
477
|
+
}
|
|
478
|
+
kind, _ := systemEvent["kind"].(string)
|
|
479
|
+
statusByKind := map[string]string{"member_joined": "active", "member_left": "left", "member_kicked": "kicked"}
|
|
480
|
+
status, ok := statusByKind[kind]
|
|
481
|
+
if !ok {
|
|
482
|
+
return false, nil
|
|
483
|
+
}
|
|
484
|
+
if err := UpsertGroupMember(ctx, db, GroupMemberRecord{
|
|
485
|
+
OwnerDID: ownerDID,
|
|
486
|
+
GroupID: groupID,
|
|
487
|
+
UserID: userID,
|
|
488
|
+
MemberDID: stringFromAny(subject["did"]),
|
|
489
|
+
MemberHandle: stringFromAny(subject["handle"]),
|
|
490
|
+
ProfileURL: stringFromAny(subject["profile_url"]),
|
|
491
|
+
Role: "member",
|
|
492
|
+
Status: status,
|
|
493
|
+
Metadata: metadataFromAny(map[string]any{"system_event": systemEvent}),
|
|
494
|
+
CredentialName: credentialName,
|
|
495
|
+
}); err != nil {
|
|
496
|
+
return false, err
|
|
497
|
+
}
|
|
498
|
+
return true, nil
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
func DeleteGroupMembers(ctx context.Context, db *sql.DB, ownerDID string, groupID string, targetDID string, targetUserID string) (int64, error) {
|
|
502
|
+
var (
|
|
503
|
+
result sql.Result
|
|
504
|
+
err error
|
|
505
|
+
)
|
|
506
|
+
switch {
|
|
507
|
+
case strings.TrimSpace(targetDID) != "":
|
|
508
|
+
result, err = db.ExecContext(ctx, `DELETE FROM group_members WHERE owner_did = ? AND group_id = ? AND member_did = ?`, normalizeOwnerDID(ownerDID), groupID, targetDID)
|
|
509
|
+
case strings.TrimSpace(targetUserID) != "":
|
|
510
|
+
result, err = db.ExecContext(ctx, `DELETE FROM group_members WHERE owner_did = ? AND group_id = ? AND user_id = ?`, normalizeOwnerDID(ownerDID), groupID, targetUserID)
|
|
511
|
+
default:
|
|
512
|
+
result, err = db.ExecContext(ctx, `DELETE FROM group_members WHERE owner_did = ? AND group_id = ?`, normalizeOwnerDID(ownerDID), groupID)
|
|
513
|
+
}
|
|
514
|
+
if err != nil {
|
|
515
|
+
return 0, err
|
|
516
|
+
}
|
|
517
|
+
return result.RowsAffected()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
func RebindOwnerDID(ctx context.Context, db *sql.DB, oldOwnerDID string, newOwnerDID string) (map[string]int64, error) {
|
|
521
|
+
oldOwnerDID = normalizeOwnerDID(oldOwnerDID)
|
|
522
|
+
newOwnerDID = normalizeOwnerDID(newOwnerDID)
|
|
523
|
+
result := map[string]int64{
|
|
524
|
+
"messages": 0,
|
|
525
|
+
"contacts": 0,
|
|
526
|
+
"relationship_events": 0,
|
|
527
|
+
"groups": 0,
|
|
528
|
+
"group_members": 0,
|
|
529
|
+
}
|
|
530
|
+
if oldOwnerDID == "" || newOwnerDID == "" || oldOwnerDID == newOwnerDID {
|
|
531
|
+
return result, nil
|
|
532
|
+
}
|
|
533
|
+
tx, err := db.BeginTx(ctx, nil)
|
|
534
|
+
if err != nil {
|
|
535
|
+
return nil, err
|
|
536
|
+
}
|
|
537
|
+
for _, table := range []string{"messages", "contacts", "relationship_events", "groups", "group_members"} {
|
|
538
|
+
var count int64
|
|
539
|
+
if err := tx.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE owner_did = ?", table), oldOwnerDID).Scan(&count); err != nil {
|
|
540
|
+
_ = tx.Rollback()
|
|
541
|
+
return nil, err
|
|
542
|
+
}
|
|
543
|
+
result[table] = count
|
|
544
|
+
if _, err := tx.ExecContext(ctx, fmt.Sprintf("UPDATE OR IGNORE %s SET owner_did = ? WHERE owner_did = ?", table), newOwnerDID, oldOwnerDID); err != nil {
|
|
545
|
+
_ = tx.Rollback()
|
|
546
|
+
return nil, err
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if err := tx.Commit(); err != nil {
|
|
550
|
+
return nil, err
|
|
551
|
+
}
|
|
552
|
+
return result, nil
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
func ClearOwnerE2EEData(ctx context.Context, db *sql.DB, ownerDID string) (map[string]int64, error) {
|
|
556
|
+
ownerDID = normalizeOwnerDID(ownerDID)
|
|
557
|
+
result := map[string]int64{"e2ee_outbox": 0, "e2ee_sessions": 0}
|
|
558
|
+
if ownerDID == "" {
|
|
559
|
+
return result, nil
|
|
560
|
+
}
|
|
561
|
+
for _, table := range []string{"e2ee_outbox", "e2ee_sessions"} {
|
|
562
|
+
var count int64
|
|
563
|
+
if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE owner_did = ?", table), ownerDID).Scan(&count); err != nil {
|
|
564
|
+
return nil, err
|
|
565
|
+
}
|
|
566
|
+
result[table] = count
|
|
567
|
+
if _, err := db.ExecContext(ctx, fmt.Sprintf("DELETE FROM %s WHERE owner_did = ?", table), ownerDID); err != nil {
|
|
568
|
+
return nil, err
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return result, nil
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
func ExecuteSQL(ctx context.Context, db *sql.DB, statement string, params ...any) ([]map[string]any, error) {
|
|
575
|
+
sqlText := strings.TrimSpace(strings.TrimSuffix(statement, ";"))
|
|
576
|
+
if sqlText == "" {
|
|
577
|
+
return nil, fmt.Errorf("%w: empty statement", ErrUnsafeSQL)
|
|
578
|
+
}
|
|
579
|
+
if strings.Contains(sqlText, ";") {
|
|
580
|
+
return nil, fmt.Errorf("%w: multiple statements are not allowed", ErrUnsafeSQL)
|
|
581
|
+
}
|
|
582
|
+
upper := strings.ToUpper(sqlText)
|
|
583
|
+
for _, pattern := range forbiddenPatterns {
|
|
584
|
+
if pattern.MatchString(upper) {
|
|
585
|
+
return nil, fmt.Errorf("%w: forbidden SQL operation", ErrUnsafeSQL)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if matched, _ := regexp.MatchString(`(?i)^\s*DELETE\b`, sqlText); matched {
|
|
589
|
+
if !regexp.MustCompile(`(?i)\bWHERE\b`).MatchString(sqlText) {
|
|
590
|
+
return nil, fmt.Errorf("%w: DELETE without WHERE clause is not allowed", ErrUnsafeSQL)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if strings.HasPrefix(upper, "SELECT") {
|
|
594
|
+
return queryMaps(ctx, db, sqlText, params...)
|
|
595
|
+
}
|
|
596
|
+
result, err := db.ExecContext(ctx, sqlText, params...)
|
|
597
|
+
if err != nil {
|
|
598
|
+
return nil, err
|
|
599
|
+
}
|
|
600
|
+
rowsAffected, _ := result.RowsAffected()
|
|
601
|
+
return []map[string]any{{"rows_affected": rowsAffected}}, nil
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
func boolToInt(value bool) int {
|
|
605
|
+
if value {
|
|
606
|
+
return 1
|
|
607
|
+
}
|
|
608
|
+
return 0
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
func defaultBoolValue(value *bool) int {
|
|
612
|
+
if value == nil {
|
|
613
|
+
return 0
|
|
614
|
+
}
|
|
615
|
+
return boolToInt(*value)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
func defaultString(value string, fallback string) string {
|
|
619
|
+
if strings.TrimSpace(value) == "" {
|
|
620
|
+
return fallback
|
|
621
|
+
}
|
|
622
|
+
return value
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
func stringFromAny(value any) string {
|
|
626
|
+
text, _ := value.(string)
|
|
627
|
+
return text
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
func generateID() string {
|
|
631
|
+
return fmt.Sprintf("local-%d", time.Now().UnixNano())
|
|
632
|
+
}
|