@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,838 @@
|
|
|
1
|
+
package message
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"database/sql"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
"sort"
|
|
9
|
+
"strconv"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
"github.com/agentconnect/awiki-cli/internal/identity"
|
|
13
|
+
"github.com/agentconnect/awiki-cli/internal/store"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func (s *Service) CreateGroup(ctx context.Context, request GroupCreateRequest) (*CommandResult, error) {
|
|
17
|
+
if strings.TrimSpace(request.Name) == "" {
|
|
18
|
+
return nil, ErrGroupRequired
|
|
19
|
+
}
|
|
20
|
+
record, err := s.requireActiveIdentity(request.IdentityName)
|
|
21
|
+
if err != nil {
|
|
22
|
+
return nil, err
|
|
23
|
+
}
|
|
24
|
+
transport, warnings, err := s.transportFor(record)
|
|
25
|
+
if err != nil {
|
|
26
|
+
return nil, err
|
|
27
|
+
}
|
|
28
|
+
result, err := transport.CreateGroup(ctx, request)
|
|
29
|
+
if err != nil {
|
|
30
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
31
|
+
if httpErr != nil {
|
|
32
|
+
return nil, err
|
|
33
|
+
}
|
|
34
|
+
result, err = httpTransport.CreateGroup(ctx, request)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return nil, err
|
|
37
|
+
}
|
|
38
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
39
|
+
warnings = append(warnings, httpWarnings...)
|
|
40
|
+
}
|
|
41
|
+
groupDID := stringFromAny(result["group_did"])
|
|
42
|
+
warnings = append(warnings, s.syncGroupState(ctx, record, groupDID, true)...)
|
|
43
|
+
snapshot, _ := s.readCachedGroupSnapshot(ctx, record, groupDID)
|
|
44
|
+
members, _ := s.readCachedGroupMembers(ctx, record, groupDID, 100)
|
|
45
|
+
return &CommandResult{
|
|
46
|
+
Data: map[string]any{
|
|
47
|
+
"group": snapshot,
|
|
48
|
+
"members": members,
|
|
49
|
+
"delivery": result,
|
|
50
|
+
"source": sourceWithDefault(result, s.runtimeConfig().Mode),
|
|
51
|
+
},
|
|
52
|
+
Summary: fmt.Sprintf("Created group %s", groupDID),
|
|
53
|
+
Warnings: compactWarnings(warnings),
|
|
54
|
+
}, nil
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func (s *Service) GetGroup(ctx context.Context, request GroupGetRequest) (*CommandResult, error) {
|
|
58
|
+
if strings.TrimSpace(request.Group) == "" {
|
|
59
|
+
return nil, ErrGroupRequired
|
|
60
|
+
}
|
|
61
|
+
record, err := s.requireActiveIdentity(request.IdentityName)
|
|
62
|
+
if err != nil {
|
|
63
|
+
return nil, err
|
|
64
|
+
}
|
|
65
|
+
transport, warnings, err := s.transportFor(record)
|
|
66
|
+
if err != nil {
|
|
67
|
+
return nil, err
|
|
68
|
+
}
|
|
69
|
+
result, err := transport.GetGroup(ctx, request)
|
|
70
|
+
if err != nil {
|
|
71
|
+
cached, cacheErr := s.readCachedGroupSnapshot(ctx, record, request.Group)
|
|
72
|
+
if cacheErr == nil && len(cached) > 0 {
|
|
73
|
+
return &CommandResult{Data: map[string]any{"group": cached, "source": "local_ws_cache_fallback"}, Summary: "Loaded group snapshot from local cache", Warnings: []string{err.Error()}}, nil
|
|
74
|
+
}
|
|
75
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
76
|
+
if httpErr != nil {
|
|
77
|
+
return nil, err
|
|
78
|
+
}
|
|
79
|
+
result, err = httpTransport.GetGroup(ctx, request)
|
|
80
|
+
if err != nil {
|
|
81
|
+
return nil, err
|
|
82
|
+
}
|
|
83
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
84
|
+
warnings = append(warnings, httpWarnings...)
|
|
85
|
+
}
|
|
86
|
+
warnings = append(warnings, s.persistGroupSnapshot(ctx, record, result)...)
|
|
87
|
+
snapshot, _ := s.readCachedGroupSnapshot(ctx, record, request.Group)
|
|
88
|
+
if len(snapshot) == 0 {
|
|
89
|
+
snapshot = normalizeGroupSnapshot(result)
|
|
90
|
+
}
|
|
91
|
+
return &CommandResult{Data: map[string]any{"group": snapshot, "source": sourceWithDefault(result, s.runtimeConfig().Mode)}, Summary: "Loaded group snapshot", Warnings: compactWarnings(warnings)}, nil
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func (s *Service) JoinGroup(ctx context.Context, request GroupJoinRequest) (*CommandResult, error) {
|
|
95
|
+
if strings.TrimSpace(request.Group) == "" {
|
|
96
|
+
return nil, ErrGroupRequired
|
|
97
|
+
}
|
|
98
|
+
record, err := s.requireActiveIdentity(request.IdentityName)
|
|
99
|
+
if err != nil {
|
|
100
|
+
return nil, err
|
|
101
|
+
}
|
|
102
|
+
transport, warnings, err := s.transportFor(record)
|
|
103
|
+
if err != nil {
|
|
104
|
+
return nil, err
|
|
105
|
+
}
|
|
106
|
+
result, err := transport.JoinGroup(ctx, request)
|
|
107
|
+
if err != nil {
|
|
108
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
109
|
+
if httpErr != nil {
|
|
110
|
+
return nil, err
|
|
111
|
+
}
|
|
112
|
+
result, err = httpTransport.JoinGroup(ctx, request)
|
|
113
|
+
if err != nil {
|
|
114
|
+
return nil, err
|
|
115
|
+
}
|
|
116
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
117
|
+
warnings = append(warnings, httpWarnings...)
|
|
118
|
+
}
|
|
119
|
+
groupDID := stringFromAny(result["group_did"])
|
|
120
|
+
warnings = append(warnings, s.syncGroupState(ctx, record, groupDID, true)...)
|
|
121
|
+
snapshot, _ := s.readCachedGroupSnapshot(ctx, record, groupDID)
|
|
122
|
+
return &CommandResult{Data: map[string]any{"group": snapshot, "delivery": result, "source": sourceWithDefault(result, s.runtimeConfig().Mode)}, Summary: fmt.Sprintf("Joined group %s", groupDID), Warnings: compactWarnings(warnings)}, nil
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func (s *Service) AddGroupMember(ctx context.Context, request GroupMemberRequest) (*CommandResult, error) {
|
|
126
|
+
return s.mutateGroupMember(ctx, request, "add")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func (s *Service) RemoveGroupMember(ctx context.Context, request GroupMemberRequest) (*CommandResult, error) {
|
|
130
|
+
return s.mutateGroupMember(ctx, request, "remove")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func (s *Service) mutateGroupMember(ctx context.Context, request GroupMemberRequest, action string) (*CommandResult, error) {
|
|
134
|
+
if strings.TrimSpace(request.Group) == "" {
|
|
135
|
+
return nil, ErrGroupRequired
|
|
136
|
+
}
|
|
137
|
+
if strings.TrimSpace(request.Member) == "" {
|
|
138
|
+
return nil, ErrMemberRequired
|
|
139
|
+
}
|
|
140
|
+
record, err := s.requireActiveIdentity(request.IdentityName)
|
|
141
|
+
if err != nil {
|
|
142
|
+
return nil, err
|
|
143
|
+
}
|
|
144
|
+
memberDID, memberHandle, err := s.resolveTarget(ctx, request.Member)
|
|
145
|
+
if err != nil {
|
|
146
|
+
return nil, err
|
|
147
|
+
}
|
|
148
|
+
request.Member = memberDID
|
|
149
|
+
transport, warnings, err := s.transportFor(record)
|
|
150
|
+
if err != nil {
|
|
151
|
+
return nil, err
|
|
152
|
+
}
|
|
153
|
+
var result map[string]any
|
|
154
|
+
if action == "add" {
|
|
155
|
+
result, err = transport.AddGroupMember(ctx, request)
|
|
156
|
+
} else {
|
|
157
|
+
result, err = transport.RemoveGroupMember(ctx, request)
|
|
158
|
+
}
|
|
159
|
+
if err != nil {
|
|
160
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
161
|
+
if httpErr != nil {
|
|
162
|
+
return nil, err
|
|
163
|
+
}
|
|
164
|
+
if action == "add" {
|
|
165
|
+
result, err = httpTransport.AddGroupMember(ctx, request)
|
|
166
|
+
} else {
|
|
167
|
+
result, err = httpTransport.RemoveGroupMember(ctx, request)
|
|
168
|
+
}
|
|
169
|
+
if err != nil {
|
|
170
|
+
return nil, err
|
|
171
|
+
}
|
|
172
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
173
|
+
warnings = append(warnings, httpWarnings...)
|
|
174
|
+
}
|
|
175
|
+
warnings = append(warnings, s.syncGroupState(ctx, record, request.Group, true)...)
|
|
176
|
+
snapshot, _ := s.readCachedGroupSnapshot(ctx, record, request.Group)
|
|
177
|
+
members, _ := s.readCachedGroupMembers(ctx, record, request.Group, 100)
|
|
178
|
+
return &CommandResult{Data: map[string]any{"group": snapshot, "members": members, "delivery": result, "member": map[string]any{"did": memberDID, "handle": memberHandle}}, Summary: fmt.Sprintf("Updated group membership via %s", action), Warnings: compactWarnings(warnings)}, nil
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func (s *Service) LeaveGroup(ctx context.Context, request GroupLeaveRequest) (*CommandResult, error) {
|
|
182
|
+
if strings.TrimSpace(request.Group) == "" {
|
|
183
|
+
return nil, ErrGroupRequired
|
|
184
|
+
}
|
|
185
|
+
record, err := s.requireActiveIdentity(request.IdentityName)
|
|
186
|
+
if err != nil {
|
|
187
|
+
return nil, err
|
|
188
|
+
}
|
|
189
|
+
transport, warnings, err := s.transportFor(record)
|
|
190
|
+
if err != nil {
|
|
191
|
+
return nil, err
|
|
192
|
+
}
|
|
193
|
+
result, err := transport.LeaveGroup(ctx, request)
|
|
194
|
+
if err != nil {
|
|
195
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
196
|
+
if httpErr != nil {
|
|
197
|
+
return nil, err
|
|
198
|
+
}
|
|
199
|
+
result, err = httpTransport.LeaveGroup(ctx, request)
|
|
200
|
+
if err != nil {
|
|
201
|
+
return nil, err
|
|
202
|
+
}
|
|
203
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
204
|
+
warnings = append(warnings, httpWarnings...)
|
|
205
|
+
}
|
|
206
|
+
warnings = append(warnings, s.markCachedGroupLeft(ctx, record, request.Group)...)
|
|
207
|
+
return &CommandResult{Data: map[string]any{"delivery": result, "group": request.Group}, Summary: fmt.Sprintf("Left group %s", request.Group), Warnings: compactWarnings(warnings)}, nil
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
func (s *Service) UpdateGroup(ctx context.Context, request GroupUpdateRequest) (*CommandResult, error) {
|
|
211
|
+
if strings.TrimSpace(request.Group) == "" {
|
|
212
|
+
return nil, ErrGroupRequired
|
|
213
|
+
}
|
|
214
|
+
record, err := s.requireActiveIdentity(request.IdentityName)
|
|
215
|
+
if err != nil {
|
|
216
|
+
return nil, err
|
|
217
|
+
}
|
|
218
|
+
profilePatch := buildGroupProfilePatch(request.Name, request.Description, request.Discoverability, request.Slug, request.Goal, request.Rules, request.MessagePrompt, request.DocURL)
|
|
219
|
+
policyPatch := buildGroupPolicyPatch(request.AdmissionMode, request.AttachmentsAllowed, request.MaxMembers, request.MemberMaxMessages, request.MemberMaxTotalChars)
|
|
220
|
+
if len(profilePatch) == 0 && len(policyPatch) == 0 {
|
|
221
|
+
return nil, fmt.Errorf("group update requires at least one mutable field")
|
|
222
|
+
}
|
|
223
|
+
transport, warnings, err := s.transportFor(record)
|
|
224
|
+
if err != nil {
|
|
225
|
+
return nil, err
|
|
226
|
+
}
|
|
227
|
+
responses := make([]map[string]any, 0, 2)
|
|
228
|
+
if len(profilePatch) > 0 {
|
|
229
|
+
result, callErr := transport.UpdateGroupProfile(ctx, GroupGetRequest{Group: request.Group}, profilePatch)
|
|
230
|
+
if callErr != nil {
|
|
231
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
232
|
+
if httpErr != nil {
|
|
233
|
+
return nil, callErr
|
|
234
|
+
}
|
|
235
|
+
result, callErr = httpTransport.UpdateGroupProfile(ctx, GroupGetRequest{Group: request.Group}, profilePatch)
|
|
236
|
+
if callErr != nil {
|
|
237
|
+
return nil, callErr
|
|
238
|
+
}
|
|
239
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
240
|
+
warnings = append(warnings, httpWarnings...)
|
|
241
|
+
}
|
|
242
|
+
responses = append(responses, result)
|
|
243
|
+
}
|
|
244
|
+
if len(policyPatch) > 0 {
|
|
245
|
+
result, callErr := transport.UpdateGroupPolicy(ctx, GroupGetRequest{Group: request.Group}, policyPatch)
|
|
246
|
+
if callErr != nil {
|
|
247
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
248
|
+
if httpErr != nil {
|
|
249
|
+
return nil, callErr
|
|
250
|
+
}
|
|
251
|
+
result, callErr = httpTransport.UpdateGroupPolicy(ctx, GroupGetRequest{Group: request.Group}, policyPatch)
|
|
252
|
+
if callErr != nil {
|
|
253
|
+
return nil, callErr
|
|
254
|
+
}
|
|
255
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
256
|
+
warnings = append(warnings, httpWarnings...)
|
|
257
|
+
}
|
|
258
|
+
responses = append(responses, result)
|
|
259
|
+
}
|
|
260
|
+
warnings = append(warnings, s.syncGroupState(ctx, record, request.Group, false)...)
|
|
261
|
+
snapshot, _ := s.readCachedGroupSnapshot(ctx, record, request.Group)
|
|
262
|
+
return &CommandResult{Data: map[string]any{"group": snapshot, "delivery": responses}, Summary: fmt.Sprintf("Updated group %s", request.Group), Warnings: compactWarnings(warnings)}, nil
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func (s *Service) GroupMembers(ctx context.Context, request GroupMembersRequest) (*CommandResult, error) {
|
|
266
|
+
if strings.TrimSpace(request.Group) == "" {
|
|
267
|
+
return nil, ErrGroupRequired
|
|
268
|
+
}
|
|
269
|
+
record, err := s.requireActiveIdentity(request.IdentityName)
|
|
270
|
+
if err != nil {
|
|
271
|
+
return nil, err
|
|
272
|
+
}
|
|
273
|
+
transport, warnings, err := s.transportFor(record)
|
|
274
|
+
if err != nil {
|
|
275
|
+
return nil, err
|
|
276
|
+
}
|
|
277
|
+
result, err := transport.ListGroupMembers(ctx, request)
|
|
278
|
+
if err != nil {
|
|
279
|
+
cached, cacheErr := s.readCachedGroupMembers(ctx, record, request.Group, request.Limit)
|
|
280
|
+
if cacheErr == nil && len(cached) > 0 {
|
|
281
|
+
return &CommandResult{Data: map[string]any{"members": cached, "total": len(cached), "group": request.Group, "source": "local_ws_cache_fallback"}, Summary: "Loaded group members from local cache", Warnings: []string{err.Error()}}, nil
|
|
282
|
+
}
|
|
283
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
284
|
+
if httpErr != nil {
|
|
285
|
+
return nil, err
|
|
286
|
+
}
|
|
287
|
+
result, err = httpTransport.ListGroupMembers(ctx, request)
|
|
288
|
+
if err != nil {
|
|
289
|
+
return nil, err
|
|
290
|
+
}
|
|
291
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
292
|
+
warnings = append(warnings, httpWarnings...)
|
|
293
|
+
}
|
|
294
|
+
warnings = append(warnings, s.persistGroupMembers(ctx, record, request.Group, result)...)
|
|
295
|
+
members, _ := s.readCachedGroupMembers(ctx, record, request.Group, request.Limit)
|
|
296
|
+
if len(members) == 0 {
|
|
297
|
+
members = groupMembersFromResult(result["members"])
|
|
298
|
+
}
|
|
299
|
+
total := intValueFromAny(result["total"], len(members))
|
|
300
|
+
return &CommandResult{Data: map[string]any{"group": request.Group, "members": members, "total": total, "source": sourceWithDefault(result, s.runtimeConfig().Mode)}, Summary: fmt.Sprintf("Loaded %d group members", total), Warnings: compactWarnings(warnings)}, nil
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func (s *Service) GroupMessages(ctx context.Context, request GroupMessagesRequest) (*CommandResult, error) {
|
|
304
|
+
if strings.TrimSpace(request.Group) == "" {
|
|
305
|
+
return nil, ErrGroupRequired
|
|
306
|
+
}
|
|
307
|
+
record, err := s.requireActiveIdentity(request.IdentityName)
|
|
308
|
+
if err != nil {
|
|
309
|
+
return nil, err
|
|
310
|
+
}
|
|
311
|
+
transport, warnings, err := s.transportFor(record)
|
|
312
|
+
if err != nil {
|
|
313
|
+
return nil, err
|
|
314
|
+
}
|
|
315
|
+
result, err := transport.ListGroupMessages(ctx, request)
|
|
316
|
+
if err != nil {
|
|
317
|
+
cached, cacheErr := s.readCachedGroupMessages(ctx, record, request.Group, request.Limit, request.Cursor)
|
|
318
|
+
if cacheErr == nil && len(cached) > 0 {
|
|
319
|
+
return &CommandResult{Data: map[string]any{"group": request.Group, "messages": cached, "total": len(cached), "source": "local_ws_cache_fallback"}, Summary: "Loaded group messages from local cache", Warnings: []string{err.Error()}}, nil
|
|
320
|
+
}
|
|
321
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
322
|
+
if httpErr != nil {
|
|
323
|
+
return nil, err
|
|
324
|
+
}
|
|
325
|
+
result, err = httpTransport.ListGroupMessages(ctx, request)
|
|
326
|
+
if err != nil {
|
|
327
|
+
return nil, err
|
|
328
|
+
}
|
|
329
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
330
|
+
warnings = append(warnings, httpWarnings...)
|
|
331
|
+
}
|
|
332
|
+
warnings = append(warnings, s.persistGroupMessages(ctx, record, request.Group, result)...)
|
|
333
|
+
messages, _ := s.readCachedGroupMessages(ctx, record, request.Group, request.Limit, request.Cursor)
|
|
334
|
+
if len(messages) == 0 {
|
|
335
|
+
messages = messagesFromResult(result["messages"])
|
|
336
|
+
}
|
|
337
|
+
total := intValueFromAny(result["total"], len(messages))
|
|
338
|
+
return &CommandResult{Data: map[string]any{"group": request.Group, "messages": messages, "total": total, "has_more": boolFromAny(result["has_more"]), "next_since_seq": result["next_since_seq"], "source": sourceWithDefault(result, s.runtimeConfig().Mode)}, Summary: fmt.Sprintf("Loaded %d group messages", total), Warnings: compactWarnings(warnings)}, nil
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
func (s *Service) sendGroup(ctx context.Context, request SendRequest) (*CommandResult, error) {
|
|
342
|
+
if strings.TrimSpace(request.Group) == "" {
|
|
343
|
+
return nil, ErrGroupRequired
|
|
344
|
+
}
|
|
345
|
+
if strings.TrimSpace(request.Text) == "" {
|
|
346
|
+
return nil, ErrTextRequired
|
|
347
|
+
}
|
|
348
|
+
if request.SecureMode == "on" {
|
|
349
|
+
return nil, ErrSecureNotSupported
|
|
350
|
+
}
|
|
351
|
+
record, err := s.requireActiveIdentity(request.IdentityName)
|
|
352
|
+
if err != nil {
|
|
353
|
+
return nil, err
|
|
354
|
+
}
|
|
355
|
+
transport, warnings, err := s.transportFor(record)
|
|
356
|
+
if err != nil {
|
|
357
|
+
return nil, err
|
|
358
|
+
}
|
|
359
|
+
result, err := transport.SendGroup(ctx, request)
|
|
360
|
+
if err != nil {
|
|
361
|
+
httpTransport, httpWarnings, httpErr := s.httpTransport(record)
|
|
362
|
+
if httpErr != nil {
|
|
363
|
+
return nil, err
|
|
364
|
+
}
|
|
365
|
+
result, err = httpTransport.SendGroup(ctx, request)
|
|
366
|
+
if err != nil {
|
|
367
|
+
return nil, err
|
|
368
|
+
}
|
|
369
|
+
warnings = append(warnings, "WebSocket transport unavailable; used HTTP fallback.")
|
|
370
|
+
warnings = append(warnings, httpWarnings...)
|
|
371
|
+
}
|
|
372
|
+
return s.persistGroupSendResult(ctx, record, request, result, warnings)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func (s *Service) syncGroupState(ctx context.Context, record *identity.StoredIdentity, groupDID string, includeMembers bool) []string {
|
|
376
|
+
if strings.TrimSpace(groupDID) == "" {
|
|
377
|
+
return nil
|
|
378
|
+
}
|
|
379
|
+
httpTransport, _, err := s.httpTransport(record)
|
|
380
|
+
if err != nil {
|
|
381
|
+
return []string{fmt.Sprintf("Failed to prepare group sync transport: %v", err)}
|
|
382
|
+
}
|
|
383
|
+
warnings := make([]string, 0)
|
|
384
|
+
groupResult, err := httpTransport.GetGroup(ctx, GroupGetRequest{Group: groupDID})
|
|
385
|
+
if err != nil {
|
|
386
|
+
return []string{fmt.Sprintf("Failed to refresh group snapshot: %v", err)}
|
|
387
|
+
}
|
|
388
|
+
warnings = append(warnings, s.persistGroupSnapshot(ctx, record, groupResult)...)
|
|
389
|
+
if includeMembers {
|
|
390
|
+
memberResult, memberErr := httpTransport.ListGroupMembers(ctx, GroupMembersRequest{Group: groupDID, Limit: 100})
|
|
391
|
+
if memberErr != nil {
|
|
392
|
+
warnings = append(warnings, fmt.Sprintf("Failed to refresh group members: %v", memberErr))
|
|
393
|
+
} else {
|
|
394
|
+
warnings = append(warnings, s.persistGroupMembers(ctx, record, groupDID, memberResult)...)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return compactWarnings(warnings)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
func (s *Service) persistGroupSendResult(ctx context.Context, record *identity.StoredIdentity, request SendRequest, result *groupSendResult, warnings []string) (*CommandResult, error) {
|
|
401
|
+
db, err := store.Open(s.resolved.Paths)
|
|
402
|
+
if err != nil {
|
|
403
|
+
return nil, err
|
|
404
|
+
}
|
|
405
|
+
defer db.Close()
|
|
406
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
407
|
+
return nil, err
|
|
408
|
+
}
|
|
409
|
+
msgID := result.MessageID
|
|
410
|
+
if strings.TrimSpace(result.GroupDID) != "" && strings.TrimSpace(result.GroupEventSeq) != "" {
|
|
411
|
+
msgID = fmt.Sprintf("%s:%s", result.GroupDID, result.GroupEventSeq)
|
|
412
|
+
} else if msgID == "" {
|
|
413
|
+
msgID = "msg-" + generateOperationID()
|
|
414
|
+
}
|
|
415
|
+
groupKey := groupStorageKey(request.Group)
|
|
416
|
+
if err := store.StoreMessage(ctx, db, store.MessageRecord{
|
|
417
|
+
MsgID: msgID,
|
|
418
|
+
OwnerDID: record.DID,
|
|
419
|
+
ThreadID: store.MakeThreadID(record.DID, "", groupKey),
|
|
420
|
+
Direction: 1,
|
|
421
|
+
SenderDID: record.DID,
|
|
422
|
+
GroupID: groupKey,
|
|
423
|
+
GroupDID: request.Group,
|
|
424
|
+
ContentType: contentTypeForMessageType(request.MessageType),
|
|
425
|
+
Content: request.Text,
|
|
426
|
+
SentAt: result.AcceptedAt,
|
|
427
|
+
IsRead: true,
|
|
428
|
+
Metadata: metadataString(map[string]any{"group_event_seq": result.GroupEventSeq, "group_state_version": result.GroupStateVersion, "operation_id": result.OperationID}),
|
|
429
|
+
CredentialName: record.IdentityName,
|
|
430
|
+
}); err != nil {
|
|
431
|
+
warnings = append(warnings, fmt.Sprintf("Failed to persist local group message: %v", err))
|
|
432
|
+
}
|
|
433
|
+
warnings = append(warnings, s.touchCachedGroup(ctx, record, request.Group, result.AcceptedAt, result.GroupEventSeq, result.GroupStateVersion)...)
|
|
434
|
+
return &CommandResult{Data: map[string]any{"action": "send_message", "target": map[string]any{"kind": "group", "did": request.Group}, "message": map[string]any{"id": msgID, "type": request.MessageType, "secure": false, "sent_at": result.AcceptedAt}, "delivery": result}, Summary: fmt.Sprintf("Sent a group %s message", request.MessageType), Warnings: compactWarnings(warnings)}, nil
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
func (s *Service) persistGroupSnapshot(ctx context.Context, record *identity.StoredIdentity, raw map[string]any) []string {
|
|
438
|
+
snapshot := normalizeGroupSnapshot(raw)
|
|
439
|
+
if len(snapshot) == 0 {
|
|
440
|
+
return nil
|
|
441
|
+
}
|
|
442
|
+
db, err := store.Open(s.resolved.Paths)
|
|
443
|
+
if err != nil {
|
|
444
|
+
return []string{fmt.Sprintf("Failed to open local store for group snapshot: %v", err)}
|
|
445
|
+
}
|
|
446
|
+
defer db.Close()
|
|
447
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
448
|
+
return []string{fmt.Sprintf("Failed to ensure local schema for group snapshot: %v", err)}
|
|
449
|
+
}
|
|
450
|
+
groupDID := stringFromAny(snapshot["group_did"])
|
|
451
|
+
if groupDID == "" {
|
|
452
|
+
return nil
|
|
453
|
+
}
|
|
454
|
+
memberCount := int64PtrFromAny(snapshot["member_count"])
|
|
455
|
+
lastSyncedSeq := int64PtrFromAny(snapshot["group_event_seq"])
|
|
456
|
+
recordToStore := store.GroupRecord{
|
|
457
|
+
OwnerDID: record.DID,
|
|
458
|
+
GroupID: groupStorageKey(groupDID),
|
|
459
|
+
GroupDID: groupDID,
|
|
460
|
+
Name: stringFromAny(snapshot["name"]),
|
|
461
|
+
Slug: stringFromAny(snapshot["slug"]),
|
|
462
|
+
Description: stringFromAny(snapshot["description"]),
|
|
463
|
+
Goal: stringFromAny(snapshot["goal"]),
|
|
464
|
+
Rules: stringFromAny(snapshot["rules"]),
|
|
465
|
+
MessagePrompt: stringFromAny(snapshot["message_prompt"]),
|
|
466
|
+
DocURL: stringFromAny(snapshot["doc_url"]),
|
|
467
|
+
GroupOwnerDID: stringFromAny(snapshot["owner_did"]),
|
|
468
|
+
MyRole: stringFromAny(snapshot["member_role"]),
|
|
469
|
+
MembershipStatus: stringFromAny(snapshot["member_status"]),
|
|
470
|
+
JoinEnabled: boolPtrFromAny(snapshot["join_enabled"]),
|
|
471
|
+
MemberCount: memberCount,
|
|
472
|
+
LastSyncedSeq: lastSyncedSeq,
|
|
473
|
+
RemoteCreatedAt: stringFromAny(snapshot["created_at"]),
|
|
474
|
+
RemoteUpdatedAt: stringFromAny(snapshot["updated_at"]),
|
|
475
|
+
Metadata: metadataString(snapshot),
|
|
476
|
+
CredentialName: record.IdentityName,
|
|
477
|
+
}
|
|
478
|
+
if err := store.UpsertGroup(ctx, db, recordToStore); err != nil {
|
|
479
|
+
return []string{fmt.Sprintf("Failed to persist group snapshot: %v", err)}
|
|
480
|
+
}
|
|
481
|
+
return nil
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
func (s *Service) persistGroupMembers(ctx context.Context, record *identity.StoredIdentity, groupDID string, raw map[string]any) []string {
|
|
485
|
+
members := groupMembersFromResult(raw["members"])
|
|
486
|
+
if len(members) == 0 {
|
|
487
|
+
return nil
|
|
488
|
+
}
|
|
489
|
+
db, err := store.Open(s.resolved.Paths)
|
|
490
|
+
if err != nil {
|
|
491
|
+
return []string{fmt.Sprintf("Failed to open local store for group members: %v", err)}
|
|
492
|
+
}
|
|
493
|
+
defer db.Close()
|
|
494
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
495
|
+
return []string{fmt.Sprintf("Failed to ensure local schema for group members: %v", err)}
|
|
496
|
+
}
|
|
497
|
+
records := make([]store.GroupMemberRecord, 0, len(members))
|
|
498
|
+
for _, member := range members {
|
|
499
|
+
memberDID := stringFromAny(member["agent_did"])
|
|
500
|
+
if memberDID == "" {
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
records = append(records, store.GroupMemberRecord{
|
|
504
|
+
OwnerDID: record.DID,
|
|
505
|
+
GroupID: groupStorageKey(groupDID),
|
|
506
|
+
UserID: memberDID,
|
|
507
|
+
MemberDID: memberDID,
|
|
508
|
+
Role: stringFromAny(member["role"]),
|
|
509
|
+
Status: stringFromAny(member["status"]),
|
|
510
|
+
JoinedAt: stringFromAny(member["joined_at"]),
|
|
511
|
+
Metadata: metadataString(member),
|
|
512
|
+
CredentialName: record.IdentityName,
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
if err := store.ReplaceGroupMembers(ctx, db, record.DID, groupStorageKey(groupDID), records, record.IdentityName); err != nil {
|
|
516
|
+
return []string{fmt.Sprintf("Failed to persist group members: %v", err)}
|
|
517
|
+
}
|
|
518
|
+
return nil
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
func (s *Service) persistGroupMessages(ctx context.Context, record *identity.StoredIdentity, groupDID string, raw map[string]any) []string {
|
|
522
|
+
messages := messagesFromResult(raw["messages"])
|
|
523
|
+
if len(messages) == 0 {
|
|
524
|
+
return nil
|
|
525
|
+
}
|
|
526
|
+
db, err := store.Open(s.resolved.Paths)
|
|
527
|
+
if err != nil {
|
|
528
|
+
return []string{fmt.Sprintf("Failed to open local store for group messages: %v", err)}
|
|
529
|
+
}
|
|
530
|
+
defer db.Close()
|
|
531
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
532
|
+
return []string{fmt.Sprintf("Failed to ensure local schema for group messages: %v", err)}
|
|
533
|
+
}
|
|
534
|
+
batch := make([]store.MessageRecord, 0, len(messages))
|
|
535
|
+
for _, item := range messages {
|
|
536
|
+
msgID := stringFromAny(item["id"])
|
|
537
|
+
if msgID == "" {
|
|
538
|
+
msgID = stringFromAny(item["message_id"])
|
|
539
|
+
}
|
|
540
|
+
if msgID == "" {
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
direction := 0
|
|
544
|
+
if stringFromAny(item["sender_did"]) == record.DID {
|
|
545
|
+
direction = 1
|
|
546
|
+
}
|
|
547
|
+
contentType := stringFromAny(item["content_type"])
|
|
548
|
+
contentValue := item["content"]
|
|
549
|
+
content := stringFromAny(contentValue)
|
|
550
|
+
if content == "" {
|
|
551
|
+
content = metadataString(contentValue)
|
|
552
|
+
}
|
|
553
|
+
batch = append(batch, store.MessageRecord{
|
|
554
|
+
MsgID: msgID,
|
|
555
|
+
OwnerDID: record.DID,
|
|
556
|
+
ThreadID: store.MakeThreadID(record.DID, "", groupStorageKey(groupDID)),
|
|
557
|
+
Direction: direction,
|
|
558
|
+
SenderDID: stringFromAny(item["sender_did"]),
|
|
559
|
+
GroupID: groupStorageKey(groupDID),
|
|
560
|
+
GroupDID: groupDID,
|
|
561
|
+
ContentType: defaultString(contentType, inferGroupMessageContentType(item)),
|
|
562
|
+
Content: content,
|
|
563
|
+
ServerSeq: int64PtrFromAny(item["server_seq"]),
|
|
564
|
+
SentAt: defaultString(stringFromAny(item["sent_at"]), stringFromAny(item["created_at"])),
|
|
565
|
+
IsRead: boolFromAny(item["is_read"]),
|
|
566
|
+
Metadata: metadataString(item),
|
|
567
|
+
CredentialName: record.IdentityName,
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
if err := store.StoreMessagesBatch(ctx, db, batch); err != nil {
|
|
571
|
+
return []string{fmt.Sprintf("Failed to persist group messages: %v", err)}
|
|
572
|
+
}
|
|
573
|
+
if len(messages) > 0 {
|
|
574
|
+
latest := messages[0]
|
|
575
|
+
_ = store.UpsertGroup(ctx, db, store.GroupRecord{
|
|
576
|
+
OwnerDID: record.DID,
|
|
577
|
+
GroupID: groupStorageKey(groupDID),
|
|
578
|
+
GroupDID: groupDID,
|
|
579
|
+
LastSyncedSeq: int64PtrFromAny(raw["next_since_seq"]),
|
|
580
|
+
LastMessageAt: defaultString(stringFromAny(latest["sent_at"]), stringFromAny(latest["created_at"])),
|
|
581
|
+
CredentialName: record.IdentityName,
|
|
582
|
+
Metadata: metadataString(map[string]any{"source": "group.list_messages"}),
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
return nil
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
func (s *Service) touchCachedGroup(ctx context.Context, record *identity.StoredIdentity, groupDID string, sentAt string, groupEventSeq string, groupStateVersion string) []string {
|
|
589
|
+
db, err := store.Open(s.resolved.Paths)
|
|
590
|
+
if err != nil {
|
|
591
|
+
return []string{fmt.Sprintf("Failed to open local store for group cache update: %v", err)}
|
|
592
|
+
}
|
|
593
|
+
defer db.Close()
|
|
594
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
595
|
+
return []string{fmt.Sprintf("Failed to ensure local schema for group cache update: %v", err)}
|
|
596
|
+
}
|
|
597
|
+
if err := store.UpsertGroup(ctx, db, store.GroupRecord{OwnerDID: record.DID, GroupID: groupStorageKey(groupDID), GroupDID: groupDID, LastMessageAt: sentAt, LastSyncedSeq: parseInt64Ptr(groupEventSeq), CredentialName: record.IdentityName, Metadata: metadataString(map[string]any{"group_state_version": groupStateVersion})}); err != nil {
|
|
598
|
+
return []string{fmt.Sprintf("Failed to update group cache: %v", err)}
|
|
599
|
+
}
|
|
600
|
+
return nil
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
func (s *Service) markCachedGroupLeft(ctx context.Context, record *identity.StoredIdentity, groupDID string) []string {
|
|
604
|
+
db, err := store.Open(s.resolved.Paths)
|
|
605
|
+
if err != nil {
|
|
606
|
+
return []string{fmt.Sprintf("Failed to open local store for leave projection: %v", err)}
|
|
607
|
+
}
|
|
608
|
+
defer db.Close()
|
|
609
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
610
|
+
return []string{fmt.Sprintf("Failed to ensure local schema for leave projection: %v", err)}
|
|
611
|
+
}
|
|
612
|
+
if err := store.UpsertGroup(ctx, db, store.GroupRecord{OwnerDID: record.DID, GroupID: groupStorageKey(groupDID), GroupDID: groupDID, MembershipStatus: "left", CredentialName: record.IdentityName}); err != nil {
|
|
613
|
+
return []string{fmt.Sprintf("Failed to update local group leave status: %v", err)}
|
|
614
|
+
}
|
|
615
|
+
return nil
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
func (s *Service) readCachedGroupSnapshot(ctx context.Context, record *identity.StoredIdentity, groupDID string) (map[string]any, error) {
|
|
619
|
+
db, err := store.Open(s.resolved.Paths)
|
|
620
|
+
if err != nil {
|
|
621
|
+
return nil, err
|
|
622
|
+
}
|
|
623
|
+
defer db.Close()
|
|
624
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
625
|
+
return nil, err
|
|
626
|
+
}
|
|
627
|
+
row, err := store.GetGroupSnapshot(ctx, db, record.DID, groupStorageKey(groupDID))
|
|
628
|
+
if err != nil {
|
|
629
|
+
if err == sql.ErrNoRows {
|
|
630
|
+
return nil, nil
|
|
631
|
+
}
|
|
632
|
+
return nil, err
|
|
633
|
+
}
|
|
634
|
+
return row, nil
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
func (s *Service) readCachedGroupMembers(ctx context.Context, record *identity.StoredIdentity, groupDID string, limit int) ([]map[string]any, error) {
|
|
638
|
+
db, err := store.Open(s.resolved.Paths)
|
|
639
|
+
if err != nil {
|
|
640
|
+
return nil, err
|
|
641
|
+
}
|
|
642
|
+
defer db.Close()
|
|
643
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
644
|
+
return nil, err
|
|
645
|
+
}
|
|
646
|
+
return store.ListCachedGroupMembers(ctx, db, record.DID, groupStorageKey(groupDID), limit)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
func (s *Service) readCachedGroupMessages(ctx context.Context, record *identity.StoredIdentity, groupDID string, limit int, cursor string) ([]map[string]any, error) {
|
|
650
|
+
db, err := store.Open(s.resolved.Paths)
|
|
651
|
+
if err != nil {
|
|
652
|
+
return nil, err
|
|
653
|
+
}
|
|
654
|
+
defer db.Close()
|
|
655
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
656
|
+
return nil, err
|
|
657
|
+
}
|
|
658
|
+
return store.ListGroupMessages(ctx, db, record.DID, groupStorageKey(groupDID), limit, parseInt64Ptr(cursor))
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
func normalizeGroupSnapshot(raw map[string]any) map[string]any {
|
|
662
|
+
if raw == nil {
|
|
663
|
+
return nil
|
|
664
|
+
}
|
|
665
|
+
if snapshot, ok := raw["group_snapshot"].(map[string]any); ok {
|
|
666
|
+
return snapshot
|
|
667
|
+
}
|
|
668
|
+
if groupDID := stringFromAny(raw["group_did"]); groupDID != "" {
|
|
669
|
+
name := ""
|
|
670
|
+
if profile, ok := raw["group_profile"].(map[string]any); ok {
|
|
671
|
+
name = stringFromAny(profile["display_name"])
|
|
672
|
+
return map[string]any{
|
|
673
|
+
"group_did": groupDID,
|
|
674
|
+
"group_state_version": raw["group_state_version"],
|
|
675
|
+
"name": name,
|
|
676
|
+
"description": profile["description"],
|
|
677
|
+
"discoverability": profile["discoverability"],
|
|
678
|
+
"member_count": raw["member_count"],
|
|
679
|
+
"group_profile": profile,
|
|
680
|
+
"group_policy": raw["group_policy"],
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return nil
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
func groupMembersFromResult(value any) []map[string]any {
|
|
688
|
+
return messagesFromResult(value)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
func inferGroupMessageContentType(message map[string]any) string {
|
|
692
|
+
systemEvent, _ := message["system_event"].(map[string]any)
|
|
693
|
+
subjectMethod := stringFromAny(systemEvent["subject_method"])
|
|
694
|
+
switch subjectMethod {
|
|
695
|
+
case "group.join", "group.add":
|
|
696
|
+
return "group_system_member_joined"
|
|
697
|
+
case "group.leave":
|
|
698
|
+
return "group_system_member_left"
|
|
699
|
+
case "group.remove":
|
|
700
|
+
return "group_system_member_kicked"
|
|
701
|
+
default:
|
|
702
|
+
if systemEvent != nil {
|
|
703
|
+
return "application/json"
|
|
704
|
+
}
|
|
705
|
+
return "text/plain"
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
func groupStorageKey(groupDID string) string {
|
|
710
|
+
return strings.TrimSpace(groupDID)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
func parseInt64Ptr(value string) *int64 {
|
|
714
|
+
value = strings.TrimSpace(value)
|
|
715
|
+
if value == "" {
|
|
716
|
+
return nil
|
|
717
|
+
}
|
|
718
|
+
parsed, err := strconv.ParseInt(value, 10, 64)
|
|
719
|
+
if err != nil {
|
|
720
|
+
return nil
|
|
721
|
+
}
|
|
722
|
+
return &parsed
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
func boolPtrFromAny(value any) *bool {
|
|
726
|
+
switch typed := value.(type) {
|
|
727
|
+
case bool:
|
|
728
|
+
value := typed
|
|
729
|
+
return &value
|
|
730
|
+
case int:
|
|
731
|
+
value := typed != 0
|
|
732
|
+
return &value
|
|
733
|
+
case int64:
|
|
734
|
+
value := typed != 0
|
|
735
|
+
return &value
|
|
736
|
+
case float64:
|
|
737
|
+
value := typed != 0
|
|
738
|
+
return &value
|
|
739
|
+
case string:
|
|
740
|
+
if typed == "" {
|
|
741
|
+
return nil
|
|
742
|
+
}
|
|
743
|
+
value := strings.EqualFold(typed, "true") || typed == "1"
|
|
744
|
+
return &value
|
|
745
|
+
default:
|
|
746
|
+
return nil
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
func compactWarnings(warnings []string) []string {
|
|
751
|
+
if len(warnings) == 0 {
|
|
752
|
+
return nil
|
|
753
|
+
}
|
|
754
|
+
seen := make(map[string]struct{}, len(warnings))
|
|
755
|
+
result := make([]string, 0, len(warnings))
|
|
756
|
+
for _, warning := range warnings {
|
|
757
|
+
warning = strings.TrimSpace(warning)
|
|
758
|
+
if warning == "" {
|
|
759
|
+
continue
|
|
760
|
+
}
|
|
761
|
+
if _, ok := seen[warning]; ok {
|
|
762
|
+
continue
|
|
763
|
+
}
|
|
764
|
+
seen[warning] = struct{}{}
|
|
765
|
+
result = append(result, warning)
|
|
766
|
+
}
|
|
767
|
+
if len(result) == 0 {
|
|
768
|
+
return nil
|
|
769
|
+
}
|
|
770
|
+
return result
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
func encodeAnyString(value any) string {
|
|
774
|
+
if value == nil {
|
|
775
|
+
return ""
|
|
776
|
+
}
|
|
777
|
+
if text, ok := value.(string); ok {
|
|
778
|
+
return text
|
|
779
|
+
}
|
|
780
|
+
raw, err := json.Marshal(value)
|
|
781
|
+
if err != nil {
|
|
782
|
+
return ""
|
|
783
|
+
}
|
|
784
|
+
return string(raw)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
func readGroupInboxFromCache(ctx context.Context, resolvedPathOpener func() (*sql.DB, error), ownerDID string, groupDID string, limit int, unreadOnly bool) ([]map[string]any, error) {
|
|
788
|
+
db, err := resolvedPathOpener()
|
|
789
|
+
if err != nil {
|
|
790
|
+
return nil, err
|
|
791
|
+
}
|
|
792
|
+
defer db.Close()
|
|
793
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
794
|
+
return nil, err
|
|
795
|
+
}
|
|
796
|
+
return store.ListGroupInboxMessages(ctx, db, ownerDID, limit, groupStorageKey(groupDID), unreadOnly)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
func (s *Service) readGroupInboxFromCache(ctx context.Context, record *identity.StoredIdentity, groupDID string, limit int, unreadOnly bool) ([]map[string]any, error) {
|
|
800
|
+
return readGroupInboxFromCache(ctx, func() (*sql.DB, error) { return store.Open(s.resolved.Paths) }, record.DID, groupDID, limit, unreadOnly)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
func readAllLocalGroupInbox(ctx context.Context, resolvedPathOpener func() (*sql.DB, error), ownerDID string, limit int, unreadOnly bool) ([]map[string]any, error) {
|
|
804
|
+
db, err := resolvedPathOpener()
|
|
805
|
+
if err != nil {
|
|
806
|
+
return nil, err
|
|
807
|
+
}
|
|
808
|
+
defer db.Close()
|
|
809
|
+
if err := store.EnsureSchema(ctx, db); err != nil {
|
|
810
|
+
return nil, err
|
|
811
|
+
}
|
|
812
|
+
return store.ListGroupInboxMessages(ctx, db, ownerDID, limit, "", unreadOnly)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
func (s *Service) readAllLocalGroupInbox(ctx context.Context, record *identity.StoredIdentity, limit int, unreadOnly bool) ([]map[string]any, error) {
|
|
816
|
+
return readAllLocalGroupInbox(ctx, func() (*sql.DB, error) { return store.Open(s.resolved.Paths) }, record.DID, limit, unreadOnly)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
func mergeInboxMessages(limit int, left []map[string]any, right []map[string]any) []map[string]any {
|
|
820
|
+
all := make([]map[string]any, 0, len(left)+len(right))
|
|
821
|
+
all = append(all, left...)
|
|
822
|
+
all = append(all, right...)
|
|
823
|
+
if len(all) <= 1 {
|
|
824
|
+
if limit > 0 && len(all) > limit {
|
|
825
|
+
return all[:limit]
|
|
826
|
+
}
|
|
827
|
+
return all
|
|
828
|
+
}
|
|
829
|
+
sort.SliceStable(all, func(i, j int) bool {
|
|
830
|
+
leftTS := defaultString(stringFromAny(all[i]["sent_at"]), stringFromAny(all[i]["stored_at"]))
|
|
831
|
+
rightTS := defaultString(stringFromAny(all[j]["sent_at"]), stringFromAny(all[j]["stored_at"]))
|
|
832
|
+
return leftTS > rightTS
|
|
833
|
+
})
|
|
834
|
+
if limit > 0 && len(all) > limit {
|
|
835
|
+
return all[:limit]
|
|
836
|
+
}
|
|
837
|
+
return all
|
|
838
|
+
}
|