// Package who is the public HTTP client for OscarKilo's //who
// service. It is used by `okg` and any other tool that talks
// to oscarkilo.com over the network.
//
// The struct here is named HTTPClient (not just Client) so
// applications can wrap it in their own Client abstractions
// without name collision.
package who
import "time"
import "oscarkilo.com/okg/internal/rest"
// HTTPClient talks to a //who server over HTTPS. Embeds the
// shared rest helpers; who-specific methods live below.
type HTTPClient struct {
*rest.Client
}
func NewHTTPClient(host, apiKey string) *HTTPClient {
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
}
// ---- Common types ----
// AppDataEntry is one user-scoped app-data record served by
// //who (per-user key/value, namespaced by app).
type AppDataEntry struct {
App string `json:"app"`
Key string `json:"key"`
Value string `json:"value"`
Timestamp time.Time `json:"timestamp"`
}
// ---- Caller identity ----
// Profile is the caller's //who profile. Mirrors the
// JSON-serialized subset of //who.Profile that GET
// /login/profile/get returns. ApiKeys, Logins, and timestamps
// are not exposed here yet — add when okg has a use.
type Profile struct {
Owid string `json:"owid"`
Username string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
PortraitUrl string `json:"portrait_url"`
OwnerOwid string `json:"owner_owid"`
Groups []string `json:"groups"`
AppData []AppDataEntry `json:"app_data,omitempty"`
}
// GetProfile returns the caller's own profile, identified by
// the Bearer key.
func (c *HTTPClient) GetProfile() (*Profile, error) {
var p Profile
if err := c.GetJSON("/login/profile/get", &p); err != nil {
return nil, err
}
return &p, nil
}
// ---- Groups ----
// Group describes a //who group (a named entity with members).
// Matches the row shape in /groups/list's response.
type Group struct {
Owid string `json:"owid"`
Username string `json:"username"`
Name string `json:"name"`
OwnerOwid string `json:"owner_owid"`
OwnerUsername string `json:"owner_username"`
}
type listGroupsResponse struct {
Groups []Group `json:"groups"`
}
// ListGroups returns the groups visible to the caller.
func (c *HTTPClient) ListGroups() ([]Group, error) {
var res listGroupsResponse
if err := c.GetJSON("/groups/list", &res); err != nil {
return nil, err
}
return res.Groups, nil
}
// CreateGroupRequest is the body for POST /groups/add. Username
// is the group's //who username (e.g. "team"); Name is the
// human-facing display name; OwnerUsername is the user who'll
// own the new group.
type CreateGroupRequest struct {
Username string `json:"username"`
Name string `json:"name"`
OwnerUsername string `json:"owner_username"`
}
// CreateGroup creates a new //who group.
func (c *HTTPClient) CreateGroup(req CreateGroupRequest) error {
return c.PostJSON("/groups/add", req, nil)
}
// JoinGroupsRequest is the body for POST /groups/join. The
// server adds every (GroupUsernames[i], MemberUsernames[j])
// pair, requiring the caller to be an owner of each group.
type JoinGroupsRequest struct {
GroupUsernames []string `json:"group_usernames"`
MemberUsernames []string `json:"member_usernames"`
}
// JoinGroups adds members to groups (all pairwise combinations).
func (c *HTTPClient) JoinGroups(req JoinGroupsRequest) error {
return c.PostJSON("/groups/join", req, nil)
}
// GroupMembersResponse is the response from POST
// /groups/members. Up lists the groups that contain the queried
// entity (each level as a slice of owids). Down lists its
// members the same way. Usernames maps every owid mentioned to
// its //who username.
type GroupMembersResponse struct {
Up [][]string `json:"up"`
Down [][]string `json:"down"`
Usernames map[string]string `json:"usernames"`
}
// GroupMembers returns membership DAG navigation for the given
// entity owid. For a group, Down enumerates its members.
func (c *HTTPClient) GroupMembers(
ownerOwid string,
) (*GroupMembersResponse, error) {
var res GroupMembersResponse
err := c.PostJSON("/groups/members",
map[string]string{"owid": ownerOwid}, &res)
if err != nil {
return nil, err
}
return &res, nil
}
// LeaveGroupRequest is the body for POST /groups/leave. Both
// IDs are owids (obfuscated wids); to translate from usernames,
// call ListGroups + GroupMembers first.
type LeaveGroupRequest struct {
GroupOwid string `json:"group_owid"`
MemberOwid string `json:"member_owid"`
}
// LeaveGroup removes a member from a group. The caller must own
// the group.
func (c *HTTPClient) LeaveGroup(req LeaveGroupRequest) error {
return c.PostJSON("/groups/leave", req, nil)
}
// DeleteGroupRequest is the body for POST /groups/delete. Owid
// is the group's obfuscated wid.
type DeleteGroupRequest struct {
Owid string `json:"owid"`
}
// DeleteGroup destroys a group. The caller must own it (or be a
// //who admin).
func (c *HTTPClient) DeleteGroup(req DeleteGroupRequest) error {
return c.PostJSON("/groups/delete", req, nil)
}
// ---- Authz ----
// User is the public //who-user identifier. It carries only
// the fields safe to expose to external callers.
type User struct {
Owid string `json:"owid"`
Username string `json:"username"`
AppData []AppDataEntry `json:"appdata,omitempty"`
}
// AuthzEntry is one row of GET /authz/list: the URI, its owner
// and reader, and the caller's effective rights on it.
type AuthzEntry struct {
Uri string `json:"uri"`
Owner *User `json:"owner"`
Reader *User `json:"reader"`
IsOwner bool `json:"is_owner"`
IsReader bool `json:"is_reader"`
}
type listAuthzResponse struct {
Uris []AuthzEntry `json:"uris"`
}
// ListAuthz returns all authz URIs visible to the caller.
func (c *HTTPClient) ListAuthz() ([]AuthzEntry, error) {
var res listAuthzResponse
if err := c.GetJSON("/authz/list", &res); err != nil {
return nil, err
}
return res.Uris, nil
}
// AuthzSetRequest is the body for POST /authz/set. Both
// usernames must be non-empty and known to //who; the public
// endpoint does not honor the "anyone" sentinel.
type AuthzSetRequest struct {
Uri string `json:"uri"`
OwnerUsername string `json:"owner_username"`
ReaderUsername string `json:"reader_username"`
}
// SetAuthzMulti writes one or more authz entries atomically.
// Either every entry's write succeeds or none do (server-side
// Pebble batch). The caller must be an owner of every URI in
// the batch (or a //who admin).
func (c *HTTPClient) SetAuthzMulti(
reqs []AuthzSetRequest,
) error {
return c.PostJSON("/authz/set", reqs, nil)
}
// SetAuthz writes one authz entry. Syntactic sugar over
// SetAuthzMulti for the single-entry case.
func (c *HTTPClient) SetAuthz(req AuthzSetRequest) error {
return c.SetAuthzMulti([]AuthzSetRequest{req})
}
// AuthzDeleteRequest is the body for POST /authz/delete.
type AuthzDeleteRequest struct {
Uri string `json:"uri"`
}
// DeleteAuthz removes an authz entry. The caller must own the URI.
func (c *HTTPClient) DeleteAuthz(req AuthzDeleteRequest) error {
return c.PostJSON("/authz/delete", req, nil)
}