main.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. package main
  2. import (
  3. "encoding/base64"
  4. "fmt"
  5. "net"
  6. "os"
  7. "os/user"
  8. "path/filepath"
  9. "strings"
  10. "github.com/cryptix/go/logging"
  11. "github.com/cryptix/secretstream"
  12. "github.com/cryptix/secretstream/secrethandshake"
  13. "github.com/pkg/errors"
  14. "github.com/shurcooL/go-goon"
  15. "gopkg.in/urfave/cli.v2"
  16. "scuttlebot.io/go/muxrpc"
  17. "scuttlebot.io/go/muxrpc/codec"
  18. )
  19. var (
  20. sbotAppKey []byte
  21. defaultKeyFile string
  22. client *muxrpc.Client
  23. log logging.Interface
  24. check = logging.CheckFatal
  25. )
  26. func init() {
  27. var err error
  28. sbotAppKey, err = base64.StdEncoding.DecodeString("1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=")
  29. check(err)
  30. u, err := user.Current()
  31. check(err)
  32. defaultKeyFile = filepath.Join(u.HomeDir, ".ssb", "secret")
  33. }
  34. var Revision = "unset"
  35. func main() {
  36. logging.SetupLogging(nil)
  37. log = logging.Logger("gophbot")
  38. app := cli.App{
  39. Name: "ssb-gophbot",
  40. Usage: "what can I say? sbot in Go",
  41. Version: "alpha2",
  42. }
  43. cli.VersionPrinter = func(c *cli.Context) {
  44. // go install -ldflags="-X main.Revision=$(git rev-parse HEAD)"
  45. fmt.Printf("%s ( rev: %s )\n", c.App.Version, Revision)
  46. }
  47. app.Flags = []cli.Flag{
  48. &cli.StringFlag{Name: "addr", Value: "localhost:8008", Usage: "tcp address of the sbot to connect to (or listen on)"},
  49. &cli.StringFlag{Name: "remoteKey", Value: "", Usage: "the remote pubkey you are connecting to (by default the local key)"},
  50. &cli.StringFlag{Name: "key,k", Value: defaultKeyFile},
  51. &cli.BoolFlag{Name: "verbose,vv", Usage: "print muxrpc packets"},
  52. }
  53. app.Before = initClient
  54. app.Commands = []*cli.Command{
  55. {
  56. Name: "log",
  57. Action: logStreamCmd,
  58. },
  59. {
  60. Name: "hist",
  61. Action: historyStreamCmd,
  62. Flags: []cli.Flag{
  63. &cli.IntFlag{Name: "limit", Value: -1},
  64. &cli.IntFlag{Name: "seq", Value: 0},
  65. &cli.BoolFlag{Name: "reverse"},
  66. &cli.BoolFlag{Name: "live"},
  67. &cli.BoolFlag{Name: "keys", Value: true},
  68. &cli.BoolFlag{Name: "values", Value: true},
  69. },
  70. },
  71. {
  72. Name: "qry",
  73. Action: query,
  74. },
  75. {
  76. Name: "call",
  77. Action: callCmd,
  78. Usage: "make an dump* async call",
  79. UsageText: `SUPPORTS:
  80. * whoami
  81. * latestSequence
  82. * getLatest
  83. * get
  84. * blobs.(has|want|rm|wants)
  85. * gossip.(peers|add|connect)
  86. see https://scuttlebot.io/apis/scuttlebot/ssb.html#createlogstream-source for more
  87. CAVEAT: only one argument...
  88. `,
  89. },
  90. {
  91. Name: "private",
  92. Subcommands: []*cli.Command{
  93. {
  94. Name: "publish",
  95. Usage: "p",
  96. Action: privatePublishCmd,
  97. Flags: []cli.Flag{
  98. &cli.StringFlag{Name: "type", Value: "post"},
  99. &cli.StringFlag{Name: "text", Value: "Hello, World!"},
  100. &cli.StringFlag{Name: "root", Usage: "the ID of the first message of the thread"},
  101. &cli.StringFlag{Name: "branch", Usage: "the post ID that is beeing replied to"},
  102. &cli.StringFlag{Name: "channel"},
  103. &cli.StringSliceFlag{Name: "recps", Usage: "posting to these IDs privatly"},
  104. },
  105. },
  106. {
  107. Name: "unbox",
  108. Usage: "u",
  109. Action: privateUnboxCmd,
  110. },
  111. },
  112. },
  113. {
  114. Name: "publish",
  115. Usage: "p",
  116. Action: publishCmd,
  117. Flags: []cli.Flag{
  118. &cli.StringFlag{Name: "type", Value: "post"},
  119. &cli.StringFlag{Name: "text", Value: "Hello, World!"},
  120. &cli.StringFlag{Name: "root", Value: "", Usage: "the ID of the first message of the thread"},
  121. &cli.StringFlag{Name: "branch", Value: "", Usage: "the post ID that is beeing replied to"},
  122. },
  123. },
  124. }
  125. check(app.Run(os.Args))
  126. }
  127. func initClient(ctx *cli.Context) error {
  128. localKey, err := secrethandshake.LoadSSBKeyPair(ctx.String("key"))
  129. if err != nil {
  130. return err
  131. }
  132. var conn net.Conn
  133. if ctx.Bool("listen") { // TODO: detect server command..
  134. srv, err := secretstream.NewServer(*localKey, sbotAppKey)
  135. if err != nil {
  136. return err
  137. }
  138. l, err := srv.Listen("tcp", ctx.String("addr"))
  139. if err != nil {
  140. return err
  141. }
  142. conn, err = l.Accept()
  143. if err != nil {
  144. return err
  145. }
  146. } else {
  147. c, err := secretstream.NewClient(*localKey, sbotAppKey)
  148. if err != nil {
  149. return err
  150. }
  151. var remotPubKey = localKey.Public
  152. if rk := ctx.String("remoteKey"); rk != "" {
  153. rk = strings.TrimSuffix(rk, ".ed25519")
  154. rk = strings.TrimPrefix(rk, "@")
  155. rpk, err := base64.StdEncoding.DecodeString(rk)
  156. if err != nil {
  157. return errors.Wrapf(err, "ssb-gophbot: base64 decode of --remoteKey failed")
  158. }
  159. copy(remotPubKey[:], rpk)
  160. }
  161. d, err := c.NewDialer(remotPubKey)
  162. if err != nil {
  163. return err
  164. }
  165. conn, err = d("tcp", ctx.String("addr"))
  166. if err != nil {
  167. return err
  168. }
  169. }
  170. if ctx.Bool("verbose") {
  171. client = muxrpc.NewClient(log, codec.Wrap(log, conn))
  172. } else {
  173. client = muxrpc.NewClient(log, conn)
  174. }
  175. go func() {
  176. client.Handle()
  177. log.Log("warning", "muxrpc disconnected")
  178. }()
  179. return nil
  180. }
  181. func privatePublishCmd(ctx *cli.Context) error {
  182. content := map[string]interface{}{
  183. "text": ctx.String("text"),
  184. "type": ctx.String("type"),
  185. }
  186. if c := ctx.String("channel"); c != "" {
  187. content["channel"] = c
  188. }
  189. if r := ctx.String("root"); r != "" {
  190. content["root"] = r
  191. if b := ctx.String("branch"); b != "" {
  192. content["branch"] = b
  193. } else {
  194. content["branch"] = r
  195. }
  196. }
  197. recps := ctx.StringSlice("recps")
  198. if len(recps) == 0 {
  199. return errors.Errorf("private.publish: 0 recps.. that would be quite the lonely message..")
  200. }
  201. var reply map[string]interface{}
  202. err := client.Call("private.publish", &reply, content, recps)
  203. if err != nil {
  204. return errors.Wrapf(err, "publish call failed.")
  205. }
  206. log.Log("event", "private published")
  207. goon.Dump(reply)
  208. return client.Close()
  209. }
  210. func privateUnboxCmd(ctx *cli.Context) error {
  211. id := ctx.Args().Get(0)
  212. if id == "" {
  213. return errors.New("get: id can't be empty")
  214. }
  215. var getReply map[string]interface{}
  216. if err := client.Call("get", id, &getReply); err != nil {
  217. return errors.Wrapf(err, "get call failed.")
  218. }
  219. log.Log("event", "get reply")
  220. goon.Dump(getReply)
  221. var reply map[string]interface{}
  222. if err := client.Call("private.unbox", getReply["content"], &reply); err != nil {
  223. return errors.Wrapf(err, "get call failed.")
  224. }
  225. log.Log("event", "unboxed")
  226. goon.Dump(reply)
  227. return client.Close()
  228. }
  229. func publishCmd(ctx *cli.Context) error {
  230. arg := map[string]interface{}{
  231. "text": ctx.String("text"),
  232. "type": ctx.String("type"),
  233. }
  234. if r := ctx.String("root"); r != "" {
  235. arg["root"] = r
  236. if b := ctx.String("branch"); b != "" {
  237. arg["branch"] = b
  238. } else {
  239. arg["branch"] = r
  240. }
  241. }
  242. var reply map[string]interface{}
  243. err := client.Call("publish", arg, &reply)
  244. if err != nil {
  245. return errors.Wrapf(err, "publish call failed.")
  246. }
  247. log.Log("event", "published")
  248. goon.Dump(reply)
  249. return client.Close()
  250. }
  251. func historyStreamCmd(ctx *cli.Context) error {
  252. id := ctx.Args().Get(0)
  253. if id == "" {
  254. return errors.New("createHist: id can't be empty")
  255. }
  256. arg := map[string]interface{}{
  257. "id": id,
  258. "limit": ctx.Int("limit"),
  259. "seq": ctx.Int("seq"),
  260. "live": ctx.Bool("live"),
  261. "reverse": ctx.Bool("reverse"),
  262. "keys": ctx.Bool("keys"),
  263. "values": ctx.Bool("values"),
  264. }
  265. reply := make(chan map[string]interface{})
  266. go func() {
  267. for r := range reply {
  268. goon.Dump(r)
  269. }
  270. }()
  271. if err := client.Source("createHistoryStream", reply, arg); err != nil {
  272. return errors.Wrap(err, "source stream call failed")
  273. }
  274. return client.Close()
  275. }
  276. func logStreamCmd(ctx *cli.Context) error {
  277. reply := make(chan map[string]interface{})
  278. go func() {
  279. for r := range reply {
  280. goon.Dump(r)
  281. }
  282. }()
  283. if err := client.Source("createLogStream", reply); err != nil {
  284. return errors.Wrap(err, "source stream call failed")
  285. }
  286. return client.Close()
  287. }
  288. func callCmd(ctx *cli.Context) error {
  289. cmd := ctx.Args().Get(0)
  290. if cmd == "" {
  291. return errors.New("call: cmd can't be empty")
  292. }
  293. var reply interface{}
  294. if err := client.Call(cmd, &reply, ctx.Args().Slice()); err != nil {
  295. return errors.Wrapf(err, "%s: call failed.", cmd)
  296. }
  297. log.Log("event", "call reply")
  298. goon.Dump(reply)
  299. return client.Close()
  300. }
  301. func query(ctx *cli.Context) error {
  302. reply := make(chan map[string]interface{})
  303. go func() {
  304. for r := range reply {
  305. goon.Dump(r)
  306. }
  307. }()
  308. if err := client.Source("query.read", reply, ctx.Args().Get(0)); err != nil {
  309. return errors.Wrap(err, "source stream call failed")
  310. }
  311. return client.Close()
  312. }