From 40b4e1756ae3b01b95d8d9692e5ecd2a31ba87c6 Mon Sep 17 00:00:00 2001 From: shaoying Date: Tue, 22 Sep 2020 20:40:07 +0800 Subject: [PATCH] opt chat --- base/aaa/user.go | 4 +- base/ctx/ctx.go | 30 +++++ base/mdb/mdb.go | 14 ++- base/web/serve.go | 27 ++--- core/chat/action.go | 81 ++++++++++--- core/chat/chat.go | 276 +++---------------------------------------- core/chat/header.go | 5 + core/chat/river.go | 259 ++++++++++++++++------------------------ core/chat/trash.go | 89 ++++++++++++++ core/code/publish.go | 5 + type.go | 4 +- 11 files changed, 338 insertions(+), 456 deletions(-) create mode 100644 core/chat/trash.go diff --git a/base/aaa/user.go b/base/aaa/user.go index cb75e6ad..9aeac13f 100644 --- a/base/aaa/user.go +++ b/base/aaa/user.go @@ -17,12 +17,14 @@ func _user_list(m *ice.Message) { m.Sort(USERNAME) } func _user_login(m *ice.Message, name, word string) (ok bool) { + m.Debug("what %v %v", name, word) if m.Richs(USER, nil, name, nil) == nil { _user_create(m, name, "") } m.Richs(USER, nil, name, func(key string, value map[string]interface{}) { - if value[PASSWORD] == "" { + m.Debug("what %v %v", name, word) + if kit.Format(value[PASSWORD]) == "" { ok, value[PASSWORD] = true, word } else if value[PASSWORD] == word { ok = true diff --git a/base/ctx/ctx.go b/base/ctx/ctx.go index d582a41c..8c277837 100644 --- a/base/ctx/ctx.go +++ b/base/ctx/ctx.go @@ -1,7 +1,10 @@ package ctx import ( + "strings" + ice "github.com/shylinux/icebergs" + "github.com/shylinux/icebergs/base/mdb" kit "github.com/shylinux/toolkits" ) @@ -58,6 +61,33 @@ var Index = &ice.Context{Name: "ctx", Help: "配置模块", }) } }}, + ice.CTX_INIT: {Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { + m.Cmd(mdb.SEARCH, mdb.CREATE, COMMAND, m.AddCmd(&ice.Command{Hand: func(m *ice.Message, c *ice.Context, cc string, arg ...string) { + arg = arg[1:] + ice.Pulse.Travel(func(p *ice.Context, s *ice.Context, key string, cmd *ice.Command) { + if strings.HasPrefix(key, "_") || strings.HasPrefix(key, "/") { + return + } + if arg[1] != "" && arg[1] != key && arg[1] != s.Name { + return + } + if arg[2] != "" && !strings.Contains(kit.Format(cmd.Name), arg[2]) && !strings.Contains(kit.Format(cmd.Help), arg[2]) { + return + } + + m.Push("pod", "") + m.Push("ctx", "web.chat") + m.Push("cmd", cc) + + m.Push("time", m.Time()) + m.Push("size", "") + + m.Push("type", COMMAND) + m.Push("name", key) + m.Push("text", s.Cap(ice.CTX_FOLLOW)) + }) + }})) + }}, }, } diff --git a/base/mdb/mdb.go b/base/mdb/mdb.go index fbbe2562..e2273d09 100644 --- a/base/mdb/mdb.go +++ b/base/mdb/mdb.go @@ -147,13 +147,17 @@ func _list_insert(m *ice.Message, prefix, key string, arg ...string) int { func _list_delete(m *ice.Message, prefix, chain, field, value string) { } func _list_select(m *ice.Message, prefix, key, field, value string) { - fields := strings.Split(kit.Select("time,type,name,text", m.Option("fields")), ",") + fields := strings.Split(kit.Select("time,type,name,text", m.Option(FIELDS)), ",") m.Grows(prefix, key, field, value, func(index int, val map[string]interface{}) { - if field == kit.MDB_ID && value != "" { - m.Push("detail", val) - return + if val[kit.MDB_META] != nil { + val = val[kit.MDB_META].(map[string]interface{}) + } + + if m.Option(FIELDS) == "detail" { + m.Push("detail", val) + } else { + m.Push(key, val, fields, val[kit.MDB_META]) } - m.Push("", val, fields, val[kit.MDB_META]) }) if m.Option(FIELDS) != "detail" { m.Sort(kit.MDB_ID, "int_r") diff --git a/base/web/serve.go b/base/web/serve.go index 1bfd7509..1173cb9b 100644 --- a/base/web/serve.go +++ b/base/web/serve.go @@ -4,7 +4,6 @@ import ( ice "github.com/shylinux/icebergs" "github.com/shylinux/icebergs/base/aaa" "github.com/shylinux/icebergs/base/cli" - "github.com/shylinux/icebergs/base/gdb" "github.com/shylinux/icebergs/base/mdb" "github.com/shylinux/icebergs/base/tcp" kit "github.com/shylinux/toolkits" @@ -13,7 +12,6 @@ import ( "encoding/json" "net/http" "net/url" - "os" "path" "strings" ) @@ -193,16 +191,7 @@ func _serve_main(m *ice.Message, w http.ResponseWriter, r *http.Request) bool { r.URL.Path = strings.Replace(r.URL.Path, "/debug", "/code", -1) } - if r.URL.Path == "/" && m.Conf(SERVE, "meta.init") != "true" && len(ice.BinPack) == 0 { - if _, e := os.Stat(m.Conf(SERVE, "meta.volcanos.path")); e == nil { - // 初始化成功 - m.Conf(SERVE, "meta.init", "true") - } - m.W = w - Render(m, "refresh", m.Conf(SERVE, "meta.volcanos.refresh")) - m.Event(gdb.SYSTEM_INIT) - m.W = nil - } else if r.URL.Path == "/" && m.Conf(SERVE, "meta.sso") != "" { + if r.URL.Path == "/" && m.Conf(SERVE, "meta.sso") != "" { if r.ParseForm(); r.FormValue(ice.MSG_SESSID) != "" { return true } @@ -220,7 +209,7 @@ func _serve_main(m *ice.Message, w http.ResponseWriter, r *http.Request) bool { } return true } else if r.URL.Path == "/share" && r.Method == "GET" { - http.ServeFile(w, r, m.Conf(SERVE, "meta.page.share")) + http.ServeFile(w, r, m.Conf(SERVE, "meta.volcanos.share")) } else { if b, ok := ice.BinPack[r.URL.Path]; ok { log.Info("BinPack %v %v", r.URL.Path, len(b)) @@ -241,9 +230,10 @@ func init() { Index.Merge(&ice.Context{ Configs: map[string]*ice.Config{ SERVE: {Name: "serve", Help: "服务器", Value: kit.Data( - "init", "false", "logheaders", "false", + "logheaders", "false", "black", kit.Dict(), "white", kit.Dict( + "header", true, "login", true, "share", true, "space", true, @@ -254,19 +244,18 @@ func init() { ), "intshell", kit.Dict( - "path", "usr/intshell", "index", "index.sh", "require", ".ish/pluged", + "index", "index.sh", + "path", "usr/intshell", "require", ".ish/pluged", "repos", "https://github.com/shylinux/volcanos", "branch", "master", ), - "static", kit.Dict("/", "usr/volcanos/"), "volcanos", kit.Dict("refresh", "5", + "share", "usr/volcanos/page/share.html", "path", "usr/volcanos", "require", ".ish/pluged", "repos", "https://github.com/shylinux/volcanos", "branch", "master", - ), "page", kit.Dict( - "index", "usr/volcanos/page/index.html", - "share", "usr/volcanos/page/share.html", ), "publish", "usr/publish/", + "static", kit.Dict("/", "usr/volcanos/"), "template", kit.Dict("path", "usr/template", "list", []interface{}{ `{{define "raw"}}{{.Result}}{{end}}`, }), diff --git a/core/chat/action.go b/core/chat/action.go index cfe95d14..313e7c0d 100644 --- a/core/chat/action.go +++ b/core/chat/action.go @@ -67,6 +67,24 @@ func _action_order_list(m *ice.Message, river, storm string, arg ...string) { } } func _action_list(m *ice.Message, river, storm string) { + m.Option(ice.MSG_RIVER, river) + m.Cmd(TOOL, storm).Table(func(index int, value map[string]string, head []string) { + m.Push(RIVER, river) + m.Push(STORM, storm) + m.Push(ACTION, value[kit.MDB_ID]) + + m.Push("node", value[POD]) + m.Push("group", value[CTX]) + m.Push("index", value[CMD]) + + msg := m.Cmd(m.Space(value[POD]), ctx.COMMAND, kit.Keys(value[CTX], value[CMD])) + ls := strings.Split(kit.Format(value[CMD]), ".") + m.Push("name", ls[len(ls)-1]) + m.Push("help", kit.Select(msg.Append("help"), kit.Format(value["help"]))) + m.Push("feature", msg.Append("meta")) + m.Push("inputs", msg.Append("list")) + }) + return if p := m.Option(POD); p != "" { m.Option(POD, "") // 代理列表 @@ -98,9 +116,32 @@ func _action_list(m *ice.Message, river, storm string) { } }) } + +func _action_right(m *ice.Message, river string, storm string) (ok bool) { + if ok = true; m.Option(ice.MSG_USERROLE) == aaa.VOID { + m.Richs(RIVER, "", river, func(key string, value map[string]interface{}) { + if ok = m.Richs(RIVER, kit.Keys(kit.MDB_HASH, key, USER), m.Option(ice.MSG_USERNAME), nil) != nil; ok { + m.Log_AUTH(RIVER, river, STORM, storm) + } + }) + } + return ok +} + func _action_show(m *ice.Message, river, storm, index string, arg ...string) { + cmds := []string{index} prefix := kit.Keys(kit.MDB_HASH, river, TOOL, kit.MDB_HASH, storm) - cmds := []string{} + if m.Grows(RIVER, prefix, kit.MDB_ID, index, func(index int, value map[string]interface{}) { + if cmds = kit.Simple(kit.Keys(value[CTX], value[CMD])); value[POD] != "" { + m.Option(POD, value[POD]) + } + }) == nil && m.Warn(!m.Right(cmds), ice.ErrNotAuth) { + m.Debug("what %v", prefix) + return + } + + m.Cmdy(_action_proxy(m), cmds, arg) + return if i, e := strconv.Atoi(index); e == nil { m.Richs(web.SHARE, nil, m.Option("share"), func(key string, value map[string]interface{}) { @@ -162,8 +203,8 @@ func _action_show(m *ice.Message, river, storm, index string, arg ...string) { m.Cmdy(_action_proxy(m), cmds) } func _action_proxy(m *ice.Message) (proxy []string) { - if m.Option(POD) != "" { - proxy = append(proxy, web.SPACE, m.Option(POD)) + if p := m.Option(POD); p != "" { + proxy = append(proxy, web.SPACE, p) m.Option(POD, "") } return proxy @@ -176,7 +217,19 @@ const ACTION = "action" func init() { Index.Merge(&ice.Context{Commands: map[string]*ice.Command{ - "/" + ACTION: {Name: "/action", Help: "工作台", Action: map[string]*ice.Action{ + ACTION: {Name: "action hash auto 添加", Help: "群组", Action: map[string]*ice.Action{ + ctx.COMMAND: {Name: "command", Help: "命令", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy(ctx.COMMAND, arg[0]) + }}, + }, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { + if len(arg) == 0 { + _action_list(m, m.Option(ice.MSG_RIVER), m.Option(ice.MSG_STORM)) + return + } + _action_show(m, m.Option(ice.MSG_RIVER), m.Option(ice.MSG_STORM), arg[0], arg[1:]...) + }}, + + "/action": {Name: "/action", Help: "工作台", Action: map[string]*ice.Action{ web.SHARE: {Name: "share name text [pod ctx cmd arg]...", Help: "共享", Hand: func(m *ice.Message, arg ...string) { _action_share_create(m, arg[0], arg[1], arg[2:]...) }}, @@ -191,21 +244,19 @@ func init() { _action_order_list(m, m.Option(ice.MSG_RIVER), m.Option(ice.MSG_STORM), arg...) }}, - "command": {Name: "command", Help: "命令", Hand: func(m *ice.Message, arg ...string) { - if len(arg) == 1 { - m.Cmdy(ctx.COMMAND, arg[0]) - return - } - m.Cmdy(arg[0], arg[1:]) + ctx.COMMAND: {Name: "command", Help: "命令", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy(ctx.COMMAND, arg[0]) }}, }, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { - if len(arg) == 0 { - // 命令列表 - _action_list(m, m.Option(ice.MSG_RIVER), m.Option(ice.MSG_STORM)) + if m.Warn(!_action_right(m, m.Option(ice.MSG_RIVER, arg[0]), m.Option(ice.MSG_STORM, arg[1])), ice.ErrNotAuth) { return } - // 执行命令 - _action_show(m, m.Option(ice.MSG_RIVER), m.Option(ice.MSG_STORM), arg[0], arg[1:]...) + + if len(arg) == 2 { + _action_list(m, arg[0], arg[1]) + return + } + _action_show(m, arg[0], arg[1], arg[2], arg[3:]...) }}, }}, nil) } diff --git a/core/chat/chat.go b/core/chat/chat.go index f2f54987..394fb259 100644 --- a/core/chat/chat.go +++ b/core/chat/chat.go @@ -2,275 +2,35 @@ package chat import ( ice "github.com/shylinux/icebergs" - "github.com/shylinux/icebergs/base/aaa" - "github.com/shylinux/icebergs/base/ctx" - "github.com/shylinux/icebergs/base/gdb" - "github.com/shylinux/icebergs/base/mdb" "github.com/shylinux/icebergs/base/web" kit "github.com/shylinux/toolkits" - - "strings" ) -var Index = &ice.Context{Name: "chat", Help: "聊天中心", - Configs: map[string]*ice.Config{}, +const CHAT = "chat" + +var Index = &ice.Context{Name: CHAT, Help: "聊天中心", Commands: map[string]*ice.Command{ ice.CTX_INIT: {Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { m.Load() - m.Watch(gdb.SYSTEM_INIT, m.Prefix("init")) - m.Watch(gdb.USER_CREATE, m.Prefix("auto")) - m.Cmd(mdb.SEARCH, mdb.CREATE, ctx.COMMAND, m.AddCmd(&ice.Command{Hand: func(m *ice.Message, c *ice.Context, cc string, arg ...string) { - arg = arg[1:] - ice.Pulse.Travel(func(p *ice.Context, s *ice.Context, key string, cmd *ice.Command) { - if strings.HasPrefix(key, "_") || strings.HasPrefix(key, "/") { - return - } - if arg[1] != "" && arg[1] != key && arg[1] != s.Name { - return - } - if arg[2] != "" && !strings.Contains(kit.Format(cmd.Name), arg[2]) && !strings.Contains(kit.Format(cmd.Help), arg[2]) { - return - } - - m.Push("pod", "") - m.Push("ctx", "web.chat") - m.Push("cmd", cc) - - m.Push("time", m.Time()) - m.Push("size", "") - - m.Push("type", ctx.COMMAND) - m.Push("name", key) - m.Push("text", s.Cap(ice.CTX_FOLLOW)) - }) - }})) + m.Conf(RIVER, "meta.template", kit.Dict( + "base", kit.Dict( + "info", []interface{}{ + "web.chat.info", + "web.chat.node", + "web.chat.tool", + "web.chat.user", + }, + "miss", []interface{}{ + "web.team.task", + "web.team.plan", + "web.wiki.word", + }, + ), + )) }}, ice.CTX_EXIT: {Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { m.Save(RIVER) }}, - - "init": {Name: "init", Help: "初始化", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { - if len(m.Confm(RIVER, kit.MDB_HASH)) == 0 { - // 黑名单 - kit.Fetch(m.Confv(RIVER, "meta.black.tech"), func(index int, value interface{}) { - m.Cmd(aaa.ROLE, aaa.Black, aaa.TECH, value) - }) - // 白名单 - kit.Fetch(m.Confv(RIVER, "meta.white.void"), func(index int, value interface{}) { - m.Cmd(aaa.ROLE, aaa.White, aaa.VOID, value) - }) - } - m.Cap(ice.CTX_STATUS, "start") - }}, - "auto": {Name: "auto user", Help: "自动化", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { - m.Richs(aaa.USER, nil, arg[0], func(key string, value map[string]interface{}) { - m.Option(ice.MSG_USERNICK, value[aaa.USERNAME]) - m.Option(ice.MSG_USERNAME, value[aaa.USERNAME]) - - // 创建应用 - storm, river := "", "" - m.Option("cache.limit", -2) - web.FavorList(m, kit.Keys(c.Cap(ice.CTX_FOLLOW), aaa.UserRole(m, value[aaa.USERNAME])), "").Table(func(index int, value map[string]string, head []string) { - switch value[kit.MDB_TYPE] { - case "river": - name, _ := kit.Render(kit.Format(value["name"]), m) - river = m.Option(ice.MSG_RIVER, m.Cmdx("/ocean", "spawn", string(name))) - case "storm": - storm = m.Option(ice.MSG_STORM, m.Cmdx("/steam", river, "spawn", value["name"])) - case "field": - m.Cmd("/storm", river, storm, "add", "", kit.Select("", value["text"]), value["name"], "") - } - }) - }) - }}, - - web.LOGIN: {Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { - m.Option(ice.MSG_RIVER, m.Option("river")) - m.Option(ice.MSG_STORM, m.Option("storm")) - - if len(arg) > 0 { - switch arg[0] { - case "login": - // 密码登录 - if len(arg) > 2 && aaa.UserLogin(m, arg[1], arg[2]) { - m.Option(ice.MSG_SESSID, aaa.SessCreate(m, m.Option(ice.MSG_USERNAME), m.Option(ice.MSG_USERROLE))) - web.Render(m, web.COOKIE, m.Option(ice.MSG_SESSID)) - } - - default: - switch m.Option(ice.MSG_USERURL) { - case "/river": - if len(arg) > 0 { - m.Option(ice.MSG_RIVER, arg[0]) - arg = arg[1:] - } - if len(arg) > 1 && arg[1] == "storm" { - m.Option(ice.MSG_STORM, arg[0]) - arg = arg[1:] - } - case "/storm": - if len(arg) > 0 { - m.Option(ice.MSG_RIVER, arg[0]) - arg = arg[1:] - } - if len(arg) > 0 { - m.Option(ice.MSG_STORM, arg[0]) - arg = arg[1:] - } - case "/action": - if len(arg) > 0 { - if arg[0] != "" { - m.Option(ice.MSG_RIVER, arg[0]) - } - arg = arg[1:] - } - if len(arg) > 0 { - if arg[0] != "" { - m.Option(ice.MSG_STORM, arg[0]) - } - arg = arg[1:] - } - } - - // 群组检查 - m.Richs(RIVER, nil, m.Option(ice.MSG_RIVER), func(key string, value map[string]interface{}) { - if kit.Value(value, "meta.type") == "private" { - m.Option(ice.MSG_DOMAIN, "U"+m.Option(ice.MSG_USERNAME)) - } else if kit.Value(value, "meta.type") != "public" { - m.Option(ice.MSG_DOMAIN, "R"+m.Option(ice.MSG_RIVER)) - } - - if m.Richs(RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), USER), m.Option(ice.MSG_USERNAME), func(key string, value map[string]interface{}) { - // 应用检查 - m.Richs(RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL), m.Option(ice.MSG_STORM), func(key string, value map[string]interface{}) { - - }) - }) == nil { - if m.Option("share") != "" { - m.Richs(web.SHARE, "", m.Option("share"), func(key string, value map[string]interface{}) { - if m.Option(ice.MSG_RIVER) == kit.Value(value, "extra.river") && m.Richs(RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), USER), kit.Value(value, "extra.username"), nil) != nil { - return - } - - // m.Option(ice.MSG_USERNAME, kit.Value(value, "extra.username")) - // m.Option(ice.MSG_USERROLE, kit.Value(value, "extra.userrole")) - // m.Log_AUTH(web.SHARE, key, "username", m.Option(ice.MSG_USERNAME)) - m.Render(web.STATUS, 403, "not join") - m.Option(ice.MSG_USERURL, "") - return - }) - return - } - m.Render(web.STATUS, 403, "not join") - m.Option(ice.MSG_USERURL, "") - return - } - }) - m.Log_AUTH(RIVER, m.Option(ice.MSG_RIVER), STORM, m.Option(ice.MSG_STORM), "domain", m.Option(ice.MSG_DOMAIN)) - m.Optionv(ice.MSG_CMDS, arg) - } - } - - if m.Option(ice.MSG_USERURL) == "/header" { - // 免检 - return - } - if m.Warn(!m.Right(m.Option(ice.MSG_USERURL), m.Optionv(ice.MSG_CMDS))) { - return - } - - // 登录检查 - if m.Warn(!m.Options(ice.MSG_USERNAME), "not login") { - if m.Option("share") == "" { - m.Render(web.STATUS, 401, "not login") - m.Option(ice.MSG_USERURL, "") - return - } - m.Option(ice.MSG_USERROLE, aaa.VOID) - } - - // 权限检查 - if m.Warn(!m.Right(m.Option(ice.MSG_USERURL), m.Optionv(ice.MSG_CMDS)), "not auth") { - m.Render(web.STATUS, 403, "not auth") - m.Option(ice.MSG_USERURL, "") - return - } - - }}, - - "/ocean": {Name: "/ocean", Help: "大海洋", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { - if len(arg) == 0 { - // 用户列表 - m.Richs(aaa.USER, nil, "*", func(key string, value map[string]interface{}) { - m.Push(key, value, []string{"username", "usernode"}) - }) - return - } - - switch arg[0] { - case "spawn": - // 创建群组 - river := m.Rich(RIVER, nil, kit.Dict( - kit.MDB_META, kit.Dict(kit.MDB_NAME, arg[1]), - "user", kit.Data(kit.MDB_SHORT, "username"), - "tool", kit.Data(), - )) - m.Log(ice.LOG_CREATE, "river: %v name: %v", river, arg[1]) - // 添加用户 - m.Cmd("/river", river, "add", m.Option(ice.MSG_USERNAME), arg[2:]) - m.Echo(river) - } - }}, - "/steam": {Name: "/steam", Help: "大气层", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { - if m.Warn(m.Option(ice.MSG_RIVER) == "", "not join") { - m.Render("status", 402, "not join") - return - } - - if len(arg) < 2 { - if list := []string{}; m.Option("pod") != "" { - // 远程空间 - m.Cmdy(web.SPACE, m.Option("pod"), "web.chat./steam").Table(func(index int, value map[string]string, head []string) { - list = append(list, kit.Keys(m.Option("pod"), value["name"])) - }) - m.Append("name", list) - } else { - // 本地空间 - m.Richs(web.SPACE, nil, "*", func(key string, value map[string]interface{}) { - switch value[kit.MDB_TYPE] { - case web.SERVER, web.WORKER: - m.Push(key, value, []string{"type", "name", "user"}) - } - }) - } - return - } - - if m.Warn(!m.Right(cmd, arg[1])) { - m.Render("status", 403, "not auth") - return - } - - switch arg[1] { - case "spawn": - // 创建应用 - storm := m.Rich(RIVER, kit.Keys(kit.MDB_HASH, arg[0], "tool"), kit.Dict( - kit.MDB_META, kit.Dict(kit.MDB_NAME, arg[2]), - )) - m.Log(ice.LOG_CREATE, "storm: %s name: %v", storm, arg[2]) - // 添加命令 - m.Cmd("/storm", arg[0], storm, "add", arg[3:]) - m.Echo(storm) - - case "append": - // 追加命令 - m.Cmd("/storm", arg[0], arg[2], "add", arg[3:]) - - default: - // 命令列表 - m.Cmdy(web.SPACE, arg[2], ctx.COMMAND) - } - }}, }, } diff --git a/core/chat/header.go b/core/chat/header.go index eef04e2e..2f3967fd 100644 --- a/core/chat/header.go +++ b/core/chat/header.go @@ -4,6 +4,7 @@ import ( ice "github.com/shylinux/icebergs" "github.com/shylinux/icebergs/base/aaa" "github.com/shylinux/icebergs/base/nfs" + "github.com/shylinux/icebergs/base/web" kit "github.com/shylinux/toolkits" "fmt" @@ -51,6 +52,10 @@ func init() { m.Echo(m.Option(ice.MSG_USERNAME)) }}, LOGIN: {Name: "login", Help: "用户登录", Hand: func(m *ice.Message, arg ...string) { + if aaa.UserLogin(m, arg[0], arg[1]) { + m.Option(ice.MSG_SESSID, aaa.SessCreate(m, m.Option(ice.MSG_USERNAME), m.Option(ice.MSG_USERROLE))) + web.Render(m, web.COOKIE, m.Option(ice.MSG_SESSID)) + } m.Echo(m.Option(ice.MSG_USERNAME)) }}, diff --git a/core/chat/river.go b/core/chat/river.go index 2b1e8da2..feb3a6d9 100644 --- a/core/chat/river.go +++ b/core/chat/river.go @@ -3,10 +3,8 @@ package chat import ( ice "github.com/shylinux/icebergs" "github.com/shylinux/icebergs/base/aaa" - "github.com/shylinux/icebergs/base/cli" "github.com/shylinux/icebergs/base/ctx" "github.com/shylinux/icebergs/base/mdb" - "github.com/shylinux/icebergs/base/tcp" "github.com/shylinux/icebergs/base/web" kit "github.com/shylinux/toolkits" ) @@ -21,159 +19,40 @@ func _river_list(m *ice.Message) { m.Cmdy(web.SPACE, p, "web.chat./river") } m.Richs(RIVER, nil, kit.MDB_FOREACH, func(key string, value map[string]interface{}) { - if kit.Value(value, "meta.type") == "public" { - m.Push(key, value[kit.MDB_META], []string{kit.MDB_KEY, kit.MDB_NAME}) - return - } m.Richs(RIVER, kit.Keys(kit.MDB_HASH, key, USER), m.Option(ice.MSG_USERNAME), func(k string, val map[string]interface{}) { m.Push(key, value[kit.MDB_META], []string{kit.MDB_KEY, kit.MDB_NAME}, val[kit.MDB_META]) }) }) } -func _river_share(m *ice.Message, river, name string, arg ...string) { - m.Cmdy(web.SHARE, RIVER, name, river, arg) -} -func _river_remove(m *ice.Message, river string) { - m.Richs(RIVER, nil, river, func(value map[string]interface{}) { - m.Log_REMOVE(RIVER, river, kit.MDB_VALUE, kit.Format(value)) - }) - m.Conf(RIVER, kit.Keys(kit.MDB_HASH, river), "") -} -func _river_rename(m *ice.Message, river string, name string) { - prefix := kit.Keys(kit.MDB_HASH, river, kit.MDB_META, kit.MDB_NAME) - old := m.Conf(RIVER, prefix) - m.Log_MODIFY(RIVER, river, kit.MDB_VALUE, name, "old", old) - m.Conf(RIVER, prefix, name) -} -func _river_create(m *ice.Message, kind, name, text string, arg ...string) { - h := m.Rich(RIVER, nil, kit.Dict(kit.MDB_META, kit.Dict( - kit.MDB_TYPE, kind, kit.MDB_NAME, name, kit.MDB_TEXT, text, - kit.MDB_EXTRA, kit.Dict(arg), - ), - USER, kit.Data(kit.MDB_SHORT, aaa.USERNAME), - TOOL, kit.Data(), - )) - m.Log_CREATE(kit.MDB_META, RIVER, kit.MDB_TYPE, kind, kit.MDB_NAME, name) - - m.Option(ice.MSG_RIVER, h) - m.Cmdy(m.Prefix(USER), mdb.INSERT, aaa.USERNAME, cli.UserName) - m.Cmdy(m.Prefix(USER), mdb.INSERT, aaa.USERNAME, m.Option(ice.MSG_USERNAME)) - - kit.Fetch(m.Confv(RIVER, kit.Keys("meta.template", "base")), func(storm string, value interface{}) { - list := []string{} - kit.Fetch(value, func(index int, value string) { - m.Search(value, func(p *ice.Context, s *ice.Context, key string, cmd *ice.Command) { - list = append(list, "", s.Cap(ice.CTX_FOLLOW), key, kit.Simple(cmd.Help)[0]) - }) - }) - storm = _storm_create(m, h, "", storm, "") - _storm_tool(m, h, storm, list...) - }) - m.Set(ice.MSG_RESULT) - m.Echo(h) -} const ( - USER = "user" + INFO = "info" NODE = "node" TOOL = "tool" + USER = "user" ) const RIVER = "river" func init() { Index.Merge(&ice.Context{ Configs: map[string]*ice.Config{ - RIVER: {Name: "river", Help: "群组", Value: kit.Data( - "template", kit.Dict( - "base", kit.Dict( - "info", []interface{}{ - "web.chat.info", - "web.chat.tool", - "web.chat.node", - "web.chat.user", - }, - "miss", []interface{}{ - "web.team.task", - "web.team.plan", - "web.wiki.word", - }, - ), - ), - aaa.Black, kit.Dict(aaa.TECH, []interface{}{ - "/river.rename", - "/river.remove", - "/storm.remove", - "/storm.rename", - }), - aaa.White, kit.Dict(aaa.VOID, []interface{}{ - "/header", - "/river", - "/storm", - "/action", - "/footer", - }), - )}, + RIVER: {Name: RIVER, Help: "群组", Value: kit.Data()}, }, Commands: map[string]*ice.Command{ - "info": {Name: "info auto 导出:button", Help: "信息", Action: map[string]*ice.Action{ + INFO: {Name: "info auto", Help: "信息", Action: map[string]*ice.Action{ mdb.MODIFY: {Name: "modify", Help: "编辑", Hand: func(m *ice.Message, arg ...string) { - arg[0] = kit.Keys(kit.MDB_META, arg[0]) - m.Richs(RIVER, nil, m.Option(ice.MSG_RIVER), func(key string, value map[string]interface{}) { - m.Log_MODIFY(RIVER, m.Option(ice.MSG_RIVER), arg[0], arg[1], "old", kit.Format(kit.Value(value, arg[0]))) - kit.Value(value, arg[0], arg[1]) - }) - }}, - mdb.EXPORT: {Name: "export file", Help: "导出", Hand: func(m *ice.Message, arg ...string) { - m.Cmdy(mdb.EXPORT, m.Prefix(RIVER), "", mdb.HASH) - }}, - mdb.IMPORT: {Name: "import file", Help: "导入", Hand: func(m *ice.Message, arg ...string) { - m.Cmdy(mdb.IMPORT, m.Prefix(RIVER), "", mdb.HASH) + m.Cmdy(mdb.EXPORT, RIVER, "", mdb.HASH, kit.MDB_HASH, m.Option(kit.MDB_HASH), arg) }}, }, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { - m.Richs(RIVER, nil, m.Option(ice.MSG_RIVER), func(key string, value map[string]interface{}) { - m.Push("detail", value[kit.MDB_META]) - }) - }}, - TOOL: {Name: "tool hash=auto auto 添加 创建", Help: "工具", Action: map[string]*ice.Action{ - mdb.INPUTS: {Name: "inputs", Help: "补全", Hand: func(m *ice.Message, arg ...string) { - switch arg[0] { - case "storm": - _storm_list(m, m.Option(ice.MSG_RIVER)) - case "ctx": - m.Cmdy(ctx.COMMAND) - case "cmd", "help": - m.Cmdy(ctx.COMMAND) - } - }}, - mdb.CREATE: {Name: "create type=public,protected,private name=hi text=hello", Help: "创建", Hand: func(m *ice.Message, arg ...string) { - _storm_create(m, m.Option(ice.MSG_RIVER), m.Option("type"), m.Option("name"), m.Option("text")) - }}, - mdb.INSERT: {Name: "insert storm ctx cmd help", Help: "添加", Hand: func(m *ice.Message, arg ...string) { - m.Cmdy(mdb.INSERT, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL, kit.MDB_HASH, m.Option(kit.MDB_HASH)), mdb.LIST, arg) - }}, - mdb.MODIFY: {Name: "modify", Help: "编辑", Hand: func(m *ice.Message, arg ...string) { - if m.Option(kit.MDB_ID) != "" { - m.Cmdy(mdb.MODIFY, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL, kit.MDB_HASH, m.Option(kit.MDB_HASH)), mdb.LIST, kit.MDB_ID, m.Option(kit.MDB_ID), arg) - } else { - m.Cmdy(mdb.MODIFY, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL), mdb.HASH, kit.MDB_HASH, m.Option(kit.MDB_HASH), arg) - } - }}, - }, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { - if len(arg) == 0 { - m.Option(mdb.FIELDS, "time,hash,name,count") - m.Cmdy(mdb.SELECT, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL), mdb.HASH) - } else { - m.Option(mdb.FIELDS, "time,id,ctx,cmd,help") - m.Cmdy(mdb.SELECT, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL, kit.MDB_HASH, arg[0]), mdb.LIST, kit.MDB_ID, arg[1:]) - } - m.PushAction("删除") + m.Option(mdb.FIELDS, mdb.DETAIL) + m.Cmdy(mdb.SELECT, RIVER, "", mdb.HASH, kit.MDB_HASH, m.Option(ice.MSG_RIVER)) }}, NODE: {Name: "node hash=auto auto 添加 启动", Help: "设备", Action: map[string]*ice.Action{ - "start": {Name: "start", Help: "启动", Hand: func(m *ice.Message, arg ...string) { - m.Cmdy("web.code.install", "contexts", "base") + "invite": {Name: "invite", Help: "添加", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy("web.code.publish", "contexts", "tool") }}, - mdb.INPUTS: {Name: "inputs", Help: "补全", Hand: func(m *ice.Message, arg ...string) { - m.Cmdy(web.ROUTE) + "start": {Name: "start", Help: "启动", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy("web.code.publish", "contexts", "tool") }}, mdb.INSERT: {Name: "insert route", Help: "添加", Hand: func(m *ice.Message, arg ...string) { m.Cmdy(mdb.INSERT, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), NODE), mdb.HASH, arg) @@ -181,6 +60,9 @@ func init() { mdb.DELETE: {Name: "delete", Help: "删除", Hand: func(m *ice.Message, arg ...string) { m.Cmdy(mdb.DELETE, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), NODE), mdb.HASH, kit.MDB_HASH, m.Option(kit.MDB_HASH)) }}, + mdb.INPUTS: {Name: "inputs", Help: "补全", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy(web.ROUTE) + }}, }, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { m.Option(mdb.FIELDS, "time,hash,route") m.Cmdy(mdb.SELECT, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), NODE), mdb.HASH) @@ -189,6 +71,48 @@ func init() { }) m.PushAction("删除") }}, + TOOL: {Name: "tool key auto 添加 创建", Help: "工具", Action: map[string]*ice.Action{ + mdb.CREATE: {Name: "create type=public,protected,private name=hi text=hello", Help: "创建", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy(mdb.INSERT, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL), mdb.HASH, arg) + }}, + mdb.INSERT: {Name: "insert pod ctx cmd help", Help: "添加", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy(mdb.INSERT, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL, kit.MDB_HASH, m.Option(ice.MSG_STORM)), mdb.LIST, arg) + }}, + mdb.MODIFY: {Name: "modify", Help: "编辑", Hand: func(m *ice.Message, arg ...string) { + if m.Option(kit.MDB_ID) != "" { + m.Cmdy(mdb.MODIFY, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL, kit.MDB_HASH, m.Option(ice.MSG_STORM)), mdb.LIST, kit.MDB_ID, m.Option(kit.MDB_ID), arg) + } else { + m.Cmdy(mdb.MODIFY, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL), mdb.HASH, kit.MDB_HASH, m.Option(kit.MDB_HASH), arg) + } + }}, + mdb.INPUTS: {Name: "inputs", Help: "补全", Hand: func(m *ice.Message, arg ...string) { + switch arg[0] { + case "pod": + _storm_list(m, m.Option(ice.MSG_RIVER)) + case "ctx": + m.Cmdy(ctx.CONTEXT) + case "cmd", "help": + m.Cmdy(ctx.CONTEXT, m.Option("ctx"), ctx.COMMAND) + } + }}, + }, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { + if len(arg) == 0 { + m.Option(mdb.FIELDS, "time,key,name,count") + m.Cmdy(mdb.SELECT, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL), mdb.HASH) + } else { + m.Option(mdb.FIELDS, "time,id,pod,ctx,cmd,help") + m.Cmdy(mdb.SELECT, RIVER, kit.Keys(kit.MDB_HASH, m.Option(ice.MSG_RIVER), TOOL, kit.MDB_HASH, arg[0]), mdb.LIST, kit.MDB_ID, arg[1:]) + if len(m.Appendv(CMD)) == 0 { + m.Push("time", m.Time()) + m.Push("id", "") + m.Push("pod", "") + m.Push("ctx", "") + m.Push("cmd", arg[1]) + m.Push("help", "") + } + } + m.PushAction("删除") + }}, USER: {Name: "user hash=auto auto 添加 邀请", Help: "用户", Action: map[string]*ice.Action{ "invite": {Name: "invite", Help: "邀请", Hand: func(m *ice.Message, arg ...string) { m.Cmdy("web.wiki.spark", "inner", kit.MergeURL(m.Option(ice.MSG_USERWEB), "river", m.Option(ice.MSG_RIVER))) @@ -213,33 +137,56 @@ func init() { }) m.PushAction("删除") }}, + RIVER: {Name: "river hash auto 添加", Help: "群组", Action: map[string]*ice.Action{ + mdb.CREATE: {Name: "create type=public,protected,private name=hi text=hello", Help: "添加", Hand: func(m *ice.Message, arg ...string) { + h := m.Cmdx(mdb.INSERT, RIVER, "", mdb.HASH, arg) + m.Option(ice.MSG_RIVER, h) + m.Echo(h) - "/river": {Name: "/river", Help: "小河流", - Action: map[string]*ice.Action{ - mdb.CREATE: {Name: "create type name text arg...", Help: "创建", Hand: func(m *ice.Message, arg ...string) { - _river_create(m, arg[0], arg[1], arg[2], arg[3:]...) - }}, - mdb.RENAME: {Name: "rename name", Help: "更名", Hand: func(m *ice.Message, arg ...string) { - _river_rename(m, m.Option(ice.MSG_RIVER), arg[0]) - }}, - mdb.REMOVE: {Name: "remove", Help: "删除", Hand: func(m *ice.Message, arg ...string) { - _river_remove(m, m.Option(ice.MSG_RIVER)) - }}, - web.SHARE: {Name: "share name", Help: "共享", Hand: func(m *ice.Message, arg ...string) { - _river_share(m, m.Option(ice.MSG_RIVER), arg[0]) - }}, - }, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { - if len(arg) > 0 && arg[0] == "storm" { - m.Cmdy("/storm", arg[1:]) - return - } - if m.Option("_source") == "" && m.Option(ice.MSG_SESSID) == "" && !tcp.IPIsLocal(m, m.Option(ice.MSG_USERIP)) { - m.Render("status", "401") - return - } + m.Conf(RIVER, kit.Keys(kit.MDB_HASH, h, USER, kit.MDB_META, kit.MDB_SHORT), aaa.USERNAME) + m.Cmd(USER, mdb.INSERT, aaa.USERNAME, m.Option(ice.MSG_USERNAME)) + kit.Fetch(m.Confv(RIVER, kit.Keys("meta.template", "base")), func(storm string, value interface{}) { + h := m.Cmdx(TOOL, mdb.CREATE, kit.MDB_TYPE, "public", kit.MDB_NAME, storm, kit.MDB_TEXT, storm) + m.Option(ice.MSG_STORM, h) - _river_list(m) + kit.Fetch(value, func(index int, value string) { + m.Search(value, func(p *ice.Context, s *ice.Context, key string, cmd *ice.Command) { + m.Cmd(TOOL, mdb.INSERT, "pod", "", "ctx", s.Cap(ice.CTX_FOLLOW), "cmd", key, "help", kit.Simple(cmd.Help)[0]) + }) + }) + }) }}, + mdb.MODIFY: {Name: "modify", Help: "编辑", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy(mdb.MODIFY, RIVER, "", mdb.HASH, kit.MDB_HASH, m.Option(kit.MDB_HASH), arg) + }}, + mdb.REMOVE: {Name: "remove hash", Help: "删除", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy(mdb.DELETE, RIVER, "", mdb.HASH, kit.MDB_HASH, m.Option(kit.MDB_HASH)) + }}, + mdb.EXPORT: {Name: "export", Help: "导出", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy(mdb.EXPORT, RIVER, "", mdb.HASH) + }}, + mdb.IMPORT: {Name: "import", Help: "导入", Hand: func(m *ice.Message, arg ...string) { + m.Cmdy(mdb.IMPORT, RIVER, "", mdb.HASH) + }}, + }, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { + m.Cmdy(mdb.SELECT, RIVER, "", mdb.HASH, kit.MDB_HASH, arg) + m.PushAction("删除") + }}, + + "/river": {Name: "/river", Help: "小河流", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { + if len(arg) == 0 { + _river_list(m) + return + } + if len(arg) == 1 { + m.Option(ice.MSG_RIVER, arg[0]) + m.Cmdy(TOOL, arg[1:]) + return + } + if !m.Warn(!m.Right(RIVER, arg), ice.ErrNotAuth) { + m.Cmdy(RIVER, arg) + } + }}, }, }, nil) } diff --git a/core/chat/trash.go b/core/chat/trash.go new file mode 100644 index 00000000..f8aa0043 --- /dev/null +++ b/core/chat/trash.go @@ -0,0 +1,89 @@ +package chat + +import ( + ice "github.com/shylinux/icebergs" + "github.com/shylinux/icebergs/base/aaa" + "github.com/shylinux/icebergs/base/ctx" + "github.com/shylinux/icebergs/base/web" + kit "github.com/shylinux/toolkits" +) + +func init() { + Index.Merge(&ice.Context{ + Commands: map[string]*ice.Command{ + "/ocean": {Name: "/ocean", Help: "大海洋", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { + if len(arg) == 0 { + // 用户列表 + m.Richs(aaa.USER, nil, "*", func(key string, value map[string]interface{}) { + m.Push(key, value, []string{"username", "usernode"}) + }) + return + } + + switch arg[0] { + case "spawn": + // 创建群组 + river := m.Rich(RIVER, nil, kit.Dict( + kit.MDB_META, kit.Dict(kit.MDB_NAME, arg[1]), + "user", kit.Data(kit.MDB_SHORT, "username"), + "tool", kit.Data(), + )) + m.Log(ice.LOG_CREATE, "river: %v name: %v", river, arg[1]) + // 添加用户 + m.Cmd("/river", river, "add", m.Option(ice.MSG_USERNAME), arg[2:]) + m.Echo(river) + } + }}, + "/steam": {Name: "/steam", Help: "大气层", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { + if m.Warn(m.Option(ice.MSG_RIVER) == "", "not join") { + m.Render("status", 402, "not join") + return + } + + if len(arg) < 2 { + if list := []string{}; m.Option("pod") != "" { + // 远程空间 + m.Cmdy(web.SPACE, m.Option("pod"), "web.chat./steam").Table(func(index int, value map[string]string, head []string) { + list = append(list, kit.Keys(m.Option("pod"), value["name"])) + }) + m.Append("name", list) + } else { + // 本地空间 + m.Richs(web.SPACE, nil, "*", func(key string, value map[string]interface{}) { + switch value[kit.MDB_TYPE] { + case web.SERVER, web.WORKER: + m.Push(key, value, []string{"type", "name", "user"}) + } + }) + } + return + } + + if m.Warn(!m.Right(cmd, arg[1])) { + m.Render("status", 403, "not auth") + return + } + + switch arg[1] { + case "spawn": + // 创建应用 + storm := m.Rich(RIVER, kit.Keys(kit.MDB_HASH, arg[0], "tool"), kit.Dict( + kit.MDB_META, kit.Dict(kit.MDB_NAME, arg[2]), + )) + m.Log(ice.LOG_CREATE, "storm: %s name: %v", storm, arg[2]) + // 添加命令 + m.Cmd("/storm", arg[0], storm, "add", arg[3:]) + m.Echo(storm) + + case "append": + // 追加命令 + m.Cmd("/storm", arg[0], arg[2], "add", arg[3:]) + + default: + // 命令列表 + m.Cmdy(web.SPACE, arg[2], ctx.COMMAND) + } + }}, + }, + }, nil) +} diff --git a/core/code/publish.go b/core/code/publish.go index 41d7a1a2..5799c3c9 100644 --- a/core/code/publish.go +++ b/core/code/publish.go @@ -94,6 +94,11 @@ export ctx_dev={{.Option "httphost"}} ctx_temp=$(mktemp); curl -sL $ctx_dev >$ct # 开发环境 mkdir contexts; cd contexts export ctx_dev={{.Option "httphost"}} ctx_temp=$(mktemp); curl -sL $ctx_dev >$ctx_temp; source $ctx_temp dev +`, + "tool", ` +# 生产环境 +mkdir contexts; cd contexts +export ctx_dev={{.Option "httphost"}} ctx_share={{.Option "share"}} ctx_temp=$(mktemp); curl -sL $ctx_dev >$ctx_temp; source $ctx_temp dev `, ) diff --git a/type.go b/type.go index a921592f..5400edd6 100644 --- a/type.go +++ b/type.go @@ -587,7 +587,7 @@ func (m *Message) Search(key interface{}, cb interface{}) *Message { break } - for _, p = range []*Context{p, m.target, m.source} { + for _, p := range []*Context{m.target, p, m.source} { for c := p; c != nil; c = c.context { if cmd, ok := c.Commands[key]; ok { cb(c, p, key, cmd) @@ -596,7 +596,7 @@ func (m *Message) Search(key interface{}, cb interface{}) *Message { } } case func(p *Context, s *Context, key string, conf *Config): - for _, p = range []*Context{p, m.target, m.source} { + for _, p := range []*Context{m.target, p, m.source} { for c := p; c != nil; c = c.context { if cmd, ok := c.Configs[key]; ok { cb(c.context, c, key, cmd)