clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / tinyclient / read_contracts.go
read_contracts.go 5.6K
1package tinyclient23import (4 "context"5 "encoding/json"6 "net/url"78 "github.com/FelineStateMachine/tinyrelay/protocol/nostr"9)1011// RoomsReader supplies the typed reads used by room pages. actor is the12// authenticated public key, or an empty string for an anonymous request.13// cursor is empty for the newest page and otherwise is the value returned in14// NextCursor. limit requests at most that many rows. The daemon adapter uses15// 100 when the requested limit falls outside 1 through 100.16//17// ListRooms returns visible room summaries ordered by room ID and a cursor for18// the next page. ReadRoom returns the room summary, visible members, newest19// room messages, message edits and a cursor for older messages. ReadThread20// returns the room summary, one root event, its replies, edits and a cursor for21// older replies. Reads enforce tenant and room access and return errors for22// missing rooms, invalid cursors or an inaccessible room. ReadThread also23// returns an error when rootID is not a lower-case hexadecimal event ID or does24// not identify a message in roomID.25type RoomsReader interface {26 // ListRooms returns up to limit visible room summaries after cursor.27 ListRooms(ctx context.Context, actor, cursor string, limit int) (RoomList, error)28 // ReadRoom returns one visible room, its members, messages and edits after cursor.29 ReadRoom(ctx context.Context, actor, roomID, cursor string, limit int) (RoomPage, error)30 // ReadThread returns one room thread root, replies and edits after cursor.31 ReadThread(ctx context.Context, actor, roomID, rootID, cursor string, limit int) (RoomPage, error)32}3334type RoomList struct {35 // Rooms contains visible room summaries in ascending ID order.36 Rooms []RoomSummary `json:"rooms"`37 // NextCursor is the last room ID when another page is available.38 NextCursor string `json:"next_cursor,omitempty"`39}4041// RoomSummary is the room metadata shown in the rail and room header.42type RoomSummary struct {43 ID string `json:"id"`44 Name string `json:"name,omitempty"`45 Access string `json:"access,omitempty"`46 About string `json:"about,omitempty"`47 Picture string `json:"picture,omitempty"`48 CreatedBy string `json:"created_by,omitempty"`49 CreatedAt int64 `json:"created_at,omitempty"`50 EventID string `json:"event_id,omitempty"`51 DeletedAt int64 `json:"deleted_at,omitempty"`52 Members int `json:"members,omitempty"`53 LastMessageAt int64 `json:"last_message_at,omitempty"`54 Role string `json:"role,omitempty"`55}5657type RoomMember struct {58 PubKey string `json:"pubkey"`59 Role string `json:"role,omitempty"`60 Agent bool `json:"agent,omitempty"`61 Operator string `json:"operator,omitempty"`62 AddedAt int64 `json:"added_at,omitempty"`63}6465type RoomMessage = nostr.Event6667// RoomPage is the typed result for a room or thread read. A room read fills68// Members and Messages; a thread read fills Root and Replies. Edits contains69// the newest visible edit for the returned targets.70type RoomPage struct {71 Room RoomSummary `json:"room"`72 Members []RoomMember `json:"members,omitempty"`73 Messages []RoomMessage `json:"messages,omitempty"`74 // ReplyCounts and ReactionCounts carry folded interaction summaries for75 // the roots in Messages without expanding every reply into the timeline.76 ReplyCounts map[string]int `json:"reply_counts,omitempty"`77 ReactionCounts map[string]map[string]int `json:"reaction_counts,omitempty"`78 Replies []RoomMessage `json:"replies,omitempty"`79 Edits []RoomMessage `json:"edits,omitempty"`80 Root *RoomMessage `json:"root,omitempty"`81 NextCursor string `json:"next_cursor,omitempty"`82}8384// rowsReader converts method-and-JSON query results into the row slices used85// by page renderers.86type rowsReader struct{ backend Backend }8788func (r rowsReader) rows(ctx context.Context, method string, params []json.RawMessage, actor string) ([]any, error) {89 result, err := r.backend.Query(ctx, method, params, actor)90 if err != nil {91 return nil, err92 }93 return browseRows(result), nil94}9596func (a *App) readRows(ctx context.Context, method string, params []json.RawMessage, actor string) ([]any, error) {97 return rowsReader{backend: a.backend}.rows(ctx, method, params, actor)98}99100func (a *App) roomsReader() RoomsReader {101 reader, _ := a.backend.(RoomsReader)102 return reader103}104105func roomListValue(list RoomList) any {106 return map[string]any{"items": list.Rooms, "next_cursor": list.NextCursor}107}108109func roomPageValueFromContract(page RoomPage) any {110 return map[string]any{111 "room": page.Room,112 "members": page.Members,113 "messages": page.Messages,114 "reply_counts": page.ReplyCounts,115 "reaction_counts": page.ReactionCounts,116 "replies": page.Replies,117 "edits": page.Edits,118 "root": page.Root,119 "next_cursor": page.NextCursor,120 }121}122123func (a *App) typedRoomResult(ctx context.Context, route roomPath, query url.Values, actor string) (any, error, bool) {124 reader := a.roomsReader()125 if reader == nil || route.tab == "" {126 return nil, nil, false127 }128 var (129 value any130 err error131 )132 switch route.tab {133 case "rooms":134 list, readErr := reader.ListRooms(ctx, actor, query.Get("cursor"), 100)135 value, err = roomListValue(list), readErr136 case "room":137 page, readErr := reader.ReadRoom(ctx, actor, route.id, query.Get("cursor"), 50)138 value, err = roomPageValueFromContract(page), readErr139 case "thread":140 page, readErr := reader.ReadThread(ctx, actor, route.id, route.event, query.Get("cursor"), 50)141 value, err = roomPageValueFromContract(page), readErr142 }143 return value, err, true144}