diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7eb92c9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Laurence de Jong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e811362 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# go-acapy-client + +A library for interacting with ACA-py in Go. \ No newline at end of file diff --git a/acapy.go b/acapy.go new file mode 100644 index 0000000..bd78863 --- /dev/null +++ b/acapy.go @@ -0,0 +1,69 @@ +package acapy + +import ( + "bytes" + "encoding/json" + "io" + "net/http" +) + +type Client struct { + LedgerURL string + AcapyURL string + + HTTPClient http.Client +} + +func NewClient(ledgerURL string, acapyURL string) *Client { + return &Client{ + LedgerURL: ledgerURL, + AcapyURL: acapyURL, + HTTPClient: http.Client{}, + } +} + +func (c *Client) post(url string, queryParam map[string]string, body interface{}, response interface{}) error { + return c.request(http.MethodPost, url, queryParam, body, response) +} + +func (c *Client) get(url string, queryParams map[string]string, response interface{}) error { + return c.request(http.MethodGet, url, queryParams, nil, response) +} + +func (c *Client) request(method string, url string, queryParams map[string]string, body interface{}, response interface{}) error { + var input io.Reader + var err error + + if body != nil { + jsonInput, err := json.Marshal(body) + if err != nil { + return err + } + input = bytes.NewReader(jsonInput) + } + + r, err := http.NewRequest(method, url, input) + if err != nil { + return err + } + r.Header.Add("Content-Type", "application/json") + + q := r.URL.Query() + for k, v := range queryParams { + if k != "" && v != "" { + q.Add(k, v) + } + } + r.URL.RawQuery = q.Encode() + + result, err := c.HTTPClient.Do(r) + if err != nil { + return err + } + + err = json.NewDecoder(result.Body).Decode(response) + if err != nil { + return err + } + return nil +} diff --git a/basicmessages.go b/basicmessages.go new file mode 100644 index 0000000..9d103ae --- /dev/null +++ b/basicmessages.go @@ -0,0 +1,16 @@ +package acapy + +import ( + "fmt" +) + +func (c *Client) SendBasicMessage(connectionID string, message string) error { + type BasicMessage struct { + Content string `json:"content"` + } + var basicMessage = BasicMessage{ + Content: message, + } + + return c.post(fmt.Sprintf("%s/connections/%s/send-message", c.AcapyURL, connectionID), nil, basicMessage, nil) +} diff --git a/connections.go b/connections.go new file mode 100644 index 0000000..0c314a5 --- /dev/null +++ b/connections.go @@ -0,0 +1,175 @@ +package acapy + +import ( + "fmt" + "strconv" +) + +type CreateInvitationResponse struct { + InvitationURL string `json:"invitation_url,omitempty"` + ConnectionID string `json:"connection_id,omitempty"` + Invitation struct { + ImageURL string `json:"imageUrl,omitempty"` + Label string `json:"label,omitempty"` + ServiceEndpoint string `json:"serviceEndpoint,omitempty"` + RecipientKeys []string `json:"recipientKeys,omitempty"` + RoutingKeys []string `json:"routingKeys,omitempty"` + ID string `json:"@id,omitempty"` + DID string `json:"did,omitempty"` + Type string `json:"@type,omitempty"` + } `json:"invitation,omitempty"` +} + +func (c *Client) CreateInvitation(alias string, autoAccept bool, multiUse bool, public bool) (CreateInvitationResponse, error) { + var createInvitationResponse CreateInvitationResponse + err := c.post(c.AcapyURL+"/connections/create-invitation", map[string]string{ + "alias": alias, + "auto_accept": strconv.FormatBool(autoAccept), + "multi_use": strconv.FormatBool(multiUse), + "public": strconv.FormatBool(public), + }, nil, &createInvitationResponse) + return createInvitationResponse, err +} + +type ReceiveInvitationResponse struct { + InboundConnectionID string `json:"inbound_connection_id,omitempty"` + InvitationKey string `json:"invitation_key,omitempty"` + MyDid string `json:"my_did,omitempty"` + TheirDid string `json:"their_did,omitempty"` + TheirRole string `json:"their_role,omitempty"` + RequestID string `json:"request_id,omitempty"` + State string `json:"state,omitempty"` + ConnectionID string `json:"connection_id,omitempty"` + Alias string `json:"alias,omitempty"` + InvitationMode string `json:"invitation_mode,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + Accept string `json:"accept,omitempty"` + Initiator string `json:"initiator,omitempty"` + ErrorMsg string `json:"error_msg,omitempty"` + TheirLabel string `json:"their_label,omitempty"` + RoutingState string `json:"routing_state,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` +} + +func (c *Client) ReceiveInvitation(invitation Invitation) (ReceiveInvitationResponse, error) { + var receiveInvitationResponse ReceiveInvitationResponse + + err := c.post(c.AcapyURL+"/connections/receive-invitation", map[string]string{ + "alias": invitation.Label, + "auto_accept": strconv.FormatBool(false), + }, invitation, &receiveInvitationResponse) + return receiveInvitationResponse, err +} + +type Invitation struct { + ImageURL string `json:"imageUrl,omitempty"` + Label string `json:"label,omitempty"` + ServiceEndpoint string `json:"serviceEndpoint,omitempty"` + RecipientKeys []string `json:"recipientKeys,omitempty"` + RoutingKeys []string `json:"routingKeys,omitempty"` + ID string `json:"@id,omitempty"` + DID string `json:"did,omitempty"` +} + +func (c *Client) AcceptInvitation(connectionID string) (Connection, error) { + var connection Connection + err := c.post(fmt.Sprintf("%s/connections/%s/accept-invitation", c.AcapyURL, connectionID), nil, nil, &connection) + return connection, err +} + +func (c *Client) AcceptRequest(connectionID string) (Connection, error) { + var connection Connection + err := c.post(fmt.Sprintf("%s/connections/%s/accept-request", c.AcapyURL, connectionID), nil, nil, &connection) + return connection, err +} + +type Connection struct { + Accept string `json:"accept"` + Alias string `json:"alias"` + ConnectionID string `json:"connection_id"` + CreatedAt string `json:"created_at"` + ErrorMsg string `json:"error_msg"` + InboundConnectionID string `json:"inbound_connection_id"` + Initiator string `json:"initiator"` + InvitationKey string `json:"invitation_key"` + InvitationMode string `json:"invitation_mode"` + MyDid string `json:"my_did"` + RequestID string `json:"request_id"` + RoutingState string `json:"routing_state"` + State string `json:"state"` + TheirDid string `json:"their_did"` + TheirLabel string `json:"their_label"` + TheirRole string `json:"their_role"` + UpdatedAt string `json:"updated_at"` +} + +// QueryConnectionsParams model +// +// Parameters for querying connections +// +type QueryConnectionsParams struct { + + // Alias of connection invitation + Alias string `json:"alias,omitempty"` + + // Initiator is Connection invitation initiator + Initiator string `json:"initiator,omitempty"` + + // Invitation key + InvitationKey string `json:"invitation_key,omitempty"` + + // MyDID is DID of the agent + MyDID string `json:"my_did,omitempty"` + + // State of the connection invitation + State string `json:"state"` + + // TheirDID is other party's DID + TheirDID string `json:"their_did,omitempty"` + + // TheirRole is other party's role + TheirRole string `json:"their_role,omitempty"` +} + +func (c *Client) QueryConnections(params QueryConnectionsParams) ([]Connection, error) { + var connections = struct { + Result []Connection `json:"results"` + }{} + + var queryParams = map[string]string{ + "alias": params.Alias, + "initiator": params.Initiator, + "invitation_key": params.InvitationKey, + "my_did": params.MyDID, + "connection_state": params.State, + "their_did": params.TheirDID, + "their_role": params.TheirRole, + } + err := c.get(c.AcapyURL+"/connections", queryParams, &connections) + return connections.Result, err +} + +func (c *Client) GetConnection(connectionID string) (Connection, error) { + var connection Connection + err := c.get(fmt.Sprintf("%s/connections/%s", c.AcapyURL, connectionID), nil, &connection) + return connection, err +} + +func (c *Client) RemoveConnection(connectionID string) error { + return c.post(fmt.Sprintf("%s/connections/%s", c.AcapyURL, connectionID), nil, nil, nil) +} + +type Thread struct { + ThreadID string `json:"thread_id"` +} + +func (c *Client) SendPing(connectionID string) (Thread, error) { + ping := struct { + Comment string `json:"comment"` + }{ + Comment: "ping", + } + var thread Thread + err := c.post(fmt.Sprintf("%s/connections/%s/send-ping", c.AcapyURL, connectionID), nil, ping, &thread) + return thread, err +} diff --git a/examples/go.mod b/examples/go.mod new file mode 100644 index 0000000..547d636 --- /dev/null +++ b/examples/go.mod @@ -0,0 +1,8 @@ +module github.com/ldej/go-acapy-client/example + +go 1.15 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/ldej/go-acapy-client v0.1 +) \ No newline at end of file diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..f7e1321 --- /dev/null +++ b/examples/main.go @@ -0,0 +1,226 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/gorilla/mux" + + acapy "github.com/ldej/go-acapy-client" +) + +type App struct { + client *acapy.Client + server *http.Server + port string +} + +func (app *App) ReadCommands() { + scanner := bufio.NewScanner(os.Stdin) + + for { + fmt.Println(`Choose: + (1) Register DID + (2) Register Schema + (3) Create invitation + (4) Receive invitation + (5) Accept invitation + (6) Accept request + (7) Send basic message + (8) Query connections`) + fmt.Print("Enter Command: ") + scanner.Scan() + command := scanner.Text() + + switch command { + case "exit": + app.Exit() + return + case "1": + fmt.Print("Seed: ") + scanner.Scan() + seed := scanner.Text() + app.RegisterDID(seed) + case "2": + app.RegisterSchema() + case "3": + fmt.Print("Alias: ") + scanner.Scan() + alias := scanner.Text() + invitationResponse, _ := app.CreateInvitation(alias, false, false, true) + invitation, _ := json.Marshal(invitationResponse.Invitation) + fmt.Printf("Invitation json: %s\n", string(invitation)) + case "4": + fmt.Print("Invitation json: ") + scanner.Scan() + invitation := scanner.Bytes() + receiveInvitation, _ := app.ReceiveInvitation(invitation) + fmt.Printf("Connection id: %s\n", receiveInvitation.ConnectionID) + case "5": + fmt.Print("Connection id: ") + scanner.Scan() + connectionID := scanner.Text() + app.AcceptInvitation(connectionID) + case "6": + fmt.Print("Connection id: ") + scanner.Scan() + connectionID := scanner.Text() + app.AcceptRequest(connectionID) + case "7": + fmt.Print("Connection id: ") + scanner.Scan() + connectionID := scanner.Text() + fmt.Print("Message: ") + scanner.Scan() + message := scanner.Text() + app.SendBasicMessage(connectionID, message) + case "8": + app.QueryConnections(acapy.QueryConnectionsParams{}) + } + } +} + +func (app *App) StartWebserver() { + r := mux.NewRouter() + webhooksHandler := acapy.Webhooks( + app.ConnectionsEventHandler, + app.BasicMessagesEventHandler, + app.ProblemReportEventHandler, + ) + + r.HandleFunc("/webhooks/topic/{topic}/", webhooksHandler).Methods(http.MethodPost) + fmt.Printf("Listening on %v\n", app.port) + + app.server = &http.Server{ + Addr: ":" + app.port, + Handler: r, + } + + go func() { + if err := app.server.ListenAndServe(); err != nil { + log.Fatal(err) + } + }() +} + +func (app *App) Exit() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := app.server.Shutdown(ctx); err != nil { + log.Fatal(err) + } +} + +func (app *App) ConnectionsEventHandler(event acapy.ConnectionsEvent) { + fmt.Printf("\n -> Connection %q (%s), update to state %q\n", event.Alias, event.ConnectionID, event.State) +} + +func (app *App) BasicMessagesEventHandler(event acapy.BasicMessagesEvent) { + connection, _ := app.GetConnection(event.ConnectionID) + fmt.Printf("\n -> Received message from %q (%s): %s\n", connection.Alias, event.ConnectionID, event.Content) +} + +func (app *App) ProblemReportEventHandler(event acapy.ProblemReportEvent) { + fmt.Printf("\n -> Received problem report: %+v\n", event) +} + +func main() { + var port = "4455" + var ledgerURL = "http://localhost:9000" + var acapyURL = "http://0.0.0.0:11000" + + flag.StringVar(&port, "port", "4455", "port") + flag.StringVar(&acapyURL, "acapy", "http://localhost:11000", "acapy") + flag.Parse() + + app := App{ + client: acapy.NewClient(ledgerURL, acapyURL), + port: port, + } + app.StartWebserver() + app.ReadCommands() +} + +func (app *App) RegisterDID(seed string) (acapy.RegisterDIDResponse, error) { + didResponse, err := app.client.RegisterDID( + "Laurence de Jong", + seed, + "TRUST_ANCHOR", + ) + if err != nil { + return acapy.RegisterDIDResponse{}, err + } + return didResponse, nil +} + +func (app *App) RegisterSchema() acapy.SchemaResponse { + schemaResponse, err := app.client.RegisterSchema( + "Laurence", + "1.0", + []string{"name"}, + ) + if err != nil { + log.Fatal("Failed to register schema: ", err) + } + fmt.Printf("Registered schema: %+v\n", schemaResponse) + return schemaResponse +} + +func (app *App) CreateInvitation(alias string, autoAccept bool, multiUse bool, public bool) (acapy.CreateInvitationResponse, error) { + invitationResponse, err := app.client.CreateInvitation(alias, autoAccept, multiUse, public) + if err != nil { + log.Fatal("Failed to create invitation: ", err) + return acapy.CreateInvitationResponse{}, err + } + return invitationResponse, nil +} + +func (app *App) ReceiveInvitation(inv []byte) (acapy.ReceiveInvitationResponse, error) { + var invitation acapy.Invitation + err := json.Unmarshal(inv, &invitation) + if err != nil { + return acapy.ReceiveInvitationResponse{}, err + } + return app.client.ReceiveInvitation(invitation) +} + +func (app *App) AcceptInvitation(connectionID string) (acapy.Connection, error) { + return app.client.AcceptInvitation(connectionID) +} + +func (app *App) AcceptRequest(connectionID string) (acapy.Connection, error) { + return app.client.AcceptRequest(connectionID) +} + +func (app *App) SendPing(connectionID string) (acapy.Thread, error) { + return app.client.SendPing(connectionID) +} + +func (app *App) SendBasicMessage(connectionID string, message string) error { + return app.client.SendBasicMessage(connectionID, message) +} + +func (app *App) GetConnection(connectionID string) (acapy.Connection, error) { + return app.client.GetConnection(connectionID) +} + +func (app *App) QueryConnections(params acapy.QueryConnectionsParams) ([]acapy.Connection, error) { + connections, err := app.client.QueryConnections(params) + if err != nil { + log.Fatal("Failed to list connections: ", err) + return nil, err + } + indent, err := json.MarshalIndent(connections, "", " ") + if err != nil { + return nil, err + } + fmt.Printf(string(indent) + "\n") + return connections, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..73dcffb --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/ldej/go-acapy-client + +go 1.15 \ No newline at end of file diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/registerdid.go b/registerdid.go new file mode 100644 index 0000000..1e88c73 --- /dev/null +++ b/registerdid.go @@ -0,0 +1,30 @@ +package acapy + +type registerDIDRequest struct { + Alias string `json:"alias"` + Seed string `json:"seed"` + Role string `json:"role"` +} + +type RegisterDIDResponse struct { + DID string `json:"did"` + Seed string `json:"seed"` + Verkey string `json:"verkey"` +} + +func (c *Client) RegisterDID(alias string, seed string, role string) (RegisterDIDResponse, error) { + var registerDID registerDIDRequest + var registerDIDResponse RegisterDIDResponse + + registerDID = registerDIDRequest{ + Alias: alias, + Seed: seed, // Should be random in Develop mode + Role: role, + } + err := c.post(c.LedgerURL+"/register", nil, registerDID, ®isterDIDResponse) + if err != nil { + return registerDIDResponse, err + } + + return registerDIDResponse, nil +} diff --git a/schema.go b/schema.go new file mode 100644 index 0000000..6e97ae9 --- /dev/null +++ b/schema.go @@ -0,0 +1,30 @@ +package acapy + +type Schema struct { + Version string `json:"schema_version"` + Name string `json:"schema_name"` + Attributes []string `json:"attributes"` +} + +type SchemaResponse struct { + SchemaID string `json:"schema_id"` + Schema struct { + Ver string `json:"ver"` + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + AttrNames []string `json:"attr_names"` + } `json:"schema"` + SeqNo int `json:"seqNo,omitempty"` +} + +func (c *Client) RegisterSchema(name string, version string, attributes []string) (SchemaResponse, error) { + var schema = Schema{ + Name: name, + Version: version, + Attributes: attributes, + } + var schemaResponse SchemaResponse + err := c.post(c.AcapyURL+"/schemas", nil, schema, &schemaResponse) + return schemaResponse, err +} diff --git a/webhooks.go b/webhooks.go new file mode 100644 index 0000000..24eee05 --- /dev/null +++ b/webhooks.go @@ -0,0 +1,71 @@ +package acapy + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +func Webhooks( + connectionsEventHandler func(event ConnectionsEvent), + basicMessagesEventHandler func(event BasicMessagesEvent), + problemReportEventHandler func(event ProblemReportEvent), +) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + path := strings.Split(strings.TrimSuffix(r.URL.Path, "/"), "/") + topic := path[len(path)-1] + + switch topic { + case "connections": + var connectionsEvent ConnectionsEvent + json.NewDecoder(r.Body).Decode(&connectionsEvent) + connectionsEventHandler(connectionsEvent) + case "basicmessages": + var basicMessagesEvent BasicMessagesEvent + json.NewDecoder(r.Body).Decode(&basicMessagesEvent) + basicMessagesEventHandler(basicMessagesEvent) + case "problem_report": + var problemReportEvent ProblemReportEvent + json.NewDecoder(r.Body).Decode(&problemReportEvent) + problemReportEventHandler(problemReportEvent) + default: + fmt.Printf("Topic not supported: %q\n", topic) + w.WriteHeader(404) + body, _ := ioutil.ReadAll(r.Body) + fmt.Printf(string(body)) + return + } + w.WriteHeader(200) + } +} + +type ConnectionsEvent struct { + Initiator string `json:"initiator"` + CreatedAt string `json:"created_at"` + State string `json:"state"` + ConnectionID string `json:"connection_id"` + Accept string `json:"accept"` + Alias string `json:"alias"` + InvitationMode string `json:"invitation_mode"` + UpdatedAt string `json:"updated_at"` + RoutingState string `json:"routing_state"` + InvitationKey string `json:"invitation_key"` +} + +type BasicMessagesEvent struct { + ConnectionID string `json:"connection_id"` + MessageID string `json:"message_id"` + State string `json:"state"` + Content string `json:"content"` +} + +type ProblemReportEvent struct { + Type string `json:"@type"` + ID string `json:"@id"` + Thread struct { + Thid string `json:"thid"` + } `json:"~thread"` + ExplainLtxt string `json:"explain-ltxt"` +}