code.oscarkilo.com/okg/group.go

.gitignore
README.md
auth.go
authz.go
chat.go
chat/
client.go
config.go
embed.go
exemplary.go
go.mod
go.sum
group.go
internal/
klee/
klex.go
main.go
okg_test.go
one.go
pr.go
repo.go
who/
package main

import "encoding/json"
import "flag"
import "fmt"
import "os"
import "text/tabwriter"

import "oscarkilo.com/okg/who"

func runGroup(cfg *Config, args []string) error {
  if len(args) == 0 {
    return fmt.Errorf(
      "usage: okg group SUBCOMMAND ... " +
        "(try `okg --help`)")
  }
  switch args[0] {
  case "list":
    return runGroupList(cfg, args[1:])
  case "create":
    return runGroupCreate(cfg, args[1:])
  case "add-member":
    return runGroupAddMember(cfg, args[1:])
  case "remove-member":
    return runGroupRemoveMember(cfg, args[1:])
  case "members":
    return runGroupMembers(cfg, args[1:])
  case "delete":
    return runGroupDelete(cfg, args[1:])
  default:
    return fmt.Errorf(
      "unknown group subcommand: %s", args[0])
  }
}

func runGroupList(cfg *Config, args []string) error {
  fs := flag.NewFlagSet("group list", flag.ContinueOnError)
  asJSON := fs.Bool("json", false, "output raw JSON")
  if err := fs.Parse(args); err != nil {
    return err
  }

  c, err := newWhoClient(cfg)
  if err != nil {
    return err
  }

  groups, err := c.ListGroups()
  if err != nil {
    return err
  }

  if *asJSON {
    buf, err := json.MarshalIndent(groups, "", "  ")
    if err != nil {
      return err
    }
    fmt.Println(string(buf))
    return nil
  }

  tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
  fmt.Fprintln(tw, "USERNAME\tFULL NAME\tOWNER")
  for _, g := range groups {
    fmt.Fprintf(tw, "%s\t%s\t%s\n",
      g.Username, g.Name, g.OwnerUsername)
  }
  return tw.Flush()
}

func runGroupCreate(cfg *Config, args []string) error {
  fs := flag.NewFlagSet(
    "group create", flag.ContinueOnError)
  fullName := fs.String("full-name", "",
    "display name (default: NAME)")
  owner := fs.String("owner", "",
    "owner username (default: caller)")
  if err := fs.Parse(args); err != nil {
    return err
  }
  positional := fs.Args()
  if len(positional) != 1 {
    return fmt.Errorf("usage: okg group create NAME")
  }
  name := positional[0]

  c, err := newWhoClient(cfg)
  if err != nil {
    return err
  }

  if *owner == "" {
    me, err := c.GetProfile()
    if err != nil {
      return fmt.Errorf("resolve caller: %v", err)
    }
    *owner = me.Username
  }

  displayName := *fullName
  if displayName == "" {
    displayName = name
  }

  if err := c.CreateGroup(who.CreateGroupRequest{
    Username:      name,
    Name:          displayName,
    OwnerUsername: *owner,
  }); err != nil {
    return err
  }
  fmt.Printf(
    "Created group %s (owner: %s)\n", name, *owner)
  return nil
}

func runGroupAddMember(cfg *Config, args []string) error {
  fs := flag.NewFlagSet(
    "group add-member", flag.ContinueOnError)
  if err := fs.Parse(args); err != nil {
    return err
  }
  positional := fs.Args()
  if len(positional) < 2 {
    return fmt.Errorf(
      "usage: okg group add-member GROUP USER [USER ...]")
  }
  group := positional[0]
  members := positional[1:]

  c, err := newWhoClient(cfg)
  if err != nil {
    return err
  }
  if err := c.JoinGroups(who.JoinGroupsRequest{
    GroupUsernames:  []string{group},
    MemberUsernames: members,
  }); err != nil {
    return err
  }
  fmt.Printf(
    "Added %d member(s) to %s\n", len(members), group)
  return nil
}

func runGroupRemoveMember(cfg *Config, args []string) error {
  fs := flag.NewFlagSet(
    "group remove-member", flag.ContinueOnError)
  if err := fs.Parse(args); err != nil {
    return err
  }
  positional := fs.Args()
  if len(positional) < 2 {
    return fmt.Errorf(
      "usage: okg group remove-member " +
        "GROUP USER [USER ...]")
  }
  groupName := positional[0]
  members := positional[1:]

  c, err := newWhoClient(cfg)
  if err != nil {
    return err
  }

  // /groups/leave wants owids, not usernames. Resolve the
  // group's owid via ListGroups, then the members' owids via
  // GroupMembers. Two round-trips on top of the actual leave
  // calls; shared across all members in this invocation.
  groupOwid, err := resolveGroupOwid(c, groupName)
  if err != nil {
    return err
  }
  ms, err := c.GroupMembers(groupOwid)
  if err != nil {
    return err
  }
  username2owid := make(map[string]string)
  for owid, name := range ms.Usernames {
    username2owid[name] = owid
  }

  for _, m := range members {
    memberOwid, ok := username2owid[m]
    if !ok {
      return fmt.Errorf(
        "user %q is not a member of %s", m, groupName)
    }
    if err := c.LeaveGroup(who.LeaveGroupRequest{
      GroupOwid:  groupOwid,
      MemberOwid: memberOwid,
    }); err != nil {
      return fmt.Errorf(
        "remove %s from %s: %v", m, groupName, err)
    }
  }
  fmt.Printf(
    "Removed %d member(s) from %s\n",
    len(members), groupName)
  return nil
}

func runGroupMembers(cfg *Config, args []string) error {
  fs := flag.NewFlagSet(
    "group members", flag.ContinueOnError)
  asJSON := fs.Bool("json", false,
    "output full DAG (Up/Down/Usernames) as raw JSON")
  if err := fs.Parse(args); err != nil {
    return err
  }
  positional := fs.Args()
  if len(positional) != 1 {
    return fmt.Errorf("usage: okg group members GROUP")
  }
  groupName := positional[0]

  c, err := newWhoClient(cfg)
  if err != nil {
    return err
  }
  groupOwid, err := resolveGroupOwid(c, groupName)
  if err != nil {
    return err
  }
  ms, err := c.GroupMembers(groupOwid)
  if err != nil {
    return err
  }

  if *asJSON {
    buf, err := json.MarshalIndent(ms, "", "  ")
    if err != nil {
      return err
    }
    fmt.Println(string(buf))
    return nil
  }

  // Direct members are the first level of Down. Print one
  // username per line; deeper nesting (group-of-groups) is
  // available via --json.
  if len(ms.Down) == 0 {
    return nil
  }
  for _, owid := range ms.Down[0] {
    fmt.Println(ms.Usernames[owid])
  }
  return nil
}

func runGroupDelete(cfg *Config, args []string) error {
  fs := flag.NewFlagSet(
    "group delete", flag.ContinueOnError)
  if err := fs.Parse(args); err != nil {
    return err
  }
  positional := fs.Args()
  if len(positional) != 1 {
    return fmt.Errorf("usage: okg group delete NAME")
  }
  groupName := positional[0]

  c, err := newWhoClient(cfg)
  if err != nil {
    return err
  }
  // /groups/delete takes an owid; resolve via /groups/list.
  groupOwid, err := resolveGroupOwid(c, groupName)
  if err != nil {
    return err
  }
  if err := c.DeleteGroup(who.DeleteGroupRequest{
    Owid: groupOwid,
  }); err != nil {
    return err
  }
  fmt.Printf("Deleted group %s\n", groupName)
  return nil
}

// resolveGroupOwid translates a group username to its owid via
// /groups/list. Returns an error if the group isn't visible to
// the caller.
func resolveGroupOwid(
  c *who.HTTPClient, name string,
) (string, error) {
  groups, err := c.ListGroups()
  if err != nil {
    return "", err
  }
  for _, g := range groups {
    if g.Username == name {
      return g.Owid, nil
    }
  }
  return "", fmt.Errorf(
    "group %q not found (or not visible to caller)", name)
}

// newWhoClient builds a //who client. Shared by every
// `okg group` and `okg authz` subcommand.
func newWhoClient(cfg *Config) (*who.HTTPClient, error) {
  if cfg.ApiKey == "" {
    return nil, fmt.Errorf(
      "no API key — run `okg auth login --key sk-...`")
  }
  return who.NewHTTPClient(cfg.Host, cfg.ApiKey), nil
}