Add initial Keybase Chat support (#877)
* initial work on native keybase bridging * Hopefully make a functional keybase bridge * add keybase to bridgemap * send to right channel, try to figure out received msgs * add account and userid * i am a Dam Fool * Fix formatting for messages, handle /me * update vendors, ran golint and goimports * move handlers to handlers.go, clean up unused config options * add sample config, fix inconsistent remote nick handling * Update readme with keybase links * Resolve fixmie errors * Error -> Errorf * fix linting errors in go.mod and go.sum * explicitly join channels, ignore messages from non-specified channels * check that team names match before bridging messagepull/878/head
parent
79a006c8de
commit
921f2dfcdf
@ -0,0 +1,59 @@
|
||||
package bkeybase
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/keybase/go-keybase-chat-bot/kbchat"
|
||||
)
|
||||
|
||||
func (b *Bkeybase) handleKeybase() {
|
||||
sub, err := b.kbc.ListenForNewTextMessages()
|
||||
if err != nil {
|
||||
b.Log.Errorf("Error listening: %s", err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
msg, err := sub.Read()
|
||||
if err != nil {
|
||||
b.Log.Errorf("failed to read message: %s", err.Error())
|
||||
}
|
||||
|
||||
if msg.Message.Content.Type != "text" {
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.Message.Sender.Username == b.kbc.GetUsername() {
|
||||
continue
|
||||
}
|
||||
|
||||
b.handleMessage(msg.Message)
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *Bkeybase) handleMessage(msg kbchat.Message) {
|
||||
b.Log.Debugf("== Receiving event: %#v", msg)
|
||||
if msg.Channel.TopicName != b.channel || msg.Channel.Name != b.team {
|
||||
return
|
||||
}
|
||||
|
||||
if msg.Sender.Username != b.kbc.GetUsername() {
|
||||
|
||||
// TODO download avatar
|
||||
|
||||
// Create our message
|
||||
rmsg := config.Message{Username: msg.Sender.Username, Text: msg.Content.Text.Body, UserID: msg.Sender.Uid, Channel: msg.Channel.TopicName, ID: strconv.Itoa(msg.MsgID), Account: b.Account}
|
||||
|
||||
// Text must be a string
|
||||
if msg.Content.Type != "text" {
|
||||
b.Log.Errorf("message is not text")
|
||||
return
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", msg.Sender.Username, msg.Channel.Name)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package bkeybase
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/keybase/go-keybase-chat-bot/kbchat"
|
||||
)
|
||||
|
||||
// Bkeybase bridge structure
|
||||
type Bkeybase struct {
|
||||
kbc *kbchat.API
|
||||
user string
|
||||
channel string
|
||||
team string
|
||||
*bridge.Config
|
||||
}
|
||||
|
||||
// New initializes Bkeybase object and sets team
|
||||
func New(cfg *bridge.Config) bridge.Bridger {
|
||||
b := &Bkeybase{Config: cfg}
|
||||
b.team = b.Config.GetString("Team")
|
||||
return b
|
||||
}
|
||||
|
||||
// Connect starts keybase API and listener loop
|
||||
func (b *Bkeybase) Connect() error {
|
||||
var err error
|
||||
b.Log.Infof("Connecting %s", b.GetString("Team"))
|
||||
|
||||
// use default keybase location (`keybase`)
|
||||
b.kbc, err = kbchat.Start(kbchat.RunOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.user = b.kbc.GetUsername()
|
||||
b.Log.Info("Connection succeeded")
|
||||
go b.handleKeybase()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect doesn't do anything for now
|
||||
func (b *Bkeybase) Disconnect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// JoinChannel sets channel name in struct
|
||||
func (b *Bkeybase) JoinChannel(channel config.ChannelInfo) error {
|
||||
if _, err := b.kbc.JoinChannel(b.team, channel.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
b.channel = channel.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send receives bridge messages and sends them to Keybase chat room
|
||||
func (b *Bkeybase) Send(msg config.Message) (string, error) {
|
||||
b.Log.Debugf("=> Receiving %#v", msg)
|
||||
|
||||
// Handle /me events
|
||||
if msg.Event == config.EventUserAction {
|
||||
msg.Text = "_" + msg.Text + "_"
|
||||
}
|
||||
|
||||
// Delete message if we have an ID
|
||||
// Delete message not supported by keybase go library yet
|
||||
|
||||
// Upload a file if it exists
|
||||
// kbchat lib does not support attachments yet
|
||||
|
||||
// Edit message if we have an ID
|
||||
// kbchat lib does not support message editing yet
|
||||
|
||||
// Send regular message
|
||||
resp, err := b.kbc.SendMessageByTeamName(b.team, msg.Username+msg.Text, &b.channel)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strconv.Itoa(resp.Result.MsgID), err
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2017, Keybase
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of keybase nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,693 @@
|
||||
package kbchat
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// API is the main object used for communicating with the Keybase JSON API
|
||||
type API struct {
|
||||
sync.Mutex
|
||||
apiInput io.Writer
|
||||
apiOutput *bufio.Reader
|
||||
apiCmd *exec.Cmd
|
||||
username string
|
||||
runOpts RunOptions
|
||||
}
|
||||
|
||||
func getUsername(runOpts RunOptions) (username string, err error) {
|
||||
p := runOpts.Command("status")
|
||||
output, err := p.StdoutPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = p.Start(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
doneCh := make(chan error)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(output)
|
||||
if !scanner.Scan() {
|
||||
doneCh <- errors.New("unable to find Keybase username")
|
||||
return
|
||||
}
|
||||
toks := strings.Fields(scanner.Text())
|
||||
if len(toks) != 2 {
|
||||
doneCh <- errors.New("invalid Keybase username output")
|
||||
return
|
||||
}
|
||||
username = toks[1]
|
||||
doneCh <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case err = <-doneCh:
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
return "", errors.New("unable to run Keybase command")
|
||||
}
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
type OneshotOptions struct {
|
||||
Username string
|
||||
PaperKey string
|
||||
}
|
||||
|
||||
type RunOptions struct {
|
||||
KeybaseLocation string
|
||||
HomeDir string
|
||||
Oneshot *OneshotOptions
|
||||
StartService bool
|
||||
}
|
||||
|
||||
func (r RunOptions) Location() string {
|
||||
if r.KeybaseLocation == "" {
|
||||
return "keybase"
|
||||
}
|
||||
return r.KeybaseLocation
|
||||
}
|
||||
|
||||
func (r RunOptions) Command(args ...string) *exec.Cmd {
|
||||
var cmd []string
|
||||
if r.HomeDir != "" {
|
||||
cmd = append(cmd, "--home", r.HomeDir)
|
||||
}
|
||||
cmd = append(cmd, args...)
|
||||
return exec.Command(r.Location(), cmd...)
|
||||
}
|
||||
|
||||
// Start fires up the Keybase JSON API in stdin/stdout mode
|
||||
func Start(runOpts RunOptions) (*API, error) {
|
||||
api := &API{
|
||||
runOpts: runOpts,
|
||||
}
|
||||
if err := api.startPipes(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func (a *API) auth() (string, error) {
|
||||
username, err := getUsername(a.runOpts)
|
||||
if err == nil {
|
||||
return username, nil
|
||||
}
|
||||
if a.runOpts.Oneshot == nil {
|
||||
return "", err
|
||||
}
|
||||
username = ""
|
||||
// If a paper key is specified, then login with oneshot mode (logout first)
|
||||
if a.runOpts.Oneshot != nil {
|
||||
if username == a.runOpts.Oneshot.Username {
|
||||
// just get out if we are on the desired user already
|
||||
return username, nil
|
||||
}
|
||||
if err := a.runOpts.Command("logout", "-f").Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := a.runOpts.Command("oneshot", "--username", a.runOpts.Oneshot.Username, "--paperkey",
|
||||
a.runOpts.Oneshot.PaperKey).Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
username = a.runOpts.Oneshot.Username
|
||||
return username, nil
|
||||
}
|
||||
return "", errors.New("unable to auth")
|
||||
}
|
||||
|
||||
func (a *API) startPipes() (err error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
if a.apiCmd != nil {
|
||||
a.apiCmd.Process.Kill()
|
||||
}
|
||||
a.apiCmd = nil
|
||||
|
||||
if a.runOpts.StartService {
|
||||
a.runOpts.Command("service").Start()
|
||||
}
|
||||
|
||||
if a.username, err = a.auth(); err != nil {
|
||||
return err
|
||||
}
|
||||
a.apiCmd = a.runOpts.Command("chat", "api")
|
||||
if a.apiInput, err = a.apiCmd.StdinPipe(); err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := a.apiCmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.apiCmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
a.apiOutput = bufio.NewReader(output)
|
||||
return nil
|
||||
}
|
||||
|
||||
var errAPIDisconnected = errors.New("chat API disconnected")
|
||||
|
||||
func (a *API) getAPIPipesLocked() (io.Writer, *bufio.Reader, error) {
|
||||
// this should only be called inside a lock
|
||||
if a.apiCmd == nil {
|
||||
return nil, nil, errAPIDisconnected
|
||||
}
|
||||
return a.apiInput, a.apiOutput, nil
|
||||
}
|
||||
|
||||
// GetConversations reads all conversations from the current user's inbox.
|
||||
func (a *API) GetConversations(unreadOnly bool) ([]Conversation, error) {
|
||||
apiInput := fmt.Sprintf(`{"method":"list", "params": { "options": { "unread_only": %v}}}`, unreadOnly)
|
||||
output, err := a.doFetch(apiInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var inbox Inbox
|
||||
if err := json.Unmarshal(output, &inbox); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inbox.Result.Convs, nil
|
||||
}
|
||||
|
||||
// GetTextMessages fetches all text messages from a given channel. Optionally can filter
|
||||
// ont unread status.
|
||||
func (a *API) GetTextMessages(channel Channel, unreadOnly bool) ([]Message, error) {
|
||||
channelBytes, err := json.Marshal(channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiInput := fmt.Sprintf(`{"method": "read", "params": {"options": {"channel": %s}}}`, string(channelBytes))
|
||||
output, err := a.doFetch(apiInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var thread Thread
|
||||
|
||||
if err := json.Unmarshal(output, &thread); err != nil {
|
||||
return nil, fmt.Errorf("unable to decode thread: %s", err.Error())
|
||||
}
|
||||
|
||||
var res []Message
|
||||
for _, msg := range thread.Result.Messages {
|
||||
if msg.Msg.Content.Type == "text" {
|
||||
res = append(res, msg.Msg)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type sendMessageBody struct {
|
||||
Body string
|
||||
}
|
||||
|
||||
type sendMessageOptions struct {
|
||||
Channel Channel `json:"channel,omitempty"`
|
||||
ConversationID string `json:"conversation_id,omitempty"`
|
||||
Message sendMessageBody `json:",omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
MsgID int `json:"message_id,omitempty"`
|
||||
}
|
||||
|
||||
type sendMessageParams struct {
|
||||
Options sendMessageOptions
|
||||
}
|
||||
|
||||
type sendMessageArg struct {
|
||||
Method string
|
||||
Params sendMessageParams
|
||||
}
|
||||
|
||||
func (a *API) doSend(arg interface{}) (response SendResponse, err error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
bArg, err := json.Marshal(arg)
|
||||
if err != nil {
|
||||
return SendResponse{}, err
|
||||
}
|
||||
input, output, err := a.getAPIPipesLocked()
|
||||
if err != nil {
|
||||
return SendResponse{}, err
|
||||
}
|
||||
if _, err := io.WriteString(input, string(bArg)); err != nil {
|
||||
return SendResponse{}, err
|
||||
}
|
||||
responseRaw, err := output.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return SendResponse{}, err
|
||||
}
|
||||
if err := json.Unmarshal(responseRaw, &response); err != nil {
|
||||
return SendResponse{}, fmt.Errorf("failed to decode API response: %s", err)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *API) doFetch(apiInput string) ([]byte, error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
input, output, err := a.getAPIPipesLocked()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.WriteString(input, apiInput); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byteOutput, err := output.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return byteOutput, nil
|
||||
}
|
||||
|
||||
func (a *API) SendMessage(channel Channel, body string) (SendResponse, error) {
|
||||
arg := sendMessageArg{
|
||||
Method: "send",
|
||||
Params: sendMessageParams{
|
||||
Options: sendMessageOptions{
|
||||
Channel: channel,
|
||||
Message: sendMessageBody{
|
||||
Body: body,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return a.doSend(arg)
|
||||
}
|
||||
|
||||
func (a *API) SendMessageByConvID(convID string, body string) (SendResponse, error) {
|
||||
arg := sendMessageArg{
|
||||
Method: "send",
|
||||
Params: sendMessageParams{
|
||||
Options: sendMessageOptions{
|
||||
ConversationID: convID,
|
||||
Message: sendMessageBody{
|
||||
Body: body,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return a.doSend(arg)
|
||||
}
|
||||
|
||||
// SendMessageByTlfName sends a message on the given TLF name
|
||||
func (a *API) SendMessageByTlfName(tlfName string, body string) (SendResponse, error) {
|
||||
arg := sendMessageArg{
|
||||
Method: "send",
|
||||
Params: sendMessageParams{
|
||||
Options: sendMessageOptions{
|
||||
Channel: Channel{
|
||||
Name: tlfName,
|
||||
},
|
||||
Message: sendMessageBody{
|
||||
Body: body,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return a.doSend(arg)
|
||||
}
|
||||
|
||||
func (a *API) SendMessageByTeamName(teamName string, body string, inChannel *string) (SendResponse, error) {
|
||||
channel := "general"
|
||||
if inChannel != nil {
|
||||
channel = *inChannel
|
||||
}
|
||||
arg := sendMessageArg{
|
||||
Method: "send",
|
||||
Params: sendMessageParams{
|
||||
Options: sendMessageOptions{
|
||||
Channel: Channel{
|
||||
MembersType: "team",
|
||||
Name: teamName,
|
||||
TopicName: channel,
|
||||
},
|
||||
Message: sendMessageBody{
|
||||
Body: body,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return a.doSend(arg)
|
||||
}
|
||||
|
||||
func (a *API) SendAttachmentByTeam(teamName string, filename string, title string, inChannel *string) (SendResponse, error) {
|
||||
channel := "general"
|
||||
if inChannel != nil {
|
||||
channel = *inChannel
|
||||
}
|
||||
arg := sendMessageArg{
|
||||
Method: "attach",
|
||||
Params: sendMessageParams{
|
||||
Options: sendMessageOptions{
|
||||
Channel: Channel{
|
||||
MembersType: "team",
|
||||
Name: teamName,
|
||||
TopicName: channel,
|
||||
},
|
||||
Filename: filename,
|
||||
Title: title,
|
||||
},
|
||||
},
|
||||
}
|
||||
return a.doSend(arg)
|
||||
}
|
||||
|
||||
type reactionOptions struct {
|
||||
ConversationID string `json:"conversation_id"`
|
||||
Message sendMessageBody
|
||||
MsgID int `json:"message_id"`
|
||||
Channel Channel `json:"channel"`
|
||||
}
|
||||
|
||||
type reactionParams struct {
|
||||
Options reactionOptions
|
||||
}
|
||||
|
||||
type reactionArg struct {
|
||||
Method string
|
||||
Params reactionParams
|
||||
}
|
||||
|
||||
func newReactionArg(options reactionOptions) reactionArg {
|
||||
return reactionArg{
|
||||
Method: "reaction",
|
||||
Params: reactionParams{Options: options},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) ReactByChannel(channel Channel, msgID int, reaction string) (SendResponse, error) {
|
||||
arg := newReactionArg(reactionOptions{
|
||||
Message: sendMessageBody{Body: reaction},
|
||||
MsgID: msgID,
|
||||
Channel: channel,
|
||||
})
|
||||
return a.doSend(arg)
|
||||
}
|
||||
|
||||
func (a *API) ReactByConvID(convID string, msgID int, reaction string) (SendResponse, error) {
|
||||
arg := newReactionArg(reactionOptions{
|
||||
Message: sendMessageBody{Body: reaction},
|
||||
MsgID: msgID,
|
||||
ConversationID: convID,
|
||||
})
|
||||
return a.doSend(arg)
|
||||
}
|
||||
|
||||
type advertiseParams struct {
|
||||
Options Advertisement
|
||||
}
|
||||
|
||||
type advertiseMsgArg struct {
|
||||
Method string
|
||||
Params advertiseParams
|
||||
}
|
||||
|
||||
func newAdvertiseMsgArg(ad Advertisement) advertiseMsgArg {
|
||||
return advertiseMsgArg{
|
||||
Method: "advertisecommands",
|
||||
Params: advertiseParams{
|
||||
Options: ad,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) AdvertiseCommands(ad Advertisement) (SendResponse, error) {
|
||||
return a.doSend(newAdvertiseMsgArg(ad))
|
||||
}
|
||||
|
||||
func (a *API) Username() string {
|
||||
return a.username
|
||||
}
|
||||
|
||||
// SubscriptionMessage contains a message and conversation object
|
||||
type SubscriptionMessage struct {
|
||||
Message Message
|
||||
Conversation Conversation
|
||||
}
|
||||
|
||||
type SubscriptionWalletEvent struct {
|
||||
Payment Payment
|
||||
}
|
||||
|
||||
// NewSubscription has methods to control the background message fetcher loop
|
||||
type NewSubscription struct {
|
||||
newMsgsCh <-chan SubscriptionMessage
|
||||
newWalletCh <-chan SubscriptionWalletEvent
|
||||
errorCh <-chan error
|
||||
shutdownCh chan struct{}
|
||||
}
|
||||
|
||||
// Read blocks until a new message arrives
|
||||
func (m NewSubscription) Read() (SubscriptionMessage, error) {
|
||||
select {
|
||||
case msg := <-m.newMsgsCh:
|
||||
return msg, nil
|
||||
case err := <-m.errorCh:
|
||||
return SubscriptionMessage{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Read blocks until a new message arrives
|
||||
func (m NewSubscription) ReadWallet() (SubscriptionWalletEvent, error) {
|
||||
select {
|
||||
case msg := <-m.newWalletCh:
|
||||
return msg, nil
|
||||
case err := <-m.errorCh:
|
||||
return SubscriptionWalletEvent{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown terminates the background process
|
||||
func (m NewSubscription) Shutdown() {
|
||||
m.shutdownCh <- struct{}{}
|
||||
}
|
||||
|
||||
type ListenOptions struct {
|
||||
Wallet bool
|
||||
}
|
||||
|
||||
// ListenForNewTextMessages proxies to Listen without wallet events
|
||||
func (a *API) ListenForNewTextMessages() (NewSubscription, error) {
|
||||
opts := ListenOptions{Wallet: false}
|
||||
return a.Listen(opts)
|
||||
}
|
||||
|
||||
// Listen fires of a background loop and puts chat messages and wallet
|
||||
// events into channels
|
||||
func (a *API) Listen(opts ListenOptions) (NewSubscription, error) {
|
||||
newMsgCh := make(chan SubscriptionMessage, 100)
|
||||
newWalletCh := make(chan SubscriptionWalletEvent, 100)
|
||||
errorCh := make(chan error, 100)
|
||||
shutdownCh := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
|
||||
sub := NewSubscription{
|
||||
newMsgsCh: newMsgCh,
|
||||
newWalletCh: newWalletCh,
|
||||
shutdownCh: shutdownCh,
|
||||
errorCh: errorCh,
|
||||
}
|
||||
pause := 2 * time.Second
|
||||
readScanner := func(boutput *bufio.Scanner) {
|
||||
for {
|
||||
boutput.Scan()
|
||||
t := boutput.Text()
|
||||
var typeHolder TypeHolder
|
||||
if err := json.Unmarshal([]byte(t), &typeHolder); err != nil {
|
||||
errorCh <- err
|
||||
break
|
||||
}
|
||||
switch typeHolder.Type {
|
||||
case "chat":
|
||||
var holder MessageHolder
|
||||
if err := json.Unmarshal([]byte(t), &holder); err != nil {
|
||||
errorCh <- err
|
||||
break
|
||||
}
|
||||
subscriptionMessage := SubscriptionMessage{
|
||||
Message: holder.Msg,
|
||||
Conversation: Conversation{
|
||||
ID: holder.Msg.ConversationID,
|
||||
Channel: holder.Msg.Channel,
|
||||
},
|
||||
}
|
||||
newMsgCh <- subscriptionMessage
|
||||
case "wallet":
|
||||
var holder PaymentHolder
|
||||
if err := json.Unmarshal([]byte(t), &holder); err != nil {
|
||||
errorCh <- err
|
||||
break
|
||||
}
|
||||
subscriptionPayment := SubscriptionWalletEvent{
|
||||
Payment: holder.Payment,
|
||||
}
|
||||
newWalletCh <- subscriptionPayment
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
done <- struct{}{}
|
||||
}
|
||||
|
||||
attempts := 0
|
||||
maxAttempts := 1800
|
||||
go func() {
|
||||
for {
|
||||
if attempts >= maxAttempts {
|
||||
panic("Listen: failed to auth, giving up")
|
||||
}
|
||||
attempts++
|
||||
if _, err := a.auth(); err != nil {
|
||||
log.Printf("Listen: failed to auth: %s", err)
|
||||
time.Sleep(pause)
|
||||
continue
|
||||
}
|
||||
cmdElements := []string{"chat", "api-listen"}
|
||||
if opts.Wallet {
|
||||
cmdElements = append(cmdElements, "--wallet")
|
||||
}
|
||||
p := a.runOpts.Command(cmdElements...)
|
||||
output, err := p.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("Listen: failed to listen: %s", err)
|
||||
time.Sleep(pause)
|
||||
continue
|
||||
}
|
||||
boutput := bufio.NewScanner(output)
|
||||
if err := p.Start(); err != nil {
|
||||
log.Printf("Listen: failed to make listen scanner: %s", err)
|
||||
time.Sleep(pause)
|
||||
continue
|
||||
}
|
||||
attempts = 0
|
||||
go readScanner(boutput)
|
||||
<-done
|
||||
p.Wait()
|
||||
time.Sleep(pause)
|
||||
}
|
||||
}()
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (a *API) GetUsername() string {
|
||||
return a.username
|
||||
}
|
||||
|
||||
func (a *API) ListChannels(teamName string) ([]string, error) {
|
||||
apiInput := fmt.Sprintf(`{"method": "listconvsonname", "params": {"options": {"topic_type": "CHAT", "members_type": "team", "name": "%s"}}}`, teamName)
|
||||
output, err := a.doFetch(apiInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channelsList ChannelsList
|
||||
if err := json.Unmarshal(output, &channelsList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []string
|
||||
for _, conv := range channelsList.Result.Convs {
|
||||
channels = append(channels, conv.Channel.TopicName)
|
||||
}
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (a *API) JoinChannel(teamName string, channelName string) (JoinChannelResult, error) {
|
||||
empty := JoinChannelResult{}
|
||||
|
||||
apiInput := fmt.Sprintf(`{"method": "join", "params": {"options": {"channel": {"name": "%s", "members_type": "team", "topic_name": "%s"}}}}`, teamName, channelName)
|
||||
output, err := a.doFetch(apiInput)
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
|
||||
joinChannel := JoinChannel{}
|
||||
err = json.Unmarshal(output, &joinChannel)
|
||||
if err != nil {
|
||||
return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err)
|
||||
}
|
||||
if joinChannel.Error.Message != "" {
|
||||
return empty, fmt.Errorf("received error from keybase team api: %s", joinChannel.Error.Message)
|
||||
}
|
||||
|
||||
return joinChannel.Result, nil
|
||||
}
|
||||
|
||||
func (a *API) LeaveChannel(teamName string, channelName string) (LeaveChannelResult, error) {
|
||||
empty := LeaveChannelResult{}
|
||||
|
||||
apiInput := fmt.Sprintf(`{"method": "leave", "params": {"options": {"channel": {"name": "%s", "members_type": "team", "topic_name": "%s"}}}}`, teamName, channelName)
|
||||
output, err := a.doFetch(apiInput)
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
|
||||
leaveChannel := LeaveChannel{}
|
||||
err = json.Unmarshal(output, &leaveChannel)
|
||||
if err != nil {
|
||||
return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err)
|
||||
}
|
||||
if leaveChannel.Error.Message != "" {
|
||||
return empty, fmt.Errorf("received error from keybase team api: %s", leaveChannel.Error.Message)
|
||||
}
|
||||
|
||||
return leaveChannel.Result, nil
|
||||
}
|
||||
|
||||
func (a *API) LogSend(feedback string) error {
|
||||
feedback = "go-keybase-chat-bot log send\n" +
|
||||
"username: " + a.GetUsername() + "\n" +
|
||||
feedback
|
||||
|
||||
args := []string{
|
||||
"log", "send",
|
||||
"--no-confirm",
|
||||
"--feedback", feedback,
|
||||
}
|
||||
|
||||
// We're determining whether the service is already running by running status
|
||||
// with autofork disabled.
|
||||
if err := a.runOpts.Command("--no-auto-fork", "status"); err != nil {
|
||||
// Assume that there's no service running, so log send as standalone
|
||||
args = append([]string{"--standalone"}, args...)
|
||||
}
|
||||
|
||||
return a.runOpts.Command(args...).Run()
|
||||
}
|
||||
|
||||
func (a *API) Shutdown() error {
|
||||
if a.runOpts.Oneshot != nil {
|
||||
err := a.runOpts.Command("logout", "--force").Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a.runOpts.StartService {
|
||||
err := a.runOpts.Command("ctl", "stop", "--shutdown").Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package kbchat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ListTeamMembers struct {
|
||||
Result ListTeamMembersResult `json:"result"`
|
||||
Error Error `json:"error"`
|
||||
}
|
||||
|
||||
type ListTeamMembersResult struct {
|
||||
Members ListTeamMembersResultMembers `json:"members"`
|
||||
}
|
||||
|
||||
type ListTeamMembersResultMembers struct {
|
||||
Owners []ListMembersOutputMembersCategory `json:"owners"`
|
||||
Admins []ListMembersOutputMembersCategory `json:"admins"`
|
||||
Writers []ListMembersOutputMembersCategory `json:"writers"`
|
||||
Readers []ListMembersOutputMembersCategory `json:"readers"`
|
||||
}
|
||||
|
||||
type ListMembersOutputMembersCategory struct {
|
||||
Username string `json:"username"`
|
||||
FullName string `json:"fullName"`
|
||||
}
|
||||
|
||||
type ListUserMemberships struct {
|
||||
Result ListUserMembershipsResult `json:"result"`
|
||||
Error Error `json:"error"`
|
||||
}
|
||||
|
||||
type ListUserMembershipsResult struct {
|
||||
Teams []ListUserMembershipsResultTeam `json:"teams"`
|
||||
}
|
||||
|
||||
type ListUserMembershipsResultTeam struct {
|
||||
TeamName string `json:"fq_name"`
|
||||
IsImplicitTeam bool `json:"is_implicit_team"`
|
||||
IsOpenTeam bool `json:"is_open_team"`
|
||||
Role int `json:"role"`
|
||||
MemberCount int `json:"member_count"`
|
||||
}
|
||||
|
||||
func (a *API) ListMembersOfTeam(teamName string) (ListTeamMembersResultMembers, error) {
|
||||
empty := ListTeamMembersResultMembers{}
|
||||
|
||||
apiInput := fmt.Sprintf(`{"method": "list-team-memberships", "params": {"options": {"team": "%s"}}}`, teamName)
|
||||
cmd := a.runOpts.Command("team", "api")
|
||||
cmd.Stdin = strings.NewReader(apiInput)
|
||||
bytes, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return empty, fmt.Errorf("failed to call keybase team api: %v", err)
|
||||
}
|
||||
|
||||
members := ListTeamMembers{}
|
||||
err = json.Unmarshal(bytes, &members)
|
||||
if err != nil {
|
||||
return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err)
|
||||
}
|
||||
if members.Error.Message != "" {
|
||||
return empty, fmt.Errorf("received error from keybase team api: %s", members.Error.Message)
|
||||
}
|
||||
return members.Result.Members, nil
|
||||
}
|
||||
|
||||
func (a *API) ListUserMemberships(username string) ([]ListUserMembershipsResultTeam, error) {
|
||||
empty := []ListUserMembershipsResultTeam{}
|
||||
|
||||
apiInput := fmt.Sprintf(`{"method": "list-user-memberships", "params": {"options": {"username": "%s"}}}`, username)
|
||||
cmd := a.runOpts.Command("team", "api")
|
||||
cmd.Stdin = strings.NewReader(apiInput)
|
||||
bytes, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return empty, fmt.Errorf("failed to call keybase team api: %v", err)
|
||||
}
|
||||
|
||||
members := ListUserMemberships{}
|
||||
err = json.Unmarshal(bytes, &members)
|
||||
if err != nil {
|
||||
return empty, fmt.Errorf("failed to parse output from keybase team api: %v", err)
|
||||
}
|
||||
if members.Error.Message != "" {
|
||||
return empty, fmt.Errorf("received error from keybase team api: %s", members.Error.Message)
|
||||
}
|
||||
return members.Result.Teams, nil
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
# Rename this file to `test_config.yaml`
|
||||
|
||||
config:
|
||||
bots:
|
||||
alice:
|
||||
username: "alice"
|
||||
paperkey: "foo bar car..."
|
||||
bob:
|
||||
username: "bob"
|
||||
paperkey: "one two three four..."
|
||||
teams:
|
||||
acme:
|
||||
# A real team that you add your alice1 and bob1 into
|
||||
name: "acme"
|
||||
# The channel to use
|
||||
topicname: "mysupercoolchannel"
|
@ -0,0 +1,54 @@
|
||||
package kbchat
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func randomString(t *testing.T) string {
|
||||
bytes := make([]byte, 16)
|
||||
_, err := rand.Read(bytes)
|
||||
require.NoError(t, err)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func randomTempDir(t *testing.T) string {
|
||||
return path.Join(os.TempDir(), "keybase_bot_"+randomString(t))
|
||||
}
|
||||
|
||||
func whichKeybase(t *testing.T) string {
|
||||
cmd := exec.Command("which", "keybase")
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err)
|
||||
location := strings.TrimSpace(string(out))
|
||||
return location
|
||||
}
|
||||
|
||||
func copyFile(t *testing.T, source, dest string) {
|
||||
sourceData, err := ioutil.ReadFile(source)
|
||||
require.NoError(t, err)
|
||||
err = ioutil.WriteFile(dest, sourceData, 0777)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Creates the working directory and copies over the keybase binary in PATH.
|
||||
// We do this to avoid any version mismatch issues.
|
||||
func prepWorkingDir(t *testing.T, workingDir string) string {
|
||||
kbLocation := whichKeybase(t)
|
||||
|
||||
err := os.Mkdir(workingDir, 0777)
|
||||
require.NoError(t, err)
|
||||
kbDestination := path.Join(workingDir, "keybase")
|
||||
|
||||
copyFile(t, kbLocation, kbDestination)
|
||||
|
||||
return kbDestination
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
package kbchat
|
||||
|
||||
type Sender struct {
|
||||
Uid string `json:"uid"`
|
||||
Username string `json:"username"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
Name string `json:"name"`
|
||||
Public bool `json:"public"`
|
||||
TopicType string `json:"topic_type"`
|
||||
TopicName string `json:"topic_name"`
|
||||
MembersType string `json:"members_type"`
|
||||
}
|
||||
|
||||
type Conversation struct {
|
||||
ID string `json:"id"`
|
||||
Unread bool `json:"unread"`
|
||||
Channel Channel `json:"channel"`
|
||||
}
|
||||
|
||||
type PaymentHolder struct {
|
||||
Payment Payment `json:"notification"`
|
||||
}
|
||||
|
||||
type Payment struct {
|
||||
TxID string `json:"txID"`
|
||||
StatusDescription string `json:"statusDescription"`
|
||||
FromAccountID string `json:"fromAccountID"`
|
||||
FromUsername string `json:"fromUsername"`
|
||||
ToAccountID string `json:"toAccountID"`
|
||||
ToUsername string `json:"toUsername"`
|
||||
AmountDescription string `json:"amountDescription"`
|
||||
WorthAtSendTime string `json:"worthAtSendTime"`
|
||||
ExternalTxURL string `json:"externalTxURL"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Convs []Conversation `json:"conversations"`
|
||||
}
|
||||
|
||||
type Inbox struct {
|
||||
Result Result `json:"result"`
|
||||
}
|
||||
|
||||
type ChannelsList struct {
|
||||
Result Result `json:"result"`
|
||||
}
|
||||
|
||||
type MsgPaymentDetails struct {
|
||||
ResultType int `json:"resultTyp"` // 0 good. 1 error
|
||||
PaymentID string `json:"sent"`
|
||||
}
|
||||
|
||||
type MsgPayment struct {
|
||||
Username string `json:"username"`
|
||||
PaymentText string `json:"paymentText"`
|
||||
Details MsgPaymentDetails `json:"result"`
|
||||
}
|
||||
|
||||
type Text struct {
|
||||
Body string `json:"body"`
|
||||
Payments []MsgPayment `json:"payments"`
|
||||
ReplyTo int `json:"replyTo"`
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
Type string `json:"type"`
|
||||
Text Text `json:"text"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Content Content `json:"content"`
|
||||
Sender Sender `json:"sender"`
|
||||
Channel Channel `json:"channel"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
MsgID int `json:"id"`
|
||||
}
|
||||
|
||||
type SendResult struct {
|
||||
MsgID int `json:"id"`
|
||||
}
|
||||
|
||||
type SendResponse struct {
|
||||
Result SendResult `json:"result"`
|
||||
}
|
||||
|
||||
type TypeHolder struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type MessageHolder struct {
|
||||
Msg Message `json:"msg"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type ThreadResult struct {
|
||||
Messages []MessageHolder `json:"messages"`
|
||||
}
|
||||
|
||||
type Thread struct {
|
||||
Result ThreadResult `json:"result"`
|
||||
}
|
||||
|
||||
type CommandExtendedDescription struct {
|
||||
Title string `json:"title"`
|
||||
DesktopBody string `json:"desktop_body"`
|
||||
MobileBody string `json:"mobile_body"`
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Usage string `json:"usage"`
|
||||
ExtendedDescription *CommandExtendedDescription `json:"extended_description,omitempty"`
|
||||
}
|
||||
|
||||
type CommandsAdvertisement struct {
|
||||
Typ string `json:"type"`
|
||||
Commands []Command
|
||||
TeamName string `json:"team_name,omitempty"`
|
||||
}
|
||||
|
||||
type Advertisement struct {
|
||||
Alias string `json:"alias,omitempty"`
|
||||
Advertisements []CommandsAdvertisement
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type JoinChannel struct {
|
||||
Error Error `json:"error"`
|
||||
Result JoinChannelResult `json:"result"`
|
||||
}
|
||||
|
||||
type JoinChannelResult struct {
|
||||
RateLimit []RateLimit `json:"ratelimits"`
|
||||
}
|
||||
|
||||
type LeaveChannel struct {
|
||||
Error Error `json:"error"`
|
||||
Result LeaveChannelResult `json:"result"`
|
||||
}
|
||||
|
||||
type LeaveChannelResult struct {
|
||||
RateLimit []RateLimit `json:"ratelimits"`
|
||||
}
|
||||
|
||||
type RateLimit struct {
|
||||
Tank string `json:"tank"`
|
||||
Capacity int `json:"capacity"`
|
||||
Reset int `json:"reset"`
|
||||
Gas int `json:"gas"`
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package kbchat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type WalletOutput struct {
|
||||
Result WalletResult `json:"result"`
|
||||
}
|
||||
|
||||
type WalletResult struct {
|
||||
TxID string `json:"txID"`
|
||||
Status string `json:"status"`
|
||||
Amount string `json:"amount"`
|
||||
Asset WalletAsset `json:"asset"`
|
||||
FromUsername string `json:"fromUsername"`
|
||||
ToUsername string `json:"toUsername"`
|
||||
}
|
||||
|
||||
type WalletAsset struct {
|
||||
Type string `json:"type"`
|
||||
Code string `json:"code"`
|
||||
Issuer string `json:"issuer"`
|
||||
}
|
||||
|
||||
func (a *API) GetWalletTxDetails(txID string) (wOut WalletOutput, err error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
apiInput := fmt.Sprintf(`{"method": "details", "params": {"options": {"txid": "%s"}}}`, txID)
|
||||
cmd := a.runOpts.Command("wallet", "api")
|
||||
cmd.Stdin = strings.NewReader(apiInput)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return wOut, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(out.Bytes(), &wOut); err != nil {
|
||||
return wOut, fmt.Errorf("unable to decode wallet output: %s", err.Error())
|
||||
}
|
||||
|
||||
return wOut, nil
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.12
|
||||
|
||||
package acme
|
||||
|
||||
import "runtime/debug"
|
||||
|
||||
func init() {
|
||||
// Set packageVersion if the binary was built in modules mode and x/crypto
|
||||
// was not replaced with a different module.
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, m := range info.Deps {
|
||||
if m.Path != "golang.org/x/crypto" {
|
||||
continue
|
||||
}
|
||||
if m.Replace == nil {
|
||||
packageVersion = m.Version
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.13
|
||||
|
||||
// Package ed25519 implements the Ed25519 signature algorithm. See
|
||||
// https://ed25519.cr.yp.to/.
|
||||
//
|
||||
// These functions are also compatible with the “Ed25519” function defined in
|
||||
// RFC 8032. However, unlike RFC 8032's formulation, this package's private key
|
||||
// representation includes a public key suffix to make multiple signing
|
||||
// operations with the same key more efficient. This package refers to the RFC
|
||||
// 8032 private key as the “seed”.
|
||||
//
|
||||
// Beginning with Go 1.13, the functionality of this package was moved to the
|
||||
// standard library as crypto/ed25519. This package only acts as a compatibility
|
||||
// wrapper.
|
||||
package ed25519
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
// PublicKeySize is the size, in bytes, of public keys as used in this package.
|
||||
PublicKeySize = 32
|
||||
// PrivateKeySize is the size, in bytes, of private keys as used in this package.
|
||||
PrivateKeySize = 64
|
||||
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
|
||||
SignatureSize = 64
|
||||
// SeedSize is the size, in bytes, of private key seeds. These are the private key representations used by RFC 8032.
|
||||
SeedSize = 32
|
||||
)
|
||||
|
||||
// PublicKey is the type of Ed25519 public keys.
|
||||
//
|
||||
// This type is an alias for crypto/ed25519's PublicKey type.
|
||||
// See the crypto/ed25519 package for the methods on this type.
|
||||
type PublicKey = ed25519.PublicKey
|
||||
|
||||
// PrivateKey is the type of Ed25519 private keys. It implements crypto.Signer.
|
||||
//
|
||||
// This type is an alias for crypto/ed25519's PrivateKey type.
|
||||
// See the crypto/ed25519 package for the methods on this type.
|
||||
type PrivateKey = ed25519.PrivateKey
|
||||
|
||||
// GenerateKey generates a public/private key pair using entropy from rand.
|
||||
// If rand is nil, crypto/rand.Reader will be used.
|
||||
func GenerateKey(rand io.Reader) (PublicKey, PrivateKey, error) {
|
||||
return ed25519.GenerateKey(rand)
|
||||
}
|
||||
|
||||
// NewKeyFromSeed calculates a private key from a seed. It will panic if
|
||||
// len(seed) is not SeedSize. This function is provided for interoperability
|
||||
// with RFC 8032. RFC 8032's private keys correspond to seeds in this
|
||||
// package.
|
||||
func NewKeyFromSeed(seed []byte) PrivateKey {
|
||||
return ed25519.NewKeyFromSeed(seed)
|
||||
}
|
||||
|
||||
// Sign signs the message with privateKey and returns a signature. It will
|
||||
// panic if len(privateKey) is not PrivateKeySize.
|
||||
func Sign(privateKey PrivateKey, message []byte) []byte {
|
||||
return ed25519.Sign(privateKey, message)
|
||||
}
|
||||
|
||||
// Verify reports whether sig is a valid signature of message by publicKey. It
|
||||
// will panic if len(publicKey) is not PublicKeySize.
|
||||
func Verify(publicKey PublicKey, message, sig []byte) bool {
|
||||
return ed25519.Verify(publicKey, message, sig)
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build aix dragonfly freebsd linux netbsd openbsd
|
||||
|
||||
package unix
|
||||
|
||||
// ReadDirent reads directory entries from fd and writes them into buf.
|
||||
func ReadDirent(fd int, buf []byte) (n int, err error) {
|
||||
return Getdents(fd, buf)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin
|
||||
|
||||
package unix
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// ReadDirent reads directory entries from fd and writes them into buf.
|
||||
func ReadDirent(fd int, buf []byte) (n int, err error) {
|
||||
// Final argument is (basep *uintptr) and the syscall doesn't take nil.
|
||||
// 64 bits should be enough. (32 bits isn't even on 386). Since the
|
||||
// actual system call is getdirentries64, 64 is a good guess.
|
||||
// TODO(rsc): Can we use a single global basep for all calls?
|
||||
var base = (*uintptr)(unsafe.Pointer(new(uint64)))
|
||||
return Getdirentries(fd, buf, base)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue