mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 18:19:19 +00:00
Squash merge PR #115 after resolving changelog and SQLite migration-map conflicts with current dev. Renumbered channel-context PostgreSQL migration to 000075 and bumped PG required schema to 75 plus SQLite schema to 44 so it follows the run timeline migration. Local checks passed: go test ./..., go build ./..., go build -tags sqliteonly ./..., go vet ./..., and pnpm -C ui/web build. PR CI run 26705617311 passed release-versioning, go, and web.
216 lines
6.3 KiB
Go
216 lines
6.3 KiB
Go
package http
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
|
)
|
|
|
|
type channelContextDTO struct {
|
|
ChannelInstanceID string `json:"channel_instance_id"`
|
|
ChannelType string `json:"channel_type"`
|
|
ScopeType string `json:"scope_type"`
|
|
ScopeKey string `json:"scope_key"`
|
|
DisplayName string `json:"display_name"`
|
|
Source string `json:"source"`
|
|
PeerKind string `json:"peer_kind,omitempty"`
|
|
ContactType string `json:"contact_type,omitempty"`
|
|
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
|
MemberCount int `json:"member_count,omitempty"`
|
|
LiveMembers bool `json:"live_members_supported"`
|
|
}
|
|
|
|
type channelContextMemberDTO struct {
|
|
UserID string `json:"user_id"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
Username string `json:"username,omitempty"`
|
|
PlatformID string `json:"platform_id"`
|
|
Source string `json:"source"`
|
|
Status string `json:"membership_status"`
|
|
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleListContexts(w http.ResponseWriter, r *http.Request) {
|
|
inst, ok := h.resolveInstance(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if h.contactStore == nil {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"contexts": []channelContextDTO{baseChannelContextDTO(*inst)},
|
|
})
|
|
return
|
|
}
|
|
|
|
opts := store.ContactListOpts{
|
|
ChannelType: inst.ChannelType,
|
|
ChannelInstance: inst.Name,
|
|
ContactType: "group",
|
|
Limit: 200,
|
|
}
|
|
contacts, err := h.contactStore.ListContacts(r.Context(), opts)
|
|
if err != nil {
|
|
slog.Error("channel_instances.contexts", "error", err)
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "channel contexts"))
|
|
return
|
|
}
|
|
|
|
contexts := []channelContextDTO{baseChannelContextDTO(*inst)}
|
|
for _, c := range contacts {
|
|
ctxDTO := contextDTOFromContact(inst.ID, c)
|
|
contexts = append(contexts, ctxDTO)
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"contexts": contexts})
|
|
}
|
|
|
|
func (h *ChannelInstancesHandler) handleListContextMembers(w http.ResponseWriter, r *http.Request) {
|
|
inst, ok := h.resolveInstance(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
scopeType := strings.TrimSpace(r.PathValue("scopeType"))
|
|
scopeKey := strings.TrimSpace(r.PathValue("scopeKey"))
|
|
if scopeType == "" || scopeKey == "" {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeError(w, http.StatusBadRequest, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "scopeType and scopeKey"))
|
|
return
|
|
}
|
|
|
|
var liveErr string
|
|
members := make([]channelContextMemberDTO, 0)
|
|
if h.channelMgr != nil && scopeType == store.ChannelScopeTypeGroup {
|
|
if live, err := h.channelMgr.ListGroupMembers(r.Context(), inst.Name, scopeKey); err == nil {
|
|
for _, m := range live {
|
|
members = append(members, channelContextMemberDTO{
|
|
UserID: m.MemberID,
|
|
DisplayName: m.Name,
|
|
PlatformID: m.MemberID,
|
|
Source: "live_provider",
|
|
Status: "visible",
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"members": members,
|
|
"live_members_supported": true,
|
|
"source": "live_provider",
|
|
})
|
|
return
|
|
} else {
|
|
liveErr = err.Error()
|
|
}
|
|
}
|
|
|
|
if h.contactStore != nil && scopeType != store.ChannelScopeTypeGroup && scopeType != "topic" {
|
|
contacts, err := h.contactStore.ListContacts(r.Context(), store.ContactListOpts{
|
|
ChannelType: inst.ChannelType,
|
|
ChannelInstance: inst.Name,
|
|
ContactType: "user",
|
|
PeerKind: peerKindForScope(scopeType),
|
|
Limit: 200,
|
|
})
|
|
if err != nil {
|
|
slog.Error("channel_instances.context_members", "error", err)
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeError(w, http.StatusInternalServerError, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "channel members"))
|
|
return
|
|
}
|
|
for _, c := range contacts {
|
|
members = append(members, memberDTOFromContact(c))
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"members": members,
|
|
"live_members_supported": false,
|
|
"source": "stored_contacts",
|
|
"unsupported_reason": liveErr,
|
|
})
|
|
}
|
|
|
|
func baseChannelContextDTO(inst store.ChannelInstanceData) channelContextDTO {
|
|
name := inst.DisplayName
|
|
if name == "" {
|
|
name = inst.Name
|
|
}
|
|
return channelContextDTO{
|
|
ChannelInstanceID: inst.ID.String(),
|
|
ChannelType: inst.ChannelType,
|
|
ScopeType: store.ChannelScopeTypeChannel,
|
|
ScopeKey: inst.Name,
|
|
DisplayName: name,
|
|
Source: "channel_instance",
|
|
PeerKind: "direct",
|
|
ContactType: "channel",
|
|
LiveMembers: false,
|
|
}
|
|
}
|
|
|
|
func contextDTOFromContact(instanceID uuid.UUID, c store.ChannelContact) channelContextDTO {
|
|
scopeType := store.ChannelScopeTypeGroup
|
|
if c.ContactType == "topic" {
|
|
scopeType = "topic"
|
|
}
|
|
displayName := c.SenderID
|
|
if c.DisplayName != nil && *c.DisplayName != "" {
|
|
displayName = *c.DisplayName
|
|
}
|
|
peerKind := ""
|
|
if c.PeerKind != nil {
|
|
peerKind = *c.PeerKind
|
|
}
|
|
lastSeen := c.LastSeenAt
|
|
return channelContextDTO{
|
|
ChannelInstanceID: instanceID.String(),
|
|
ChannelType: c.ChannelType,
|
|
ScopeType: scopeType,
|
|
ScopeKey: c.SenderID,
|
|
DisplayName: displayName,
|
|
Source: "stored_contact",
|
|
PeerKind: peerKind,
|
|
ContactType: c.ContactType,
|
|
LastSeenAt: &lastSeen,
|
|
}
|
|
}
|
|
|
|
func memberDTOFromContact(c store.ChannelContact) channelContextMemberDTO {
|
|
displayName := ""
|
|
if c.DisplayName != nil {
|
|
displayName = *c.DisplayName
|
|
}
|
|
username := ""
|
|
if c.Username != nil {
|
|
username = *c.Username
|
|
}
|
|
userID := c.SenderID
|
|
if c.UserID != nil && *c.UserID != "" {
|
|
userID = *c.UserID
|
|
}
|
|
lastSeen := c.LastSeenAt
|
|
return channelContextMemberDTO{
|
|
UserID: userID,
|
|
DisplayName: displayName,
|
|
Username: username,
|
|
PlatformID: c.SenderID,
|
|
Source: "stored_contact",
|
|
Status: "visible",
|
|
LastSeenAt: &lastSeen,
|
|
}
|
|
}
|
|
|
|
func peerKindForScope(scopeType string) string {
|
|
if scopeType == store.ChannelScopeTypeGroup || scopeType == "topic" {
|
|
return "group"
|
|
}
|
|
return "direct"
|
|
}
|