New communications package

Support for Slack, SMSGlobal, SMTP and Telegram

Supersedes: https://github.com/thrasher-/gocryptotrader/pull/126
This commit is contained in:
Ryan O'Hara-Reid
2018-05-21 17:08:44 +10:00
committed by Adrian Gallagher
parent d34fc9aae8
commit 9d0616d8cf
26 changed files with 2748 additions and 868 deletions

166
communications/base/base.go Normal file
View File

@@ -0,0 +1,166 @@
package base
import (
"fmt"
"sync"
"time"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
)
//global vars contain staged update data that will be sent to the communication
// mediums
var (
TickerStaged map[string]map[string]map[string]ticker.Price
OrderbookStaged map[string]map[string]map[string]Orderbook
PortfolioStaged Portfolio
SettingsStaged Settings
ServiceStarted time.Time
m sync.Mutex
)
// Orderbook holds the minimal orderbook details to be sent to a communication
// medium
type Orderbook struct {
CurrencyPair string
AssetType string
TotalAsks float64
TotalBids float64
LastUpdated string
}
// Ticker holds the minimal orderbook details to be sent to a communication
// medium
type Ticker struct {
CurrencyPair string
LastUpdated string
}
// Portfolio holds the minimal portfolio details to be sent to a communication
// medium
type Portfolio struct {
ProfitLoss string
}
// Settings holds the minimal setting details to be sent to a communication
// medium
type Settings struct {
EnabledExchanges string
EnabledCommunications string
}
// Base enforces standard variables across communication packages
type Base struct {
Name string
Enabled bool
Verbose bool
Connected bool
}
// Event is a generalise event type
type Event struct {
Type string
GainLoss string
TradeDetails string
}
// IsEnabled returns if the comms package has been enabled in the configuration
func (b *Base) IsEnabled() bool {
return b.Enabled
}
// IsConnected returns if the package is connected to a server and/or ready to
// send
func (b *Base) IsConnected() bool {
return b.Connected
}
// GetName returns a package name
func (b *Base) GetName() string {
return b.Name
}
// GetTicker returns staged ticker data
func (b *Base) GetTicker(exchangeName string) string {
m.Lock()
defer m.Unlock()
tickerPrice, ok := TickerStaged[exchangeName]
if !ok {
return ""
}
var tickerPrices []ticker.Price
for _, x := range tickerPrice {
for _, y := range x {
tickerPrices = append(tickerPrices, y)
}
}
var packagedTickers []string
for i := range tickerPrices {
packagedTickers = append(packagedTickers, fmt.Sprintf(
"Currency Pair: %s Ask: %f, Bid: %f High: %f Last: %f Low: %f ATH: %f Volume: %f",
tickerPrices[i].CurrencyPair,
tickerPrices[i].Ask,
tickerPrices[i].Bid,
tickerPrices[i].High,
tickerPrices[i].Last,
tickerPrices[i].Low,
tickerPrices[i].PriceATH,
tickerPrices[i].Volume))
}
return common.JoinStrings(packagedTickers, "\n")
}
// GetOrderbook returns staged orderbook data
func (b *Base) GetOrderbook(exchangeName string) string {
m.Lock()
defer m.Unlock()
orderbook, ok := OrderbookStaged[exchangeName]
if !ok {
return ""
}
var orderbooks []Orderbook
for _, x := range orderbook {
for _, y := range x {
orderbooks = append(orderbooks, y)
}
}
var packagedOrderbooks []string
for i := range orderbooks {
packagedOrderbooks = append(packagedOrderbooks, fmt.Sprintf(
"Currency Pair: %s AssetType: %s, LastUpdated: %s TotalAsks: %f TotalBids: %f",
orderbooks[i].CurrencyPair,
orderbooks[i].AssetType,
orderbooks[i].LastUpdated,
orderbooks[i].TotalAsks,
orderbooks[i].TotalBids))
}
return common.JoinStrings(packagedOrderbooks, "\n")
}
// GetPortfolio returns staged portfolio info
func (b *Base) GetPortfolio() string {
m.Lock()
defer m.Unlock()
return fmt.Sprintf("%v", PortfolioStaged)
}
// GetSettings returns stage setting info
func (b *Base) GetSettings() string {
m.Lock()
defer m.Unlock()
return fmt.Sprintf("%v", SettingsStaged)
}
// GetStatus returns status data
func (b *Base) GetStatus() string {
return `
GoCryptoTrader Service: Online
Service Started: ` + ServiceStarted.String()
}

View File

@@ -0,0 +1,108 @@
package base
import (
"log"
"time"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
)
// IComm is the main interface array across the communication packages
type IComm []ICommunicate
// ICommunicate enforces standard functions across communication packages
type ICommunicate interface {
Setup(config config.CommunicationsConfig)
Connect() error
PushEvent(Event) error
IsEnabled() bool
IsConnected() bool
GetName() string
}
// Setup sets up communication variables and intiates a connection to the
// communication mediums
func (c IComm) Setup() {
TickerStaged = make(map[string]map[string]map[string]ticker.Price)
OrderbookStaged = make(map[string]map[string]map[string]Orderbook)
ServiceStarted = time.Now()
for i := range c {
if c[i].IsEnabled() && !c[i].IsConnected() {
err := c[i].Connect()
if err != nil {
log.Printf("Communications: %s failed to connect. Err: %s", c[i].GetName(), err)
}
}
}
}
// PushEvent pushes triggered events to all enabled communication links
func (c IComm) PushEvent(event Event) {
for i := range c {
if c[i].IsEnabled() && c[i].IsConnected() {
err := c[i].PushEvent(event)
if err != nil {
log.Printf("Communications error - PushEvent() in package %s with %v",
c[i].GetName(), event)
}
}
}
}
// GetEnabledCommunicationMediums prints out enabled and connected communication
// packages
func (c IComm) GetEnabledCommunicationMediums() {
var count int
for i := range c {
if c[i].IsEnabled() && c[i].IsConnected() {
log.Printf("Communications: Medium %s is enabled.", c[i].GetName())
count++
}
}
if count == 0 {
log.Println("Communications: No communication mediums are enabled.")
}
}
// StageTickerData stages updated ticker data for the communications package
func (c IComm) StageTickerData(exchangeName, assetType string, tickerPrice ticker.Price) {
m.Lock()
defer m.Unlock()
if _, ok := TickerStaged[exchangeName]; !ok {
TickerStaged[exchangeName] = make(map[string]map[string]ticker.Price)
}
if _, ok := TickerStaged[exchangeName][assetType]; !ok {
TickerStaged[exchangeName][assetType] = make(map[string]ticker.Price)
}
TickerStaged[exchangeName][assetType][tickerPrice.CurrencyPair] = tickerPrice
}
// StageOrderbookData stages updated orderbook data for the communications
// package
func (c IComm) StageOrderbookData(exchangeName, assetType string, orderbook orderbook.Base) {
m.Lock()
defer m.Unlock()
if _, ok := OrderbookStaged[exchangeName]; !ok {
OrderbookStaged[exchangeName] = make(map[string]map[string]Orderbook)
}
if _, ok := OrderbookStaged[exchangeName][assetType]; !ok {
OrderbookStaged[exchangeName][assetType] = make(map[string]Orderbook)
}
_, totalAsks := orderbook.CalculateTotalAsks()
_, totalBids := orderbook.CalculateTotalBids()
OrderbookStaged[exchangeName][assetType][orderbook.CurrencyPair] = Orderbook{
CurrencyPair: orderbook.CurrencyPair,
TotalAsks: totalAsks,
TotalBids: totalBids,
LastUpdated: orderbook.LastUpdated.String()}
}

View File

@@ -0,0 +1,84 @@
package base
import (
"testing"
)
var (
b Base
i IComm
)
func TestStart(t *testing.T) {
b = Base{
Name: "test",
Enabled: true,
Verbose: true,
Connected: true,
}
}
func TestIsEnabled(t *testing.T) {
if !b.IsEnabled() {
t.Error("test failed - base IsEnabled() error")
}
}
func TestIsConnected(t *testing.T) {
if !b.IsConnected() {
t.Error("test failed - base IsConnected() error")
}
}
func TestGetName(t *testing.T) {
if b.GetName() != "test" {
t.Error("test failed - base GetName() error")
}
}
func TestGetTicker(t *testing.T) {
v := b.GetTicker("ANX")
if v != "" {
t.Error("test failed - base GetTicker() error")
}
}
func TestGetOrderbook(t *testing.T) {
v := b.GetOrderbook("ANX")
if v != "" {
t.Error("test failed - base GetOrderbook() error")
}
}
func TestGetPortfolio(t *testing.T) {
v := b.GetPortfolio()
if v != "{}" {
t.Error("test failed - base GetPortfolio() error")
}
}
func TestGetSettings(t *testing.T) {
v := b.GetSettings()
if v != "{ }" {
t.Error("test failed - base GetSettings() error")
}
}
func TestGetStatus(t *testing.T) {
v := b.GetStatus()
if v == "" {
t.Error("test failed - base GetStatus() error")
}
}
func TestSetup(t *testing.T) {
i.Setup()
}
func TestPushEvent(t *testing.T) {
i.PushEvent(Event{})
}
func TestGetEnabledCommunicationMediums(t *testing.T) {
i.GetEnabledCommunicationMediums()
}

View File

@@ -0,0 +1,47 @@
package communications
import (
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/communications/slack"
"github.com/thrasher-/gocryptotrader/communications/smsglobal"
"github.com/thrasher-/gocryptotrader/communications/smtpservice"
"github.com/thrasher-/gocryptotrader/communications/telegram"
"github.com/thrasher-/gocryptotrader/config"
)
// Communications is the overarching type across the communications packages
type Communications struct {
base.IComm
}
// NewComm sets up and returns a pointer to a Communications object
func NewComm(config config.CommunicationsConfig) *Communications {
var comm Communications
if config.TelegramConfig.Enabled {
Telegram := new(telegram.Telegram)
Telegram.Setup(config)
comm.IComm = append(comm.IComm, Telegram)
}
if config.SMSGlobalConfig.Enabled {
SMSGlobal := new(smsglobal.SMSGlobal)
SMSGlobal.Setup(config)
comm.IComm = append(comm.IComm, SMSGlobal)
}
if config.SMTPConfig.Enabled {
SMTP := new(smtpservice.SMTPservice)
SMTP.Setup(config)
comm.IComm = append(comm.IComm, SMTP)
}
if config.SlackConfig.Enabled {
Slack := new(slack.Slack)
Slack.Setup(config)
comm.IComm = append(comm.IComm, Slack)
}
comm.Setup()
return &comm
}

View File

@@ -0,0 +1,373 @@
// Package slack is used to connect to the slack network. Slack is a
// code-centric collaboration hub that allows users to connect via an app and
// share different types of data
package slack
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/config"
)
// const declares main slack url and commands that will be supported on client
// side
const (
SlackURL = "https://slack.com/api/rtm.start"
cmdStatus = "!status"
cmdHelp = "!help"
cmdSettings = "!settings"
cmdTicker = "!ticker"
cmdPortfolio = "!portfolio"
cmdOrderbook = "!orderbook"
getHelp = `GoCryptoTrader SlackBot, thank you for using this service!
Current commands are:
!status - Displays current working status of bot
!help - Displays help text
!settings - Displays current settings
!ticker - Displays recent ANX ticker
!portfolio - Displays portfolio data
!orderbook - Displays current ANX orderbook`
)
// Slack starts a websocket connection and uses https://api.slack.com/rtm real
// time messaging
type Slack struct {
base.Base
TargetChannel string
VerificationToken string
TargetChannelID string
Details Response
ReconnectURL string
WebsocketConn *websocket.Conn
Connected bool
Shutdown bool
sync.Mutex
}
// Setup takes in a slack configuration, sets bots target channel and
// sets verfication token to access workspace
func (s *Slack) Setup(config config.CommunicationsConfig) {
s.Name = config.SlackConfig.Name
s.Enabled = config.SlackConfig.Enabled
s.Verbose = config.SlackConfig.Verbose
s.TargetChannel = config.SlackConfig.TargetChannel
s.VerificationToken = config.SlackConfig.VerificationToken
}
// Connect connects to the service
func (s *Slack) Connect() error {
if err := s.NewConnection(); err != nil {
return err
}
s.Connected = true
return nil
}
// PushEvent pushes an event to either a slack channel or specific client
func (s *Slack) PushEvent(base.Event) error {
return errors.New("not yet implemented")
}
// BuildURL returns an appended token string with the SlackURL
func (s *Slack) BuildURL(token string) string {
return fmt.Sprintf("%s?token=%s", SlackURL, token)
}
// GetChannelsString returns a list of all channels on the slack workspace
func (s *Slack) GetChannelsString() []string {
var channels []string
for i := range s.Details.Channels {
channels = append(channels, s.Details.Channels[i].NameNormalized)
}
return channels
}
// GetUsernameByID returns a users name by ID
func (s *Slack) GetUsernameByID(ID string) string {
for i := range s.Details.Users {
if s.Details.Users[i].ID == ID {
return s.Details.Users[i].Name
}
}
return ""
}
// GetIDByName returns either a group ID or Channel ID
func (s *Slack) GetIDByName(userName string) (string, error) {
id, err := s.GetGroupIDByName(userName)
if err != nil {
return s.GetChannelIDByName(userName)
}
return id, err
}
// GetGroupIDByName returns a groupID by group name
func (s *Slack) GetGroupIDByName(group string) (string, error) {
for i := range s.Details.Groups {
if s.Details.Groups[i].Name == group {
return s.Details.Groups[i].ID, nil
}
}
return "", errors.New("Channel not found")
}
// GetChannelIDByName returns a channel ID by its corresponding name
func (s *Slack) GetChannelIDByName(channel string) (string, error) {
for i := range s.Details.Channels {
if s.Details.Channels[i].Name == channel {
return s.Details.Channels[i].ID, nil
}
}
return "", errors.New("Channel not found")
}
// GetUsersInGroup returns a list of users currently in a group
func (s *Slack) GetUsersInGroup(group string) []string {
for i := range s.Details.Groups {
if s.Details.Groups[i].Name == group {
return s.Details.Groups[i].Members
}
}
return nil
}
// NewConnection connects the bot to a slack workgroup using a verification
// token and a channel
func (s *Slack) NewConnection() error {
err := s.SendHTTPGetRequestSlack(s.BuildURL(s.VerificationToken), true, &s.Details)
if err != nil {
return err
}
if !s.Details.Ok {
return errors.New(s.Details.Error)
}
if s.Verbose {
log.Printf("%s [%s] connected to %s [%s] \nWebsocket URL: %s.\n",
s.Details.Self.Name,
s.Details.Self.ID,
s.Details.Team.Domain,
s.Details.Team.ID,
s.Details.URL)
log.Printf("Slack channels: %s", s.GetChannelsString())
}
s.TargetChannelID, err = s.GetIDByName(s.TargetChannel)
if err != nil {
return err
}
return s.WebsocketConnect()
}
// WebsocketConnect creates a websocket dialer amd initiates a websocket
// connection
func (s *Slack) WebsocketConnect() error {
var Dialer websocket.Dialer
var err error
websocketURL := s.Details.URL
if s.ReconnectURL != "" {
websocketURL = s.ReconnectURL
}
s.WebsocketConn, _, err = Dialer.Dial(websocketURL, http.Header{})
if err != nil {
return err
}
go s.WebsocketReader()
return nil
}
// WebsocketReader reads incoming events from the websocket connection
func (s *Slack) WebsocketReader() {
for {
_, resp, err := s.WebsocketConn.ReadMessage()
if err != nil {
log.Fatal(err)
}
var data WebsocketResponse
err = common.JSONDecode(resp, &data)
if err != nil {
log.Println(err)
continue
}
switch data.Type {
case "error":
if data.Error.Msg == "Socket URL has expired" {
if s.Verbose {
log.Println("Slack websocket URL has expired.. Reconnecting")
}
if err = s.WebsocketConn.Close(); err != nil {
log.Println(err)
}
s.ReconnectURL = ""
s.Connected = false
if err := s.NewConnection(); err != nil {
log.Fatal(err)
}
return
}
case "hello":
if s.Verbose {
log.Println("Websocket connected successfully.")
}
s.Connected = true
go s.WebsocketKeepAlive()
case "reconnect_url":
type reconnectResponse struct {
URL string `json:"url"`
}
var recURL reconnectResponse
err = common.JSONDecode(resp, &recURL)
if err != nil {
continue
}
s.ReconnectURL = recURL.URL
if s.Verbose {
log.Printf("Reconnect URL set to %s\n", s.ReconnectURL)
}
case "presence_change":
var pres PresenceChange
err = common.JSONDecode(resp, &pres)
if err != nil {
continue
}
if s.Verbose {
log.Printf("Presence change. User %s [%s] changed status to %s\n",
s.GetUsernameByID(pres.User),
pres.User, pres.Presence)
}
case "message":
if data.ReplyTo != 0 {
continue
}
var msg Message
err = common.JSONDecode(resp, &msg)
if err != nil {
continue
}
if s.Verbose {
log.Printf("Msg received by %s [%s] with text: %s\n",
s.GetUsernameByID(msg.User),
msg.User, msg.Text)
}
if string(msg.Text[0]) == "!" {
s.HandleMessage(msg)
}
case "pong":
if s.Verbose {
log.Println("Pong recieved from server")
}
default:
log.Println(string(resp))
}
}
}
// WebsocketKeepAlive sends a ping every 5 minutes to keep connection alive
func (s *Slack) WebsocketKeepAlive() {
ticker := time.NewTicker(5 * time.Minute)
for {
<-ticker.C
if err := s.WebsocketSend("ping", ""); err != nil {
log.Println("slack WebsocketKeepAlive() error", err)
}
}
}
// WebsocketSend sends a message via the websocket connection
func (s *Slack) WebsocketSend(eventType, text string) error {
s.Lock()
defer s.Unlock()
newMessage := SendMessage{
ID: time.Now().Unix(),
Type: eventType,
Channel: s.TargetChannelID,
Text: text,
}
data, err := json.Marshal(newMessage)
if err != nil {
return err
}
return s.WebsocketConn.WriteMessage(websocket.TextMessage, data)
}
// HandleMessage handles incoming messages and/or commands from slack
func (s Slack) HandleMessage(msg Message) {
switch {
case common.StringContains(msg.Text, cmdStatus):
s.WebsocketSend("message", s.GetStatus())
case common.StringContains(msg.Text, cmdHelp):
s.WebsocketSend("message", getHelp)
case common.StringContains(msg.Text, cmdTicker):
s.WebsocketSend("message", s.GetTicker("ANX"))
case common.StringContains(msg.Text, cmdOrderbook):
s.WebsocketSend("message", s.GetOrderbook("ANX"))
case common.StringContains(msg.Text, cmdSettings):
s.WebsocketSend("message", s.GetSettings())
case common.StringContains(msg.Text, cmdPortfolio):
s.WebsocketSend("message", s.GetPortfolio())
default:
s.WebsocketSend("message", "GoCryptoTrader SlackBot - Command Unknown!")
}
}
// SendHTTPGetRequestSlack sends a HTTP request
func (s *Slack) SendHTTPGetRequestSlack(url string, jsonDecode bool, result interface{}) error {
res, err := http.Get(url)
if err != nil {
return err
}
httpCode := res.StatusCode
if httpCode != 200 && httpCode != 400 {
log.Printf("HTTP status code: %d\n", httpCode)
return errors.New("status code was not 200")
}
contents, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
defer res.Body.Close()
if jsonDecode {
return common.JSONDecode(contents, &result)
}
result = string(contents)
return nil
}

View File

@@ -0,0 +1,90 @@
package slack
import (
"testing"
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/config"
)
const (
verificationToken = ""
)
var s Slack
func TestSetup(t *testing.T) {
cfg := config.GetConfig()
cfg.LoadConfig(config.ConfigTestFile)
s.Setup(cfg.GetCommunicationsConfig())
}
func TestConnect(t *testing.T) {
err := s.Connect()
if err == nil {
t.Error("test failed - slack Connect() error")
}
}
func TestPushEvent(t *testing.T) {
err := s.PushEvent(base.Event{})
if err == nil {
t.Error("test failed - slack PushEvent() error")
}
}
func TestBuildURL(t *testing.T) {
v := s.BuildURL("lol123")
if v != "https://slack.com/api/rtm.start?token=lol123" {
t.Error("test failed - slack BuildURL() error")
}
}
func TestGetChannelsString(t *testing.T) {
chans := s.GetChannelsString()
if len(chans) != 0 {
t.Error("test failed - slack GetChannelsString() error")
}
}
func TestGetUsernameByID(t *testing.T) {
username := s.GetUsernameByID("1337")
if len(username) != 0 {
t.Error("test failed - slack GetUsernameByID() error")
}
}
func TestGetIDByName(t *testing.T) {
id, err := s.GetIDByName("batman")
if err == nil {
t.Error("test failed - slack GetIDByName() error")
}
if len(id) != 0 {
t.Error("test failed - slack GetIDByName() error")
}
}
func TestGetChannelIDByName(t *testing.T) {
id, err := s.GetChannelIDByName("1337")
if err == nil {
t.Error("test failed - slack GetChannelIDByName() error")
}
if len(id) != 0 {
t.Error("test failed - slack GetChannelIDByName() error")
}
}
func TestGetUsersInGroup(t *testing.T) {
username := s.GetUsersInGroup("supergroup")
if len(username) != 0 {
t.Error("test failed - slack GetUsersInGroup() error")
}
}
func TestNewConnection(t *testing.T) {
err := s.NewConnection()
if err == nil {
t.Error("test failed - slack NewConnection() error")
}
}

View File

@@ -0,0 +1,441 @@
package slack
// WebsocketResponse holds websocket response data
type WebsocketResponse struct {
Type string `json:"type"`
ReplyTo int `json:"reply_to"`
Error struct {
Msg string `json:"msg"`
Code int `json:"code"`
} `json:"error"`
}
// SendMessage holds details for message information
type SendMessage struct {
ID int64 `json:"id"`
Type string `json:"type"`
Channel string `json:"channel"`
Text string `json:"text"`
}
// Message is a response type handling message data
type Message struct {
Channel string `json:"channel"`
User string `json:"user"`
Text string `json:"text"`
SourceTeam string `json:"source_team"`
Timestamp float64 `json:"ts,string"`
Team string `json:"team"`
}
// PresenceChange holds user presence data
type PresenceChange struct {
Presence string `json:"presence"`
User string `json:"user"`
}
// Response is a generalised response type
type Response struct {
Bots []struct {
AppID string `json:"app_id"`
Deleted bool `json:"deleted"`
Icons struct {
Image36 string `json:"image_36"`
Image48 string `json:"image_48"`
Image72 string `json:"image_72"`
} `json:"icons"`
ID string `json:"id"`
Name string `json:"name"`
Updated int `json:"updated"`
} `json:"bots"`
CacheTs int `json:"cache_ts"`
CacheTsVersion string `json:"cache_ts_version"`
CacheVersion string `json:"cache_version"`
CanManageSharedChannels bool `json:"can_manage_shared_channels"`
Channels []struct {
Created int `json:"created"`
Creator string `json:"creator"`
HasPins bool `json:"has_pins"`
ID string `json:"id"`
IsArchived bool `json:"is_archived"`
IsChannel bool `json:"is_channel"`
IsGeneral bool `json:"is_general"`
IsMember bool `json:"is_member"`
IsOrgShared bool `json:"is_org_shared"`
IsShared bool `json:"is_shared"`
Name string `json:"name"`
NameNormalized string `json:"name_normalized"`
PreviousNames []string `json:"previous_names"`
} `json:"channels"`
Dnd struct {
DndEnabled bool `json:"dnd_enabled"`
NextDndEndTs int `json:"next_dnd_end_ts"`
NextDndStartTs int `json:"next_dnd_start_ts"`
SnoozeEnabled bool `json:"snooze_enabled"`
} `json:"dnd"`
Groups []struct {
ID string `json:"id"`
Name string `json:"name"`
IsGroup bool `json:"is_group"`
Created int64 `json:"created"`
Creator string `json:"creator"`
IsArchived bool `json:"is_archived"`
NameNormalised string `json:"name_normalised"`
IsMPIM bool `json:"is_mpim"`
HasPins bool `json:"has_pins"`
IsOpen bool `json:"is_open"`
LastRead string `json:"last_read"`
Members []string `json:"members"`
Topic struct {
Value string `json:"string"`
Creator string `json:"string"`
LastSet int64 `json:"last_set"`
} `json:"topic"`
Purpose struct {
Value string `json:"string"`
Creator string `json:"string"`
LastSet int64 `json:"last_set"`
} `json:"purpose"`
} `json:"groups"`
Ims []struct {
Created int `json:"created"`
HasPins bool `json:"has_pins"`
ID string `json:"id"`
IsIm bool `json:"is_im"`
IsOpen bool `json:"is_open"`
IsOrgShared bool `json:"is_org_shared"`
LastRead string `json:"last_read"`
User string `json:"user"`
} `json:"ims"`
LatestEventTs string `json:"latest_event_ts"`
Ok bool `json:"ok"`
Error string `json:"error"`
ReadOnlyChannels []interface{} `json:"read_only_channels"`
Self struct {
Created int `json:"created"`
ID string `json:"id"`
ManualPresence string `json:"manual_presence"`
Name string `json:"name"`
Prefs struct {
A11yAnimations bool `json:"a11y_animations"`
A11yFontSize string `json:"a11y_font_size"`
AllChannelsLoud bool `json:"all_channels_loud"`
AllNotificationsPrefs string `json:"all_notifications_prefs"`
AllUnreadsSortOrder string `json:"all_unreads_sort_order"`
AllowCallsToSetCurrentStatus bool `json:"allow_calls_to_set_current_status"`
AnalyticsUpsellCoachmarkSeen bool `json:"analytics_upsell_coachmark_seen"`
ArrowHistory bool `json:"arrow_history"`
AtChannelSuppressedChannels string `json:"at_channel_suppressed_channels"`
BoxEnabled bool `json:"box_enabled"`
ChannelSort string `json:"channel_sort"`
ClientLogsPri string `json:"client_logs_pri"`
ColorNamesInList bool `json:"color_names_in_list"`
ConfirmClearAllUnreads bool `json:"confirm_clear_all_unreads"`
ConfirmShCallStart bool `json:"confirm_sh_call_start"`
ConfirmUserMarkedAway bool `json:"confirm_user_marked_away"`
ConvertEmoticons bool `json:"convert_emoticons"`
DisplayDisplayNames bool `json:"display_display_names"`
DisplayRealNamesOverride int `json:"display_real_names_override"`
DndEnabled bool `json:"dnd_enabled"`
DndEndHour string `json:"dnd_end_hour"`
DndStartHour string `json:"dnd_start_hour"`
DropboxEnabled bool `json:"dropbox_enabled"`
EmailAlerts string `json:"email_alerts"`
EmailAlertsSleepUntil int `json:"email_alerts_sleep_until"`
EmailMisc bool `json:"email_misc"`
EmailWeekly bool `json:"email_weekly"`
EmojiAutocompleteBig bool `json:"emoji_autocomplete_big"`
EmojiMode string `json:"emoji_mode"`
EmojiUse string `json:"emoji_use"`
EnableReactEmojiPicker bool `json:"enable_react_emoji_picker"`
EnableUnreadView bool `json:"enable_unread_view"`
EnhancedDebugging bool `json:"enhanced_debugging"`
EnterIsSpecialInTbt bool `json:"enter_is_special_in_tbt"`
EnterpriseMdmCustomMsg string `json:"enterprise_mdm_custom_msg"`
EnterpriseMigrationSeen bool `json:"enterprise_migration_seen"`
ExpandInlineImgs bool `json:"expand_inline_imgs"`
ExpandInternalInlineImgs bool `json:"expand_internal_inline_imgs"`
ExpandNonMediaAttachments bool `json:"expand_non_media_attachments"`
ExpandSnippets bool `json:"expand_snippets"`
FKeySearch bool `json:"f_key_search"`
FlannelServerPool string `json:"flannel_server_pool"`
FrecencyEntJumper string `json:"frecency_ent_jumper"`
FrecencyJumper string `json:"frecency_jumper"`
FullTextExtracts bool `json:"full_text_extracts"`
FullerTimestamps bool `json:"fuller_timestamps"`
GdriveAuthed bool `json:"gdrive_authed"`
GdriveEnabled bool `json:"gdrive_enabled"`
GraphicEmoticons bool `json:"graphic_emoticons"`
GrowlsEnabled bool `json:"growls_enabled"`
GrowthMsgLimitApproachingCtaCount int `json:"growth_msg_limit_approaching_cta_count"`
GrowthMsgLimitApproachingCtaTs int `json:"growth_msg_limit_approaching_cta_ts"`
GrowthMsgLimitLongReachedCtaCount int `json:"growth_msg_limit_long_reached_cta_count"`
GrowthMsgLimitLongReachedCtaLastTs int `json:"growth_msg_limit_long_reached_cta_last_ts"`
GrowthMsgLimitReachedCtaCount int `json:"growth_msg_limit_reached_cta_count"`
GrowthMsgLimitReachedCtaLastTs int `json:"growth_msg_limit_reached_cta_last_ts"`
HasCreatedChannel bool `json:"has_created_channel"`
HasInvited bool `json:"has_invited"`
HasSearched bool `json:"has_searched"`
HasUploaded bool `json:"has_uploaded"`
HideHexSwatch bool `json:"hide_hex_swatch"`
HideUserGroupInfoPane bool `json:"hide_user_group_info_pane"`
HighlightWords string `json:"highlight_words"`
IntroToAppsMessageSeen bool `json:"intro_to_apps_message_seen"`
Jumbomoji bool `json:"jumbomoji"`
KKeyOmnibox bool `json:"k_key_omnibox"`
KKeyOmniboxAutoHideCount int `json:"k_key_omnibox_auto_hide_count"`
LastSeenAtChannelWarning int `json:"last_seen_at_channel_warning"`
LastSnippetType string `json:"last_snippet_type"`
LastTosAcknowledged interface{} `json:"last_tos_acknowledged"`
LoadLato2 bool `json:"load_lato_2"`
Locale string `json:"locale"`
LoudChannels string `json:"loud_channels"`
LoudChannelsSet string `json:"loud_channels_set"`
LsDisabled bool `json:"ls_disabled"`
MacSsbBounce string `json:"mac_ssb_bounce"`
MacSsbBullet bool `json:"mac_ssb_bullet"`
MarkMsgsReadImmediately bool `json:"mark_msgs_read_immediately"`
MeasureCSSUsage bool `json:"measure_css_usage"`
MentionsExcludeAtChannels bool `json:"mentions_exclude_at_channels"`
MentionsExcludeAtUserGroups bool `json:"mentions_exclude_at_user_groups"`
MessagesTheme string `json:"messages_theme"`
MsgPreview bool `json:"msg_preview"`
MsgPreviewPersistent bool `json:"msg_preview_persistent"`
MuteSounds bool `json:"mute_sounds"`
MutedChannels string `json:"muted_channels"`
NeverChannels string `json:"never_channels"`
NewMsgSnd string `json:"new_msg_snd"`
NewxpSeenLastMessage int `json:"newxp_seen_last_message"`
NoCreatedOverlays bool `json:"no_created_overlays"`
NoInvitesWidgetInSidebar bool `json:"no_invites_widget_in_sidebar"`
NoJoinedOverlays bool `json:"no_joined_overlays"`
NoMacelectronBanner bool `json:"no_macelectron_banner"`
NoMacssb1Banner bool `json:"no_macssb1_banner"`
NoMacssb2Banner bool `json:"no_macssb2_banner"`
NoOmniboxInChannels bool `json:"no_omnibox_in_channels"`
NoTextInNotifications bool `json:"no_text_in_notifications"`
NoWinssb1Banner bool `json:"no_winssb1_banner"`
ObeyInlineImgLimit bool `json:"obey_inline_img_limit"`
OnboardingCancelled bool `json:"onboarding_cancelled"`
OnboardingSlackbotConversationStep int `json:"onboarding_slackbot_conversation_step"`
OverloadedMessageEnabled bool `json:"overloaded_message_enabled"`
PagekeysHandled bool `json:"pagekeys_handled"`
PostsFormattingGuide bool `json:"posts_formatting_guide"`
PreferredSkinTone string `json:"preferred_skin_tone"`
PrevNextBtn bool `json:"prev_next_btn"`
PrivacyPolicySeen bool `json:"privacy_policy_seen"`
PromptedForEmailDisabling bool `json:"prompted_for_email_disabling"`
PushAtChannelSuppressedChannels string `json:"push_at_channel_suppressed_channels"`
PushDmAlert bool `json:"push_dm_alert"`
PushEverything bool `json:"push_everything"`
PushIdleWait int `json:"push_idle_wait"`
PushLoudChannels string `json:"push_loud_channels"`
PushLoudChannelsSet string `json:"push_loud_channels_set"`
PushMentionAlert bool `json:"push_mention_alert"`
PushMentionChannels string `json:"push_mention_channels"`
PushShowPreview bool `json:"push_show_preview"`
PushSound string `json:"push_sound"`
QuestsEnabled bool `json:"quests_enabled"`
RequireAt bool `json:"require_at"`
SearchExcludeBots bool `json:"search_exclude_bots"`
SearchExcludeChannels string `json:"search_exclude_channels"`
SearchOnlyCurrentTeam bool `json:"search_only_current_team"`
SearchOnlyMyChannels bool `json:"search_only_my_channels"`
SearchSort string `json:"search_sort"`
SeenAppSpaceCoachmark bool `json:"seen_app_space_coachmark"`
SeenAppSpaceTutorial bool `json:"seen_app_space_tutorial"`
SeenCallsSsMainCoachmark bool `json:"seen_calls_ss_main_coachmark"`
SeenCallsSsWindowCoachmark bool `json:"seen_calls_ss_window_coachmark"`
SeenCallsVideoBetaCoachmark bool `json:"seen_calls_video_beta_coachmark"`
SeenCallsVideoGaCoachmark bool `json:"seen_calls_video_ga_coachmark"`
SeenCustomStatusBadge bool `json:"seen_custom_status_badge"`
SeenCustomStatusCallout bool `json:"seen_custom_status_callout"`
SeenDomainInviteReminder bool `json:"seen_domain_invite_reminder"`
SeenGdriveCoachmark bool `json:"seen_gdrive_coachmark"`
SeenGuestAdminSlackbotAnnouncement bool `json:"seen_guest_admin_slackbot_announcement"`
SeenHighlightsArrowsCoachmark bool `json:"seen_highlights_arrows_coachmark"`
SeenHighlightsCoachmark bool `json:"seen_highlights_coachmark"`
SeenHighlightsWarmWelcome bool `json:"seen_highlights_warm_welcome"`
SeenIntlChannelNamesCoachmark bool `json:"seen_intl_channel_names_coachmark"`
SeenMemberInviteReminder bool `json:"seen_member_invite_reminder"`
SeenOnboardingChannels bool `json:"seen_onboarding_channels"`
SeenOnboardingDirectMessages bool `json:"seen_onboarding_direct_messages"`
SeenOnboardingInvites bool `json:"seen_onboarding_invites"`
SeenOnboardingPrivateGroups bool `json:"seen_onboarding_private_groups"`
SeenOnboardingRecentMentions bool `json:"seen_onboarding_recent_mentions"`
SeenOnboardingSearch bool `json:"seen_onboarding_search"`
SeenOnboardingSlackbotConversation bool `json:"seen_onboarding_slackbot_conversation"`
SeenOnboardingStarredItems bool `json:"seen_onboarding_starred_items"`
SeenOnboardingStart bool `json:"seen_onboarding_start"`
SeenRepliesCoachmark bool `json:"seen_replies_coachmark"`
SeenSingleEmojiMsg bool `json:"seen_single_emoji_msg"`
SeenSsbPrompt bool `json:"seen_ssb_prompt"`
SeenThreadsNotificationBanner bool `json:"seen_threads_notification_banner"`
SeenUnreadViewCoachmark bool `json:"seen_unread_view_coachmark"`
SeenWelcome2 bool `json:"seen_welcome_2"`
SeparatePrivateChannels bool `json:"separate_private_channels"`
SeparateSharedChannels bool `json:"separate_shared_channels"`
ShowAllSkinTones bool `json:"show_all_skin_tones"`
ShowJumperScores bool `json:"show_jumper_scores"`
ShowMemoryInstrument bool `json:"show_memory_instrument"`
ShowTyping bool `json:"show_typing"`
SidebarBehavior string `json:"sidebar_behavior"`
SidebarTheme string `json:"sidebar_theme"`
SidebarThemeCustomValues string `json:"sidebar_theme_custom_values"`
SnippetEditorWrapLongLines bool `json:"snippet_editor_wrap_long_lines"`
SpacesNewXpBannerDismissed bool `json:"spaces_new_xp_banner_dismissed"`
SsEmojis bool `json:"ss_emojis"`
SsbSpaceWindow string `json:"ssb_space_window"`
StartScrollAtOldest bool `json:"start_scroll_at_oldest"`
TabUIReturnSelects bool `json:"tab_ui_return_selects"`
ThreadsEverything bool `json:"threads_everything"`
Time24 bool `json:"time24"`
TwoFactorAuthEnabled bool `json:"two_factor_auth_enabled"`
TwoFactorBackupType interface{} `json:"two_factor_backup_type"`
TwoFactorType interface{} `json:"two_factor_type"`
Tz interface{} `json:"tz"`
UseReactSidebar bool `json:"use_react_sidebar"`
UserColors string `json:"user_colors"`
WebappSpellcheck bool `json:"webapp_spellcheck"`
WelcomeMessageHidden bool `json:"welcome_message_hidden"`
WhatsNewRead int `json:"whats_new_read"`
WinssbRunFromTray bool `json:"winssb_run_from_tray"`
WinssbWindowFlashBehavior string `json:"winssb_window_flash_behavior"`
} `json:"prefs"`
} `json:"self"`
Subteams struct {
All []interface{} `json:"all"`
Self []interface{} `json:"self"`
} `json:"subteams"`
Team struct {
ApproachingMsgLimit bool `json:"approaching_msg_limit"`
AvatarBaseURL string `json:"avatar_base_url"`
Domain string `json:"domain"`
EmailDomain string `json:"email_domain"`
Icon struct {
Image102 string `json:"image_102"`
Image132 string `json:"image_132"`
Image230 string `json:"image_230"`
Image34 string `json:"image_34"`
Image44 string `json:"image_44"`
Image68 string `json:"image_68"`
Image88 string `json:"image_88"`
ImageOriginal string `json:"image_original"`
} `json:"icon"`
ID string `json:"id"`
MessagesCount int `json:"messages_count"`
MsgEditWindowMins int `json:"msg_edit_window_mins"`
Name string `json:"name"`
OverIntegrationsLimit bool `json:"over_integrations_limit"`
OverStorageLimit bool `json:"over_storage_limit"`
Plan string `json:"plan"`
Prefs struct {
AllowCalls bool `json:"allow_calls"`
AllowMessageDeletion bool `json:"allow_message_deletion"`
AllowRetentionOverride bool `json:"allow_retention_override"`
AllowSharedChannelPermsOverride bool `json:"allow_shared_channel_perms_override"`
AuthMode string `json:"auth_mode"`
CallingAppName string `json:"calling_app_name"`
ChannelHandyRxns interface{} `json:"channel_handy_rxns"`
ComplianceExportStart int `json:"compliance_export_start"`
CustomStatusDefaultEmoji string `json:"custom_status_default_emoji"`
CustomStatusPresets [][]string `json:"custom_status_presets"`
DefaultChannels []string `json:"default_channels"`
DefaultRxns []string `json:"default_rxns"`
DisableFileDeleting bool `json:"disable_file_deleting"`
DisableFileEditing bool `json:"disable_file_editing"`
DisableFileUploads string `json:"disable_file_uploads"`
DisallowPublicFileUrls bool `json:"disallow_public_file_urls"`
Discoverable string `json:"discoverable"`
DisplayEmailAddresses bool `json:"display_email_addresses"`
DisplayRealNames bool `json:"display_real_names"`
DmRetentionDuration int `json:"dm_retention_duration"`
DmRetentionType int `json:"dm_retention_type"`
DndEnabled bool `json:"dnd_enabled"`
DndEndHour string `json:"dnd_end_hour"`
DndStartHour string `json:"dnd_start_hour"`
EnterpriseDefaultChannels []interface{} `json:"enterprise_default_channels"`
EnterpriseMandatoryChannels []interface{} `json:"enterprise_mandatory_channels"`
EnterpriseMdmDateEnabled int `json:"enterprise_mdm_date_enabled"`
EnterpriseMdmLevel int `json:"enterprise_mdm_level"`
EnterpriseTeamCreationRequest struct {
IsEnabled bool `json:"is_enabled"`
} `json:"enterprise_team_creation_request"`
FileRetentionDuration int `json:"file_retention_duration"`
FileRetentionType int `json:"file_retention_type"`
GdriveEnabledTeam bool `json:"gdrive_enabled_team"`
GroupRetentionDuration int `json:"group_retention_duration"`
GroupRetentionType int `json:"group_retention_type"`
HideReferers bool `json:"hide_referers"`
InvitesLimit bool `json:"invites_limit"`
InvitesOnlyAdmins bool `json:"invites_only_admins"`
LimitReachedTs int `json:"limit_reached_ts"`
Locale string `json:"locale"`
LoudChannelMentionsLimit int `json:"loud_channel_mentions_limit"`
MsgEditWindowMins int `json:"msg_edit_window_mins"`
RequireAtForMention bool `json:"require_at_for_mention"`
RetentionDuration int `json:"retention_duration"`
RetentionType int `json:"retention_type"`
ShowJoinLeave bool `json:"show_join_leave"`
TeamHandyRxns struct {
List []struct {
Name string `json:"name"`
Title string `json:"title"`
} `json:"list"`
Restrict bool `json:"restrict"`
} `json:"team_handy_rxns"`
UsesCustomizedCustomStatusPresets bool `json:"uses_customized_custom_status_presets"`
WarnBeforeAtChannel string `json:"warn_before_at_channel"`
WhoCanArchiveChannels string `json:"who_can_archive_channels"`
WhoCanAtChannel string `json:"who_can_at_channel"`
WhoCanAtEveryone string `json:"who_can_at_everyone"`
WhoCanChangeTeamProfile string `json:"who_can_change_team_profile"`
WhoCanCreateChannels string `json:"who_can_create_channels"`
WhoCanCreateDeleteUserGroups string `json:"who_can_create_delete_user_groups"`
WhoCanCreateGroups string `json:"who_can_create_groups"`
WhoCanCreateSharedChannels string `json:"who_can_create_shared_channels"`
WhoCanEditUserGroups string `json:"who_can_edit_user_groups"`
WhoCanKickChannels string `json:"who_can_kick_channels"`
WhoCanKickGroups string `json:"who_can_kick_groups"`
WhoCanManageGuests struct {
Type []string `json:"type"`
} `json:"who_can_manage_guests"`
WhoCanManageIntegrations struct {
Type []string `json:"type"`
} `json:"who_can_manage_integrations"`
WhoCanManageSharedChannels struct {
Type []string `json:"type"`
} `json:"who_can_manage_shared_channels"`
WhoCanPostGeneral string `json:"who_can_post_general"`
WhoCanPostInSharedChannels struct {
Type []string `json:"type"`
} `json:"who_can_post_in_shared_channels"`
WhoHasTeamVisibility string `json:"who_has_team_visibility"`
} `json:"prefs"`
} `json:"team"`
URL string `json:"url"`
Users []struct {
Deleted bool `json:"deleted"`
ID string `json:"id"`
IsBot bool `json:"is_bot"`
Name string `json:"name"`
Presence string `json:"presence"`
Profile struct {
AvatarHash string `json:"avatar_hash"`
Email string `json:"email"`
Fields interface{} `json:"fields"`
FirstName string `json:"first_name"`
Image192 string `json:"image_192"`
Image24 string `json:"image_24"`
Image32 string `json:"image_32"`
Image48 string `json:"image_48"`
Image512 string `json:"image_512"`
Image72 string `json:"image_72"`
LastName string `json:"last_name"`
RealName string `json:"real_name"`
RealNameNormalized string `json:"real_name_normalized"`
} `json:"profile"`
TeamID string `json:"team_id"`
Updated int `json:"updated"`
} `json:"users"`
}

View File

@@ -0,0 +1,179 @@
// Package smsglobal allows bulk messaging to a desired recipient list
package smsglobal
import (
"errors"
"flag"
"net/url"
"strings"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/config"
)
const (
smsGlobalAPIURL = "https://www.smsglobal.com/http-api.php"
)
var (
errSMSNotSent = errors.New("SMSGlobal message not sent")
errContactNotFound = errors.New("SMSGlobal error contact not found")
)
// SMSGlobal is the overarching type across this package
type SMSGlobal struct {
base.Base
Contacts []Contact
Username string
Password string
SendFrom string
}
// Setup takes in a SMSGlobal configuration, sets username, password and
// and recipient list
func (s *SMSGlobal) Setup(config config.CommunicationsConfig) {
s.Name = config.SMSGlobalConfig.Name
s.Enabled = config.SMSGlobalConfig.Enabled
s.Verbose = config.SMSGlobalConfig.Verbose
s.Username = config.SMSGlobalConfig.Username
s.Password = config.SMSGlobalConfig.Password
var contacts []Contact
for x := range config.SMSGlobalConfig.Contacts {
contacts = append(contacts,
Contact{
Name: config.SMSGlobalConfig.Contacts[x].Name,
Number: config.SMSGlobalConfig.Contacts[x].Number,
Enabled: config.SMSGlobalConfig.Contacts[x].Enabled,
},
)
}
s.Contacts = contacts
}
// Connect connects to the service
func (s *SMSGlobal) Connect() error {
s.Connected = true
return nil
}
// PushEvent pushes an event to a contact list via SMS
func (s *SMSGlobal) PushEvent(base.Event) error {
return errors.New("not yet implemented")
}
// GetEnabledContacts returns how many SMS contacts are enabled in the
// contact list
func (s *SMSGlobal) GetEnabledContacts() int {
counter := 0
for x := range s.Contacts {
if s.Contacts[x].Enabled {
counter++
}
}
return counter
}
// GetContactByNumber returns a contact with supplied number
func (s *SMSGlobal) GetContactByNumber(number string) (Contact, error) {
for x := range s.Contacts {
if s.Contacts[x].Number == number {
return s.Contacts[x], nil
}
}
return Contact{}, errContactNotFound
}
// GetContactByName returns a contact with supplied name
func (s *SMSGlobal) GetContactByName(name string) (Contact, error) {
for x := range s.Contacts {
if common.StringToLower(s.Contacts[x].Name) == common.StringToLower(name) {
return s.Contacts[x], nil
}
}
return Contact{}, errContactNotFound
}
// AddContact checks to see if a contact exists and adds them if it doesn't
func (s *SMSGlobal) AddContact(contact Contact) error {
if contact.Name == "" || contact.Number == "" {
return errors.New("SMSGlobal AddContact() error - nothing to add")
}
if s.ContactExists(contact) {
return errors.New("SMSGlobal AddContact() error - contact already exists")
}
s.Contacts = append(s.Contacts, contact)
return nil
}
// ContactExists checks to see if a contact exists
func (s *SMSGlobal) ContactExists(contact Contact) bool {
for x := range s.Contacts {
if s.Contacts[x].Number == contact.Number && common.StringToLower(s.Contacts[x].Name) == common.StringToLower(contact.Name) {
return true
}
}
return false
}
// RemoveContact removes a contact if it exists
func (s *SMSGlobal) RemoveContact(contact Contact) error {
if !s.ContactExists(contact) {
return errors.New("SMSGlobal RemoveContact() error - contact does not exist")
}
for x := range s.Contacts {
if s.Contacts[x].Name == contact.Name && s.Contacts[x].Number == contact.Number {
s.Contacts = append(s.Contacts[:x], s.Contacts[x+1:]...)
return nil
}
}
return errors.New("SMSGlobal RemoveContact() error - contact already removed")
}
// SendMessageToAll sends a message to all enabled contacts in cfg
func (s *SMSGlobal) SendMessageToAll(message string) error {
for x := range s.Contacts {
if s.Contacts[x].Enabled {
err := s.SendMessage(s.Contacts[x].Number, message)
if err != nil {
return err
}
}
}
return nil
}
// SendMessage sends a message to an individual contact
func (s *SMSGlobal) SendMessage(to, message string) error {
if flag.Lookup("test.v") != nil {
return nil
}
values := url.Values{}
values.Set("action", "sendsms")
values.Set("user", s.Username)
values.Set("password", s.Password)
values.Set("from", s.SendFrom)
values.Set("to", to)
values.Set("text", message)
headers := make(map[string]string)
headers["Content-Type"] = "application/x-www-form-urlencoded"
resp, err := common.SendHTTPRequest("POST",
smsGlobalAPIURL,
headers,
strings.NewReader(values.Encode()))
if err != nil {
return err
}
if !common.StringContains(resp, "OK: 0; Sent queued message") {
return errSMSNotSent
}
return nil
}

View File

@@ -0,0 +1,101 @@
package smsglobal
import (
"log"
"testing"
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/config"
)
var s SMSGlobal
func TestSetup(t *testing.T) {
cfg := config.GetConfig()
cfg.LoadConfig("../../testdata/configtest.json")
s.Setup(cfg.GetCommunicationsConfig())
}
func TestConnect(t *testing.T) {
err := s.Connect()
if err != nil {
t.Error("test failed - SMSGlobal Connect() error")
}
}
func TestPushEvent(t *testing.T) {
err := s.PushEvent(base.Event{})
if err == nil {
t.Error("test failed - SMSGlobal PushEvent() error")
}
}
func TestGetEnabledContacts(t *testing.T) {
v := s.GetEnabledContacts()
if v != 1 {
t.Error("test failed - SMSGlobal GetEnabledContacts() error")
}
}
func TestGetContactByNumber(t *testing.T) {
_, err := s.GetContactByNumber("1231424")
if err != nil {
t.Error("test failed - SMSGlobal GetContactByNumber() error", err)
}
_, err = s.GetContactByNumber("basketball")
if err == nil {
t.Error("test failed - SMSGlobal GetContactByNumber() error")
}
}
func TestGetContactByName(t *testing.T) {
_, err := s.GetContactByName("StyleGherkin")
if err != nil {
t.Error("test failed - SMSGlobal GetContactByName() error", err)
}
_, err = s.GetContactByName("blah")
if err == nil {
t.Error("test failed - SMSGlobal GetContactByName() error")
}
}
func TestAddContact(t *testing.T) {
err := s.AddContact(Contact{Name: "bra", Number: "2876", Enabled: true})
if err != nil {
t.Error("test failed - SMSGlobal AddContact() error", err)
}
log.Println(s.Contacts)
err = s.AddContact(Contact{Name: "StyleGherkin", Number: "1231424", Enabled: true})
if err == nil {
t.Error("test failed - SMSGlobal AddContact() error")
}
err = s.AddContact(Contact{Name: "", Number: "", Enabled: true})
if err == nil {
t.Error("test failed - SMSGlobal AddContact() error")
}
}
func TestRemoveContact(t *testing.T) {
err := s.RemoveContact(Contact{Name: "StyleGherkin", Number: "1231424", Enabled: true})
if err != nil {
t.Error("test failed - SMSGlobal RemoveContact() error", err)
}
err = s.RemoveContact(Contact{Name: "frieda", Number: "243453", Enabled: true})
if err == nil {
t.Error("test failed - SMSGlobal RemoveContact() error", err)
}
}
func TestSendMessageToAll(t *testing.T) {
err := s.SendMessageToAll("Hello,World!")
if err != nil {
t.Error("test failed - SMSGlobal SendMessageToAll() error", err)
}
}
func TestSendMessage(t *testing.T) {
err := s.SendMessage("1337", "Hello!")
if err != nil {
t.Error("test failed - SMSGlobal SendMessage() error", err)
}
}

View File

@@ -0,0 +1,8 @@
package smsglobal
// Contact struct stores information related to a SMSGlobal contact
type Contact struct {
Name string `json:"Name"`
Number string `json:"Number"`
Enabled bool `json:"Enabled"`
}

View File

@@ -0,0 +1,80 @@
package smtpservice
import (
"errors"
"fmt"
"net/smtp"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/config"
)
const (
mime = "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
msgSMTP = "To: %s\r\nSubject: %s\r\n%s\r\n%s"
)
// SMTPservice uses the net/smtp package to send emails to a recipient list
type SMTPservice struct {
base.Base
Host string
Port string
AccountName string
AccountPassword string
RecipientList string
}
// Setup takes in a SMTP configuration and sets SMTP server details and
// recipient list
func (s *SMTPservice) Setup(config config.CommunicationsConfig) {
s.Name = config.SMTPConfig.Name
s.Enabled = config.SMTPConfig.Enabled
s.Verbose = config.SMTPConfig.Verbose
s.Host = config.SMTPConfig.Host
s.Port = config.SMTPConfig.Port
s.AccountName = config.SMTPConfig.AccountName
s.AccountPassword = config.SMTPConfig.AccountPassword
s.RecipientList = config.SMTPConfig.RecipientList
}
// Connect connects to service
func (s *SMTPservice) Connect() error {
s.Connected = true
return nil
}
// PushEvent sends an event to supplied recipient list via SMTP
func (s *SMTPservice) PushEvent(base.Event) error {
return errors.New("not yet implemented")
}
// Send sends an email template to the recipient list via your SMTP host when
// an internal event is triggered by GoCryptoTrader
func (s *SMTPservice) Send(subject, alert string) error {
if subject == "" || alert == "" {
return errors.New("STMPservice Send() please add subject and alert")
}
list := common.SplitStrings(s.RecipientList, ",")
for i := range list {
messageToSend := fmt.Sprintf(
msgSMTP,
list[i],
subject,
mime,
alert)
err := smtp.SendMail(
s.Host+":"+s.Port,
smtp.PlainAuth("", s.AccountName, s.AccountPassword, s.Host),
s.AccountName,
[]string{list[i]},
[]byte(messageToSend))
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,41 @@
package smtpservice
import (
"testing"
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/config"
)
var s SMTPservice
func TestSetup(t *testing.T) {
cfg := config.GetConfig()
cfg.LoadConfig("../../testdata/configtest.json")
s.Setup(cfg.GetCommunicationsConfig())
}
func TestConnect(t *testing.T) {
err := s.Connect()
if err != nil {
t.Error("test failed - smtpservice Connect() error", err)
}
}
func TestPushEvent(t *testing.T) {
err := s.PushEvent(base.Event{})
if err == nil {
t.Error("test failed - smtpservice PushEvent() error", err)
}
}
func TestSend(t *testing.T) {
err := s.Send("", "")
if err == nil {
t.Error("test failed - smtpservice Send() error", err)
}
err = s.Send("subject", "alertmessage")
if err == nil {
t.Error("test failed - smtpservice Send() error", err)
}
}

View File

@@ -0,0 +1,230 @@
// Package telegram is used to connect to a cloud-based mobile and desktop
// messaging app using the bot API defined in
// https://core.telegram.org/bots/api#recent-changes
package telegram
import (
"bytes"
"errors"
"fmt"
"log"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/config"
)
const (
apiURL = "https://api.telegram.org/bot%s/%s"
methodGetMe = "getMe"
methodGetUpdates = "getUpdates"
methodSendMessage = "sendMessage"
cmdStart = "/start"
cmdStatus = "/status"
cmdHelp = "/help"
cmdSettings = "/settings"
cmdTicker = "/ticker"
cmdPortfolio = "/portfolio"
cmdOrders = "/orderbooks"
cmdHelpReply = `GoCryptoTrader TelegramBot, thank you for using this service!
Current commands are:
/start - Will authenticate your ID
/status - Displays the status of the bot
/help - Displays current command list
/settings - Displays current bot settings
/ticker - Displays current ANX ticker data
/portfolio - Displays your current portfolio
/orderbooks - Displays current orderbooks for ANX`
talkRoot = "GoCryptoTrader bot"
)
// Telegram is the overarching type across this package
type Telegram struct {
base.Base
Token string
Offset int64
AuthorisedClients []int64
}
// Setup takes in a Telegram configuration and sets verification token
func (t *Telegram) Setup(config config.CommunicationsConfig) {
t.Name = config.TelegramConfig.Name
t.Enabled = config.TelegramConfig.Enabled
t.Token = config.TelegramConfig.VerificationToken
t.Verbose = config.TelegramConfig.Verbose
}
// Connect starts an initial connection
func (t *Telegram) Connect() error {
if err := t.TestConnection(); err != nil {
return err
}
t.Connected = true
go t.PollerStart()
return nil
}
// PushEvent sends an event to a supplied recipient list via telegram
func (t *Telegram) PushEvent(event base.Event) error {
for i := range t.AuthorisedClients {
err := t.SendMessage(fmt.Sprintf("Type: %s Details: %s GainOrLoss: %s",
event.Type, event.TradeDetails, event.GainLoss), t.AuthorisedClients[i])
if err != nil {
return err
}
}
return nil
}
// PollerStart starts the long polling sequence
func (t *Telegram) PollerStart() {
t.InitialConnect()
for {
resp, err := t.GetUpdates()
if err != nil {
log.Fatal(err)
}
for i := range resp.Result {
if resp.Result[i].UpdateID > t.Offset {
if string(resp.Result[i].Message.Text[0]) == "/" {
err = t.HandleMessages(resp.Result[i].Message.Text, resp.Result[i].Message.From.ID)
if err != nil {
log.Fatal(err)
}
}
t.Offset = resp.Result[i].UpdateID
}
}
}
}
// InitialConnect sets offset, and sends a welcome greeting to any associated
// IDs
func (t *Telegram) InitialConnect() {
resp, err := t.GetUpdates()
if err != nil {
log.Fatal(err)
}
if !resp.Ok {
log.Fatal(resp.Description)
}
warmWelcomeList := make(map[string]int64)
for i := range resp.Result {
if resp.Result[i].Message.From.ID != 0 {
warmWelcomeList[resp.Result[i].Message.From.UserName] = resp.Result[i].Message.From.ID
}
}
for userName, ID := range warmWelcomeList {
err = t.SendMessage(fmt.Sprintf("GoCryptoTrader bot has connected: Hello, %s!", userName), ID)
if err != nil {
log.Fatal(err)
}
}
if len(resp.Result) == 0 {
return
}
t.Offset = resp.Result[len(resp.Result)-1].UpdateID
}
// HandleMessages handles incoming message from the long polling routine
func (t *Telegram) HandleMessages(text string, chatID int64) error {
switch {
case common.StringContains(text, cmdHelp):
return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, cmdHelpReply), chatID)
case common.StringContains(text, cmdStart):
return t.SendMessage(fmt.Sprintf("%s: START COMMANDS HERE", talkRoot), chatID)
case common.StringContains(text, cmdOrders):
return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetOrderbook("ANX")), chatID)
case common.StringContains(text, cmdStatus):
return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetStatus()), chatID)
case common.StringContains(text, cmdTicker):
return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetTicker("ANX")), chatID)
case common.StringContains(text, cmdSettings):
return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetSettings()), chatID)
case common.StringContains(text, cmdPortfolio):
return t.SendMessage(fmt.Sprintf("%s: %s", talkRoot, t.GetPortfolio()), chatID)
default:
return t.SendMessage(fmt.Sprintf("command %s not recognized", text), chatID)
}
}
// GetUpdates gets new updates via a long poll connection
func (t *Telegram) GetUpdates() (GetUpdateResponse, error) {
var newUpdates GetUpdateResponse
path := fmt.Sprintf(apiURL, t.Token, methodGetUpdates)
return newUpdates, t.SendHTTPRequest(path, nil, &newUpdates)
}
// TestConnection tests bot's supplied authentication token
func (t *Telegram) TestConnection() error {
var isConnected User
path := fmt.Sprintf(apiURL, t.Token, methodGetMe)
err := t.SendHTTPRequest(path, nil, &isConnected)
if err != nil {
return err
}
if !isConnected.Ok {
return errors.New(isConnected.Description)
}
return nil
}
// SendMessage sends a message to a user by their chatID
func (t *Telegram) SendMessage(text string, chatID int64) error {
path := fmt.Sprintf(apiURL, t.Token, methodSendMessage)
messageToSend := struct {
ChatID int64 `json:"chat_id"`
Text string `json:"text"`
}{
chatID,
text,
}
json, err := common.JSONEncode(&messageToSend)
if err != nil {
return err
}
resp := Message{}
err = t.SendHTTPRequest(path, json, &resp)
if err != nil {
return err
}
if !resp.Ok {
return errors.New(resp.Description)
}
return nil
}
// SendHTTPRequest sends an authenticated HTTP request
func (t *Telegram) SendHTTPRequest(path string, json []byte, result interface{}) error {
headers := make(map[string]string)
headers["content-type"] = "application/json"
resp, err := common.SendHTTPRequest("POST", path, headers, bytes.NewBuffer(json))
if err != nil {
return err
}
return common.JSONDecode([]byte(resp), result)
}

View File

@@ -0,0 +1,30 @@
package telegram
import (
"testing"
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/config"
)
var T Telegram
func TestSetup(t *testing.T) {
cfg := config.GetConfig()
cfg.LoadConfig("../../testdata/configtest.json")
T.Setup(cfg.GetCommunicationsConfig())
}
func TestConnect(t *testing.T) {
err := T.Connect()
if err == nil {
t.Error("test failed - telegram Connect() error", err)
}
}
func PushEvent(t *testing.T) {
err := T.PushEvent(base.Event{})
if err != nil {
t.Error("test failed - telegram PushEvent() error", err)
}
}

View File

@@ -0,0 +1,94 @@
package telegram
// User holds user information
type User struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result struct {
ID int64 `json:"id"`
IsBot bool `json:"is_bot"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
UserName string `json:"username"`
LanguageCode string `json:"language_code"`
} `json:"result"`
}
// GetUpdateResponse represents an incoming update
type GetUpdateResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result []struct {
UpdateID int64 `json:"update_id"`
Message MessageType `json:"message"`
EditedMessage interface{} `json:"edited_message"`
ChannelPost interface{} `json:"channel_post"`
EditedChannelPost interface{} `json:"edited_channel_post"`
InlineQuery interface{} `json:"inline_query"`
ChosenInlineResult interface{} `json:"chosen_inline_result"`
CallbackQuery interface{} `json:"callback_query"`
ShippingQuery interface{} `json:"shipping_query"`
PreCheckoutQuery interface{} `json:"pre_checkout_query"`
} `json:"result"`
}
// Message holds the full message information
type Message struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result MessageType `json:"result"`
}
// MessageType contains message data
type MessageType struct {
MessageID int64 `json:"message_id"`
From UserType `json:"from"`
Date int64 `json:"date"`
Chat ChatType `json:"chat"`
ForwardFrom UserType `json:"forward_from"`
ForwardFromChat ChatType `json:"forward_from_chat"`
ForwardFromMessageID int64 `json:"forward_from_message_id"`
ForwardSignature string `json:"forward_signature"`
ForwardDate int64 `json:"forward_date"`
ReplyToMessage interface{} `json:"reply_to_message"`
EditDate int64 `json:"edit_date"`
MediaGroupID string `json:"media_group_id"`
AuthorSignature string `json:"author_signature"`
Text string `json:"text"`
Entities []MessageEntityType `json:"entities"`
CaptionEntities []MessageEntityType `json:"caption_entities"`
}
// MessageEntityType contains message entity information
type MessageEntityType struct {
Type string `json:"type"`
Offset int64 `json:"offset"`
Length int64 `json:"length"`
URL string `json:"url"`
User UserType `json:"user"`
}
// UserType contains user data
type UserType struct {
ID int64 `json:"id"`
IsBot bool `json:"is_bot"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
UserName string `json:"username"`
LanguageCode string `json:"language_code"`
}
// ChatType contains chat data
type ChatType struct {
ID int64 `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
UserName string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AllAdmin bool `json:"all_members_are_administrators"`
Description string `json:"description"`
InviteLink string `json:"invite_link"`
StickerSetName string `json:"sticker_set_name"`
CanSetStickerSet bool `json:"can_set_sticker_set"`
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/thrasher-/gocryptotrader/currency/forexprovider/base"
"github.com/thrasher-/gocryptotrader/currency/pair"
"github.com/thrasher-/gocryptotrader/portfolio"
"github.com/thrasher-/gocryptotrader/smsglobal"
)
// Constants declared here are filename strings and test strings
@@ -72,15 +71,6 @@ type WebserverConfig struct {
WebsocketAllowInsecureOrigin bool
}
// SMSGlobalConfig structure holds all the variables you need for instant
// messaging and broadcast used by SMSGlobal
type SMSGlobalConfig struct {
Enabled bool
Username string
Password string
Contacts []smsglobal.Contact
}
// Post holds the bot configuration data
type Post struct {
Data Config `json:"Data"`
@@ -95,19 +85,23 @@ type CurrencyPairFormatConfig struct {
}
// Config is the overarching object that holds all the information for
// prestart management of portfolio, SMSGlobal, webserver and enabled exchange
// prestart management of Portfolio, Communications, Webserver and Enabled
// Exchanges
type Config struct {
Name string
EncryptConfig int
Cryptocurrencies string `json:"Cryptocurrencies,omitempty"`
Currency CurrencyConfig `json:"CurrencyConfig,omitempty"`
Name string
EncryptConfig int
GlobalHTTPTimeout time.Duration `json:"GlobalHTTPTimeout"`
Currency CurrencyConfig `json:"CurrencyConfig"`
Communications CommunicationsConfig `json:"Communications"`
Portfolio portfolio.Base `json:"PortfolioAddresses"`
Webserver WebserverConfig `json:"Webserver"`
Exchanges []ExchangeConfig `json:"Exchanges"`
// Deprecated config settings, will be removed at a future date
CurrencyPairFormat *CurrencyPairFormatConfig `json:"CurrencyPairFormat,omitempty"`
FiatDisplayCurrency string `json:"FiatDispayCurrency,omitempty"`
GlobalHTTPTimeout time.Duration
Portfolio portfolio.Base `json:"PortfolioAddresses"`
SMS SMSGlobalConfig `json:"SMSGlobal"`
Webserver WebserverConfig `json:"Webserver"`
Exchanges []ExchangeConfig `json:"Exchanges"`
Cryptocurrencies string `json:"Cryptocurrencies,omitempty"`
SMS *SMSGlobalConfig `json:"SMSGlobal,omitempty"`
}
// ExchangeConfig holds all the information needed for each enabled Exchange.
@@ -141,11 +135,186 @@ type CurrencyConfig struct {
FiatDisplayCurrency string
}
// CommunicationsConfig holds all the information needed for each
// enabled communication package
type CommunicationsConfig struct {
SlackConfig SlackConfig `json:"Slack"`
SMSGlobalConfig SMSGlobalConfig `json:"SMSGlobal"`
SMTPConfig SMTPConfig `json:"SMTP"`
TelegramConfig TelegramConfig `json:"Telegram"`
}
// SlackConfig holds all variables to start and run the Slack package
type SlackConfig struct {
Name string `json:"Name"`
Enabled bool `json:"Enabled"`
Verbose bool `json:"Verbose"`
TargetChannel string `json:"TargetChannel"`
VerificationToken string `json:"VerificationToken"`
}
// SMSContact stores the SMS contact info
type SMSContact struct {
Name string `json:"Name"`
Number string `json:"Number"`
Enabled bool `json:"Enabled"`
}
// SMSGlobalConfig structure holds all the variables you need for instant
// messaging and broadcast used by SMSGlobal
type SMSGlobalConfig struct {
Name string `json:"Name"`
Enabled bool `json:"Enabled"`
Verbose bool `json:"Verbose"`
Username string `json:"Username"`
Password string `json:"Password"`
Contacts []SMSContact `json:"Contacts"`
}
// SMTPConfig holds all variables to start and run the SMTP package
type SMTPConfig struct {
Name string `json:"Name"`
Enabled bool `json:"Enabled"`
Verbose bool `json:"Verbose"`
Host string `json:"Host"`
Port string `json:"Port"`
AccountName string `json:"AccountName"`
AccountPassword string `json:"AccountPassword"`
RecipientList string `json:"RecipientList"`
}
// TelegramConfig holds all variables to start and run the Telegram package
type TelegramConfig struct {
Name string `json:"Name"`
Enabled bool `json:"Enabled"`
Verbose bool `json:"Verbose"`
VerificationToken string `json:"VerificationToken"`
}
// GetCurrencyConfig returns currency configurations
func (c *Config) GetCurrencyConfig() CurrencyConfig {
return c.Currency
}
// GetCommunicationsConfig returns the communications configuration
func (c *Config) GetCommunicationsConfig() CommunicationsConfig {
m.Lock()
defer m.Unlock()
return c.Communications
}
// UpdateCommunicationsConfig sets a new updated version of a Communications
// configuration
func (c *Config) UpdateCommunicationsConfig(config CommunicationsConfig) {
m.Lock()
c.Communications = config
m.Unlock()
}
// CheckCommunicationsConfig checks to see if the variables are set correctly
// from config.json
func (c *Config) CheckCommunicationsConfig() error {
m.Lock()
defer m.Unlock()
// If the communications config hasn't been populated, populate
// with example settings
if c.Communications.SlackConfig.Name == "" {
c.Communications.SlackConfig = SlackConfig{
Name: "Slack",
TargetChannel: "general",
VerificationToken: "testtest",
}
}
if c.Communications.SMSGlobalConfig.Name == "" {
if c.SMS.Contacts != nil {
c.Communications.SMSGlobalConfig = SMSGlobalConfig{
Name: "SMSGlobal",
Enabled: c.SMS.Enabled,
Verbose: c.SMS.Verbose,
Username: c.SMS.Username,
Password: c.SMS.Password,
Contacts: c.SMS.Contacts,
}
// flush old SMS config
c.SMS = nil
} else {
c.Communications.SMSGlobalConfig = SMSGlobalConfig{
Name: "SMSGlobal",
Username: "main",
Password: "test",
Contacts: []SMSContact{
SMSContact{
Name: "bob",
Number: "1234",
Enabled: false,
},
},
}
}
} else {
if c.SMS != nil {
// flush old SMS config
c.SMS = nil
}
}
if c.Communications.SMTPConfig.Name == "" {
c.Communications.SMTPConfig = SMTPConfig{
Name: "SMTP",
Host: "smtp.google.com",
Port: "537",
AccountName: "some",
AccountPassword: "password",
RecipientList: "lol123@gmail.com",
}
}
if c.Communications.TelegramConfig.Name == "" {
c.Communications.TelegramConfig = TelegramConfig{
Name: "Telegram",
VerificationToken: "testest",
}
}
if c.Communications.SlackConfig.Name != "Slack" ||
c.Communications.SMSGlobalConfig.Name != "SMSGlobal" ||
c.Communications.SMTPConfig.Name != "SMTP" ||
c.Communications.TelegramConfig.Name != "Telegram" {
return errors.New("Communications config name/s not set correctly")
}
if c.Communications.SlackConfig.Enabled {
if c.Communications.SlackConfig.TargetChannel == "" ||
c.Communications.SlackConfig.VerificationToken == "" {
return errors.New("Slack enabled in config but variable data not set")
}
}
if c.Communications.SMSGlobalConfig.Enabled {
if c.Communications.SMSGlobalConfig.Username == "" ||
c.Communications.SMSGlobalConfig.Password == "" ||
len(c.Communications.SMSGlobalConfig.Contacts) == 0 {
return errors.New("SMSGlobal enabled in config but variable data not set")
}
}
if c.Communications.SMTPConfig.Enabled {
if c.Communications.SMTPConfig.Host == "" ||
c.Communications.SMTPConfig.Port == "" ||
c.Communications.SMTPConfig.AccountName == "" ||
len(c.Communications.SMTPConfig.AccountName) == 0 {
return errors.New("SMTP enabled in config but variable data not set")
}
}
if c.Communications.TelegramConfig.Enabled {
if c.Communications.TelegramConfig.VerificationToken == "" {
return errors.New("Telegram enabled in config but variable data not set")
}
}
return nil
}
// SupportsPair returns true or not whether the exchange supports the supplied
// pair
func (c *Config) SupportsPair(exchName string, p pair.CurrencyPair) (bool, error) {
@@ -296,27 +465,6 @@ func (c *Config) UpdateExchangeConfig(e ExchangeConfig) error {
return fmt.Errorf(ErrExchangeNotFound, e.Name)
}
// CheckSMSGlobalConfigValues checks concurrent SMSGlobal configurations
func (c *Config) CheckSMSGlobalConfigValues() error {
if c.SMS.Username == "" || c.SMS.Username == "Username" || c.SMS.Password == "" || c.SMS.Password == "Password" {
return errors.New(WarningSMSGlobalDefaultOrEmptyValues)
}
contacts := 0
for i := range c.SMS.Contacts {
if c.SMS.Contacts[i].Enabled {
if c.SMS.Contacts[i].Name == "" || c.SMS.Contacts[i].Number == "" || (c.SMS.Contacts[i].Name == "Bob" && c.SMS.Contacts[i].Number == "12345") {
log.Printf(WarningSSMSGlobalSMSContactDefaultOrEmptyValues, i)
continue
}
contacts++
}
}
if contacts == 0 {
return errors.New(WarningSSMSGlobalSMSNoContacts)
}
return nil
}
// CheckExchangeConfigValues returns configuation values for all enabled
// exchanges
func (c *Config) CheckExchangeConfigValues() error {
@@ -693,12 +841,8 @@ func (c *Config) CheckConfig() error {
return fmt.Errorf(ErrCheckingConfigValues, err)
}
if c.SMS.Enabled {
err = c.CheckSMSGlobalConfigValues()
if err != nil {
log.Print(fmt.Errorf(ErrCheckingConfigValues, err))
c.SMS.Enabled = false
}
if err = c.CheckCommunicationsConfig(); err != nil {
log.Fatal(err)
}
if c.Webserver.Enabled {
@@ -744,7 +888,7 @@ func (c *Config) UpdateConfig(configPath string, newCfg Config) error {
c.Currency = newCfg.Currency
c.GlobalHTTPTimeout = newCfg.GlobalHTTPTimeout
c.Portfolio = newCfg.Portfolio
c.SMS = newCfg.SMS
c.Communications = newCfg.Communications
c.Webserver = newCfg.Webserver
c.Exchanges = newCfg.Exchanges

View File

@@ -266,47 +266,6 @@ func TestUpdateExchangeConfig(t *testing.T) {
}
}
func TestCheckSMSGlobalConfigValues(t *testing.T) {
t.Parallel()
checkSMSGlobalConfigValues := GetConfig()
err := checkSMSGlobalConfigValues.LoadConfig(ConfigTestFile)
if err != nil {
t.Errorf("Test failed. checkSMSGlobalConfigValues.LoadConfig: %s", err)
}
err = checkSMSGlobalConfigValues.CheckSMSGlobalConfigValues()
if err != nil {
t.Error(
`Test failed. checkSMSGlobalConfigValues.CheckSMSGlobalConfigValues: Incorrect Return Value`,
)
}
checkSMSGlobalConfigValues.SMS.Username = "Username"
err = checkSMSGlobalConfigValues.CheckSMSGlobalConfigValues()
if err == nil {
t.Error(
"Test failed. checkSMSGlobalConfigValues.CheckSMSGlobalConfigValues: Incorrect Return Value",
)
}
checkSMSGlobalConfigValues.SMS.Username = "1234"
checkSMSGlobalConfigValues.SMS.Contacts[0].Name = "Bob"
checkSMSGlobalConfigValues.SMS.Contacts[0].Number = "12345"
err = checkSMSGlobalConfigValues.CheckSMSGlobalConfigValues()
if err == nil {
t.Error(
"Test failed. checkSMSGlobalConfigValues.CheckSMSGlobalConfigValues: Incorrect Return Value",
)
}
checkSMSGlobalConfigValues.SMS.Contacts = checkSMSGlobalConfigValues.SMS.Contacts[:0]
err = checkSMSGlobalConfigValues.CheckSMSGlobalConfigValues()
if err == nil {
t.Error(
"Test failed. checkSMSGlobalConfigValues.CheckSMSGlobalConfigValues: Incorrect Return Value",
)
}
}
func TestCheckExchangeConfigValues(t *testing.T) {
t.Parallel()
checkExchangeConfigValues := Config{}

File diff suppressed because one or more lines are too long

View File

@@ -1,368 +1,369 @@
package events
import (
"testing"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency/pair"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
"github.com/thrasher-/gocryptotrader/smsglobal"
)
var (
loaded = false
)
func testSetup(t *testing.T) {
if !loaded {
cfg := config.GetConfig()
err := cfg.LoadConfig("")
if err != nil {
t.Fatalf("Test failed. Failed to load config %s", err)
}
smsglobal.New(cfg.SMS.Username, cfg.SMS.Password, cfg.Name, cfg.SMS.Contacts)
loaded = true
}
}
func TestAddEvent(t *testing.T) {
testSetup(t)
pair := pair.NewCurrencyPair("BTC", "USD")
eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
if err != nil && eventID != 0 {
t.Errorf("Test Failed. AddEvent: Error, %s", err)
}
eventID, err = AddEvent("ANXX", "price", ">,==", pair, "SPOT", actionTest)
if err == nil && eventID == 0 {
t.Error("Test Failed. AddEvent: Error, error not captured in Exchange")
}
eventID, err = AddEvent("ANX", "prices", ">,==", pair, "SPOT", actionTest)
if err == nil && eventID == 0 {
t.Error("Test Failed. AddEvent: Error, error not captured in Item")
}
eventID, err = AddEvent("ANX", "price", "3===D", pair, "SPOT", actionTest)
if err == nil && eventID == 0 {
t.Error("Test Failed. AddEvent: Error, error not captured in Condition")
}
eventID, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", "console_prints")
if err == nil && eventID == 0 {
t.Error("Test Failed. AddEvent: Error, error not captured in Action")
}
if !RemoveEvent(eventID) {
t.Error("Test Failed. RemoveEvent: Error, error removing event")
}
}
func TestRemoveEvent(t *testing.T) {
testSetup(t)
pair := pair.NewCurrencyPair("BTC", "USD")
eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
if err != nil && eventID != 0 {
t.Errorf("Test Failed. RemoveEvent: Error, %s", err)
}
if !RemoveEvent(eventID) {
t.Error("Test Failed. RemoveEvent: Error, error removing event")
}
if RemoveEvent(1234) {
t.Error("Test Failed. RemoveEvent: Error, error removing event")
}
}
func TestGetEventCounter(t *testing.T) {
testSetup(t)
pair := pair.NewCurrencyPair("BTC", "USD")
one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
if err != nil {
t.Errorf("Test Failed. GetEventCounter: Error, %s", err)
}
two, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
if err != nil {
t.Errorf("Test Failed. GetEventCounter: Error, %s", err)
}
three, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
if err != nil {
t.Errorf("Test Failed. GetEventCounter: Error, %s", err)
}
Events[three-1].Executed = true
total, _ := GetEventCounter()
if total <= 0 {
t.Errorf("Test Failed. GetEventCounter: Total = %d", total)
}
if !RemoveEvent(one) {
t.Error("Test Failed. GetEventCounter: Error, error removing event")
}
if !RemoveEvent(two) {
t.Error("Test Failed. GetEventCounter: Error, error removing event")
}
if !RemoveEvent(three) {
t.Error("Test Failed. GetEventCounter: Error, error removing event")
}
total2, _ := GetEventCounter()
if total2 != 0 {
t.Errorf("Test Failed. GetEventCounter: Total = %d", total2)
}
}
func TestExecuteAction(t *testing.T) {
testSetup(t)
pair := pair.NewCurrencyPair("BTC", "USD")
one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
if err != nil {
t.Fatalf("Test Failed. ExecuteAction: Error, %s", err)
}
isExecuted := Events[one].ExecuteAction()
if !isExecuted {
t.Error("Test Failed. ExecuteAction: Error, error removing event")
}
if !RemoveEvent(one) {
t.Error("Test Failed. ExecuteAction: Error, error removing event")
}
action := actionSMSNotify + "," + "ALL"
one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action)
if err != nil {
t.Fatalf("Test Failed. ExecuteAction: Error, %s", err)
}
isExecuted = Events[one].ExecuteAction()
if !isExecuted {
t.Error("Test Failed. ExecuteAction: Error, error removing event")
}
if !RemoveEvent(one) {
t.Error("Test Failed. ExecuteAction: Error, error removing event")
}
action = actionSMSNotify + "," + "StyleGherkin"
one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action)
if err != nil {
t.Fatalf("Test Failed. ExecuteAction: Error, %s", err)
}
isExecuted = Events[one].ExecuteAction()
if !isExecuted {
t.Error("Test Failed. ExecuteAction: Error, error removing event")
}
if !RemoveEvent(one) {
t.Error("Test Failed. ExecuteAction: Error, error removing event")
}
// More tests when ExecuteAction is expanded
}
func TestEventToString(t *testing.T) {
testSetup(t)
pair := pair.NewCurrencyPair("BTC", "USD")
one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
if err != nil {
t.Errorf("Test Failed. EventToString: Error, %s", err)
}
eventString := Events[one].String()
if eventString != "If the BTCUSD [SPOT] price on ANX is > == then ACTION_TEST." {
t.Error("Test Failed. EventToString: Error, incorrect return string")
}
if !RemoveEvent(one) {
t.Error("Test Failed. EventToString: Error, error removing event")
}
}
func TestCheckCondition(t *testing.T) {
testSetup(t)
// Test invalid currency pair
newPair := pair.NewCurrencyPair("A", "B")
one, err := AddEvent("ANX", "price", ">=,10", newPair, "SPOT", actionTest)
if err != nil {
t.Errorf("Test Failed. CheckCondition: Error, %s", err)
}
conditionBool := Events[one].CheckCondition()
if conditionBool {
t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
}
// Test last price == 0
var tickerNew ticker.Price
tickerNew.Last = 0
newPair = pair.NewCurrencyPair("BTC", "USD")
ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot)
Events[one].Pair = newPair
conditionBool = Events[one].CheckCondition()
if conditionBool {
t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
}
// Test last pricce > 0 and conditional logic
tickerNew.Last = 11
ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot)
Events[one].Condition = ">,10"
conditionBool = Events[one].CheckCondition()
if !conditionBool {
t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
}
// Test last price >= 10
Events[one].Condition = ">=,10"
conditionBool = Events[one].CheckCondition()
if !conditionBool {
t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
}
// Test last price <= 10
Events[one].Condition = "<,100"
conditionBool = Events[one].CheckCondition()
if !conditionBool {
t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
}
// Test last price <= 10
Events[one].Condition = "<=,100"
conditionBool = Events[one].CheckCondition()
if !conditionBool {
t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
}
Events[one].Condition = "==,11"
conditionBool = Events[one].CheckCondition()
if !conditionBool {
t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
}
Events[one].Condition = "^,11"
conditionBool = Events[one].CheckCondition()
if conditionBool {
t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
}
if !RemoveEvent(one) {
t.Error("Test Failed. CheckCondition: Error, error removing event")
}
}
func TestIsValidEvent(t *testing.T) {
testSetup(t)
err := IsValidEvent("ANX", "price", ">,==", actionTest)
if err != nil {
t.Errorf("Test Failed. IsValidEvent: %s", err)
}
err = IsValidEvent("ANX", "price", ">,", actionTest)
if err == nil {
t.Errorf("Test Failed. IsValidEvent: %s", err)
}
err = IsValidEvent("ANX", "Testy", ">,==", actionTest)
if err == nil {
t.Errorf("Test Failed. IsValidEvent: %s", err)
}
err = IsValidEvent("Testys", "price", ">,==", actionTest)
if err == nil {
t.Errorf("Test Failed. IsValidEvent: %s", err)
}
action := "blah,blah"
err = IsValidEvent("ANX", "price", ">=,10", action)
if err == nil {
t.Errorf("Test Failed. IsValidEvent: %s", err)
}
action = "SMS,blah"
err = IsValidEvent("ANX", "price", ">=,10", action)
if err == nil {
t.Errorf("Test Failed. IsValidEvent: %s", err)
}
//Function tests need to appended to this function when more actions are
//implemented
}
func TestCheckEvents(t *testing.T) {
testSetup(t)
pair := pair.NewCurrencyPair("BTC", "USD")
_, err := AddEvent("ANX", "price", ">=,10", pair, "SPOT", actionTest)
if err != nil {
t.Fatal("Test failed. TestChcheckEvents add event")
}
go CheckEvents()
}
func TestIsValidExchange(t *testing.T) {
testSetup(t)
boolean := IsValidExchange("ANX")
if !boolean {
t.Error("Test Failed. IsValidExchange: Error, incorrect Exchange")
}
boolean = IsValidExchange("OBTUSE")
if boolean {
t.Error("Test Failed. IsValidExchange: Error, incorrect return")
}
}
func TestIsValidCondition(t *testing.T) {
testSetup(t)
boolean := IsValidCondition(">")
if !boolean {
t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
}
boolean = IsValidCondition(">=")
if !boolean {
t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
}
boolean = IsValidCondition("<")
if !boolean {
t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
}
boolean = IsValidCondition("<=")
if !boolean {
t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
}
boolean = IsValidCondition("==")
if !boolean {
t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
}
boolean = IsValidCondition("**********")
if boolean {
t.Error("Test Failed. IsValidCondition: Error, incorrect return")
}
}
func TestIsValidAction(t *testing.T) {
testSetup(t)
boolean := IsValidAction("sms")
if !boolean {
t.Error("Test Failed. IsValidAction: Error, incorrect Action")
}
boolean = IsValidAction(actionTest)
if !boolean {
t.Error("Test Failed. IsValidAction: Error, incorrect Action")
}
boolean = IsValidAction("randomstring")
if boolean {
t.Error("Test Failed. IsValidAction: Error, incorrect return")
}
}
func TestIsValidItem(t *testing.T) {
testSetup(t)
boolean := IsValidItem("price")
if !boolean {
t.Error("Test Failed. IsValidItem: Error, incorrect Item")
}
boolean = IsValidItem("obtuse")
if boolean {
t.Error("Test Failed. IsValidItem: Error, incorrect return")
}
}
//
// import (
// "testing"
//
// "github.com/thrasher-/gocryptotrader/config"
// "github.com/thrasher-/gocryptotrader/currency/pair"
// "github.com/thrasher-/gocryptotrader/exchanges/ticker"
// "github.com/thrasher-/gocryptotrader/smsglobal"
// )
//
// var (
// loaded = false
// )
//
// func testSetup(t *testing.T) {
// if !loaded {
// cfg := config.GetConfig()
// err := cfg.LoadConfig("")
// if err != nil {
// t.Fatalf("Test failed. Failed to load config %s", err)
// }
// smsglobal.New(cfg.SMS.Username, cfg.SMS.Password, cfg.Name, cfg.SMS.Contacts)
// loaded = true
// }
// }
//
// func TestAddEvent(t *testing.T) {
// testSetup(t)
//
// pair := pair.NewCurrencyPair("BTC", "USD")
// eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
// if err != nil && eventID != 0 {
// t.Errorf("Test Failed. AddEvent: Error, %s", err)
// }
// eventID, err = AddEvent("ANXX", "price", ">,==", pair, "SPOT", actionTest)
// if err == nil && eventID == 0 {
// t.Error("Test Failed. AddEvent: Error, error not captured in Exchange")
// }
// eventID, err = AddEvent("ANX", "prices", ">,==", pair, "SPOT", actionTest)
// if err == nil && eventID == 0 {
// t.Error("Test Failed. AddEvent: Error, error not captured in Item")
// }
// eventID, err = AddEvent("ANX", "price", "3===D", pair, "SPOT", actionTest)
// if err == nil && eventID == 0 {
// t.Error("Test Failed. AddEvent: Error, error not captured in Condition")
// }
// eventID, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", "console_prints")
// if err == nil && eventID == 0 {
// t.Error("Test Failed. AddEvent: Error, error not captured in Action")
// }
//
// if !RemoveEvent(eventID) {
// t.Error("Test Failed. RemoveEvent: Error, error removing event")
// }
// }
//
// func TestRemoveEvent(t *testing.T) {
// testSetup(t)
//
// pair := pair.NewCurrencyPair("BTC", "USD")
// eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
// if err != nil && eventID != 0 {
// t.Errorf("Test Failed. RemoveEvent: Error, %s", err)
// }
// if !RemoveEvent(eventID) {
// t.Error("Test Failed. RemoveEvent: Error, error removing event")
// }
// if RemoveEvent(1234) {
// t.Error("Test Failed. RemoveEvent: Error, error removing event")
// }
// }
//
// func TestGetEventCounter(t *testing.T) {
// testSetup(t)
//
// pair := pair.NewCurrencyPair("BTC", "USD")
// one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
// if err != nil {
// t.Errorf("Test Failed. GetEventCounter: Error, %s", err)
// }
// two, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
// if err != nil {
// t.Errorf("Test Failed. GetEventCounter: Error, %s", err)
// }
// three, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
// if err != nil {
// t.Errorf("Test Failed. GetEventCounter: Error, %s", err)
// }
//
// Events[three-1].Executed = true
//
// total, _ := GetEventCounter()
// if total <= 0 {
// t.Errorf("Test Failed. GetEventCounter: Total = %d", total)
// }
// if !RemoveEvent(one) {
// t.Error("Test Failed. GetEventCounter: Error, error removing event")
// }
// if !RemoveEvent(two) {
// t.Error("Test Failed. GetEventCounter: Error, error removing event")
// }
// if !RemoveEvent(three) {
// t.Error("Test Failed. GetEventCounter: Error, error removing event")
// }
//
// total2, _ := GetEventCounter()
// if total2 != 0 {
// t.Errorf("Test Failed. GetEventCounter: Total = %d", total2)
// }
// }
//
// func TestExecuteAction(t *testing.T) {
// testSetup(t)
//
// pair := pair.NewCurrencyPair("BTC", "USD")
// one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
// if err != nil {
// t.Fatalf("Test Failed. ExecuteAction: Error, %s", err)
// }
// isExecuted := Events[one].ExecuteAction()
// if !isExecuted {
// t.Error("Test Failed. ExecuteAction: Error, error removing event")
// }
// if !RemoveEvent(one) {
// t.Error("Test Failed. ExecuteAction: Error, error removing event")
// }
//
// action := actionSMSNotify + "," + "ALL"
// one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action)
// if err != nil {
// t.Fatalf("Test Failed. ExecuteAction: Error, %s", err)
// }
//
// isExecuted = Events[one].ExecuteAction()
// if !isExecuted {
// t.Error("Test Failed. ExecuteAction: Error, error removing event")
// }
// if !RemoveEvent(one) {
// t.Error("Test Failed. ExecuteAction: Error, error removing event")
// }
//
// action = actionSMSNotify + "," + "StyleGherkin"
// one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action)
// if err != nil {
// t.Fatalf("Test Failed. ExecuteAction: Error, %s", err)
// }
//
// isExecuted = Events[one].ExecuteAction()
// if !isExecuted {
// t.Error("Test Failed. ExecuteAction: Error, error removing event")
// }
// if !RemoveEvent(one) {
// t.Error("Test Failed. ExecuteAction: Error, error removing event")
// }
// // More tests when ExecuteAction is expanded
// }
//
// func TestEventToString(t *testing.T) {
// testSetup(t)
//
// pair := pair.NewCurrencyPair("BTC", "USD")
// one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest)
// if err != nil {
// t.Errorf("Test Failed. EventToString: Error, %s", err)
// }
//
// eventString := Events[one].String()
// if eventString != "If the BTCUSD [SPOT] price on ANX is > == then ACTION_TEST." {
// t.Error("Test Failed. EventToString: Error, incorrect return string")
// }
//
// if !RemoveEvent(one) {
// t.Error("Test Failed. EventToString: Error, error removing event")
// }
// }
//
// func TestCheckCondition(t *testing.T) {
// testSetup(t)
//
// // Test invalid currency pair
// newPair := pair.NewCurrencyPair("A", "B")
// one, err := AddEvent("ANX", "price", ">=,10", newPair, "SPOT", actionTest)
// if err != nil {
// t.Errorf("Test Failed. CheckCondition: Error, %s", err)
// }
// conditionBool := Events[one].CheckCondition()
// if conditionBool {
// t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
// }
//
// // Test last price == 0
// var tickerNew ticker.Price
// tickerNew.Last = 0
// newPair = pair.NewCurrencyPair("BTC", "USD")
// ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot)
// Events[one].Pair = newPair
// conditionBool = Events[one].CheckCondition()
// if conditionBool {
// t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
// }
//
// // Test last pricce > 0 and conditional logic
// tickerNew.Last = 11
// ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot)
// Events[one].Condition = ">,10"
// conditionBool = Events[one].CheckCondition()
// if !conditionBool {
// t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
// }
//
// // Test last price >= 10
// Events[one].Condition = ">=,10"
// conditionBool = Events[one].CheckCondition()
// if !conditionBool {
// t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
// }
//
// // Test last price <= 10
// Events[one].Condition = "<,100"
// conditionBool = Events[one].CheckCondition()
// if !conditionBool {
// t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
// }
//
// // Test last price <= 10
// Events[one].Condition = "<=,100"
// conditionBool = Events[one].CheckCondition()
// if !conditionBool {
// t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
// }
//
// Events[one].Condition = "==,11"
// conditionBool = Events[one].CheckCondition()
// if !conditionBool {
// t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
// }
//
// Events[one].Condition = "^,11"
// conditionBool = Events[one].CheckCondition()
// if conditionBool {
// t.Error("Test Failed. CheckCondition: Error, wrong conditional.")
// }
//
// if !RemoveEvent(one) {
// t.Error("Test Failed. CheckCondition: Error, error removing event")
// }
// }
//
// func TestIsValidEvent(t *testing.T) {
// testSetup(t)
//
// err := IsValidEvent("ANX", "price", ">,==", actionTest)
// if err != nil {
// t.Errorf("Test Failed. IsValidEvent: %s", err)
// }
// err = IsValidEvent("ANX", "price", ">,", actionTest)
// if err == nil {
// t.Errorf("Test Failed. IsValidEvent: %s", err)
// }
// err = IsValidEvent("ANX", "Testy", ">,==", actionTest)
// if err == nil {
// t.Errorf("Test Failed. IsValidEvent: %s", err)
// }
// err = IsValidEvent("Testys", "price", ">,==", actionTest)
// if err == nil {
// t.Errorf("Test Failed. IsValidEvent: %s", err)
// }
//
// action := "blah,blah"
// err = IsValidEvent("ANX", "price", ">=,10", action)
// if err == nil {
// t.Errorf("Test Failed. IsValidEvent: %s", err)
// }
//
// action = "SMS,blah"
// err = IsValidEvent("ANX", "price", ">=,10", action)
// if err == nil {
// t.Errorf("Test Failed. IsValidEvent: %s", err)
// }
//
// //Function tests need to appended to this function when more actions are
// //implemented
// }
//
// func TestCheckEvents(t *testing.T) {
// testSetup(t)
//
// pair := pair.NewCurrencyPair("BTC", "USD")
// _, err := AddEvent("ANX", "price", ">=,10", pair, "SPOT", actionTest)
// if err != nil {
// t.Fatal("Test failed. TestChcheckEvents add event")
// }
//
// go CheckEvents()
// }
//
// func TestIsValidExchange(t *testing.T) {
// testSetup(t)
//
// boolean := IsValidExchange("ANX")
// if !boolean {
// t.Error("Test Failed. IsValidExchange: Error, incorrect Exchange")
// }
// boolean = IsValidExchange("OBTUSE")
// if boolean {
// t.Error("Test Failed. IsValidExchange: Error, incorrect return")
// }
// }
//
// func TestIsValidCondition(t *testing.T) {
// testSetup(t)
//
// boolean := IsValidCondition(">")
// if !boolean {
// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
// }
// boolean = IsValidCondition(">=")
// if !boolean {
// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
// }
// boolean = IsValidCondition("<")
// if !boolean {
// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
// }
// boolean = IsValidCondition("<=")
// if !boolean {
// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
// }
// boolean = IsValidCondition("==")
// if !boolean {
// t.Error("Test Failed. IsValidCondition: Error, incorrect Condition")
// }
// boolean = IsValidCondition("**********")
// if boolean {
// t.Error("Test Failed. IsValidCondition: Error, incorrect return")
// }
// }
//
// func TestIsValidAction(t *testing.T) {
// testSetup(t)
//
// boolean := IsValidAction("sms")
// if !boolean {
// t.Error("Test Failed. IsValidAction: Error, incorrect Action")
// }
// boolean = IsValidAction(actionTest)
// if !boolean {
// t.Error("Test Failed. IsValidAction: Error, incorrect Action")
// }
// boolean = IsValidAction("randomstring")
// if boolean {
// t.Error("Test Failed. IsValidAction: Error, incorrect return")
// }
// }
//
// func TestIsValidItem(t *testing.T) {
// testSetup(t)
//
// boolean := IsValidItem("price")
// if !boolean {
// t.Error("Test Failed. IsValidItem: Error, incorrect Item")
// }
// boolean = IsValidItem("obtuse")
// if boolean {
// t.Error("Test Failed. IsValidItem: Error, incorrect return")
// }
// }

View File

@@ -7,10 +7,11 @@ import (
"strconv"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/communications"
"github.com/thrasher-/gocryptotrader/communications/base"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency/pair"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
"github.com/thrasher-/gocryptotrader/smsglobal"
)
const (
@@ -30,6 +31,9 @@ var (
errInvalidCondition = errors.New("invalid conditional option")
errInvalidAction = errors.New("invalid action")
errExchangeDisabled = errors.New("desired exchange is disabled")
// NOTE comms is an interim implementation
comms *communications.Communications
)
// Event struct holds the event variables
@@ -48,6 +52,12 @@ type Event struct {
// appended
var Events []*Event
// SetComms is an interim function that will support a median integration. This
// sets the current comms package.
func SetComms(commsP *communications.Communications) {
comms = commsP
}
// AddEvent adds an event to the Events chain and returns an index/eventID
// and an error
func AddEvent(Exchange, Item, Condition string, CurrencyPair pair.CurrencyPair, Asset, Action string) (int, error) {
@@ -106,12 +116,8 @@ func (e *Event) ExecuteAction() bool {
action := common.SplitStrings(e.Action, ",")
if action[0] == actionSMSNotify {
message := fmt.Sprintf("Event triggered: %s", e.String())
s := smsglobal.SMSGlobal
if action[1] == "ALL" {
s.SendMessageToAll(message)
} else {
contact, _ := s.GetContactByName(action[1])
s.SendMessage(contact.Number, message)
comms.PushEvent(base.Event{TradeDetails: message})
}
}
} else {
@@ -213,11 +219,7 @@ func IsValidEvent(Exchange, Item, Condition, Action string) error {
}
if action[1] != "ALL" {
s := smsglobal.SMSGlobal
_, err := s.GetContactByName(action[1])
if err != nil {
return errInvalidAction
}
comms.PushEvent(base.Event{Type: action[1]})
}
} else {
if Action != actionConsolePrint && Action != actionTest {

27
main.go
View File

@@ -15,19 +15,19 @@ import (
"github.com/thrasher-/gocryptotrader/currency/forexprovider"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/communications"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/portfolio"
"github.com/thrasher-/gocryptotrader/smsglobal"
)
// Bot contains configuration, portfolio, exchange & ticker data and is the
// overarching type across this code base.
type Bot struct {
config *config.Config
smsglobal *smsglobal.Base
portfolio *portfolio.Base
exchanges []exchange.IBotExchange
comms *communications.Communications
shutdown chan bool
dryRun bool
configFile string
@@ -82,21 +82,9 @@ func main() {
log.Printf("Bot '%s' started.\n", bot.config.Name)
log.Printf("Bot dry run mode: %v.\n", common.IsEnabled(bot.dryRun))
if bot.config.SMS.Enabled {
bot.smsglobal = smsglobal.New(bot.config.SMS.Username, bot.config.SMS.Password,
bot.config.Name, bot.config.SMS.Contacts)
log.Printf(
"SMS support enabled. Number of SMS contacts %d.\n",
bot.smsglobal.GetEnabledContacts(),
)
} else {
log.Println("SMS support disabled.")
}
log.Printf(
"Available Exchanges: %d. Enabled Exchanges: %d.\n",
len(bot.config.Exchanges), bot.config.CountEnabledExchanges(),
)
log.Printf("Available Exchanges: %d. Enabled Exchanges: %d.\n",
len(bot.config.Exchanges),
bot.config.CountEnabledExchanges())
common.HTTPClient = common.NewHTTPClientWithTimeout(bot.config.GlobalHTTPTimeout)
log.Printf("Global HTTP request timeout: %v.\n", common.HTTPClient.Timeout)
@@ -106,6 +94,10 @@ func main() {
log.Fatalf("No exchanges were able to be loaded. Exiting")
}
log.Println("Starting communication mediums..")
bot.comms = communications.NewComm(bot.config.GetCommunicationsConfig())
bot.comms.GetEnabledCommunicationMediums()
log.Printf("Fiat display currency: %s.", bot.config.Currency.FiatDisplayCurrency)
currency.BaseCurrency = bot.config.Currency.FiatDisplayCurrency
currency.FXProviders = forexprovider.StartFXService(bot.config.GetCurrencyConfig().ForexProviders)
@@ -124,6 +116,7 @@ func main() {
bot.portfolio = &portfolio.Portfolio
bot.portfolio.SeedPortfolio(bot.config.Portfolio)
SeedExchangeAccountInfo(GetAllEnabledExchangeAccountInfo().Data)
go portfolio.StartPortfolioWatcher()
go TickerUpdaterRoutine()
go OrderbookUpdaterRoutine()

View File

@@ -206,8 +206,11 @@ func TickerUpdaterRoutine() {
result, err = exch.GetTickerPrice(c, assetType)
}
printTickerSummary(result, c, assetType, exchangeName, err)
if bot.config.Webserver.Enabled && err == nil {
relayWebsocketEvent(result, "ticker_update", assetType, exchangeName)
if err == nil {
bot.comms.StageTickerData(exchangeName, assetType, result)
if bot.config.Webserver.Enabled {
relayWebsocketEvent(result, "ticker_update", assetType, exchangeName)
}
}
}
@@ -254,8 +257,11 @@ func OrderbookUpdaterRoutine() {
processOrderbook := func(exch exchange.IBotExchange, c pair.CurrencyPair, assetType string) {
result, err := exch.UpdateOrderbook(c, assetType)
printOrderbookSummary(result, c, assetType, exchangeName, err)
if bot.config.Webserver.Enabled && err == nil {
relayWebsocketEvent(result, "orderbook_update", assetType, exchangeName)
if err == nil {
bot.comms.StageOrderbookData(exchangeName, assetType, result)
if bot.config.Webserver.Enabled {
relayWebsocketEvent(result, "orderbook_update", assetType, exchangeName)
}
}
}

View File

@@ -1,45 +0,0 @@
# GoCryptoTrader package Smsglobal
<img src="https://github.com/thrasher-/gocryptotrader/blob/master/web/src/assets/page-logo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://travis-ci.org/thrasher-/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-/gocryptotrader)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-/gocryptotrader/smsglobal)
[![Coverage Status](http://codecov.io/github/thrasher-/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-/gocryptotrader)
This smsglobal package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progresss on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://gocryptotrader.herokuapp.com/)
## Current Features for smsglobal
+ This package allows for the messaging of events to a personal phone number or a group of phone numbers.
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB***

View File

@@ -1,168 +0,0 @@
package smsglobal
import (
"errors"
"flag"
"net/url"
"strings"
"github.com/thrasher-/gocryptotrader/common"
)
const (
smsGlobalAPIURL = "https://www.smsglobal.com/http-api.php"
// ErrSMSContactNotFound is a general error code for "SMS Contact not found."
ErrSMSContactNotFound = "SMS Contact not found."
errSMSNotSent = "SMS message not sent."
)
// vars for the SMS global package
var (
SMSGlobal *Base
)
// Contact struct stores information related to a SMSGlobal contact
type Contact struct {
Name string `json:"name"`
Number string `json:"number"`
Enabled bool `json:"enabled"`
}
// Base struct stores information related to the SMSGlobal package
type Base struct {
Contacts []Contact `json:"contacts"`
Username string `json:"username"`
Password string `json:"password"`
SendFrom string `json:"send_from"`
}
// New initialises the SMSGlobal var
func New(username, password, sendFrom string, contacts []Contact) *Base {
if username == "" || password == "" || sendFrom == "" || len(contacts) == 0 {
return nil
}
var goodContacts []Contact
for x := range contacts {
if contacts[x].Name != "" || contacts[x].Number != "" {
goodContacts = append(goodContacts, contacts[x])
}
}
SMSGlobal = &Base{
Contacts: goodContacts,
Username: username,
Password: password,
SendFrom: sendFrom,
}
return SMSGlobal
}
// GetEnabledContacts returns how many SMS contacts are enabled in the
// contact list
func (s *Base) GetEnabledContacts() int {
counter := 0
for x := range s.Contacts {
if s.Contacts[x].Enabled {
counter++
}
}
return counter
}
// GetContactByNumber returns a contact with supplied number
func (s *Base) GetContactByNumber(number string) (Contact, error) {
for x := range s.Contacts {
if s.Contacts[x].Number == number {
return s.Contacts[x], nil
}
}
return Contact{}, errors.New(ErrSMSContactNotFound)
}
// GetContactByName returns a contact with supplied name
func (s *Base) GetContactByName(name string) (Contact, error) {
for x := range s.Contacts {
if common.StringToLower(s.Contacts[x].Name) == common.StringToLower(name) {
return s.Contacts[x], nil
}
}
return Contact{}, errors.New(ErrSMSContactNotFound)
}
// AddContact checks to see if a contact exists and adds them if it doesn't
func (s *Base) AddContact(contact Contact) {
if contact.Name == "" || contact.Number == "" {
return
}
if s.ContactExists(contact) {
return
}
s.Contacts = append(s.Contacts, contact)
}
// ContactExists checks to see if a contact exists
func (s *Base) ContactExists(contact Contact) bool {
for x := range s.Contacts {
if s.Contacts[x].Number == contact.Number && common.StringToLower(s.Contacts[x].Name) == common.StringToLower(contact.Name) {
return true
}
}
return false
}
// RemoveContact removes a contact if it exists
func (s *Base) RemoveContact(contact Contact) {
if !s.ContactExists(contact) {
return
}
for x := range s.Contacts {
if s.Contacts[x].Name == contact.Name && s.Contacts[x].Number == contact.Number {
s.Contacts = append(s.Contacts[:x], s.Contacts[x+1:]...)
return
}
}
}
// SendMessageToAll sends a message to all enabled contacts in cfg
func (s *Base) SendMessageToAll(message string) {
for x := range s.Contacts {
if s.Contacts[x].Enabled {
s.SendMessage(s.Contacts[x].Name, message)
}
}
}
// SendMessage sends a message to an individual contact
func (s *Base) SendMessage(to, message string) error {
if flag.Lookup("test.v") != nil {
return nil
}
values := url.Values{}
values.Set("action", "sendsms")
values.Set("user", s.Username)
values.Set("password", s.Password)
values.Set("from", s.SendFrom)
values.Set("to", to)
values.Set("text", message)
headers := make(map[string]string)
headers["Content-Type"] = "application/x-www-form-urlencoded"
resp, err := common.SendHTTPRequest(
"POST", smsGlobalAPIURL, headers, strings.NewReader(values.Encode()),
)
if err != nil {
return err
}
if !common.StringContains(resp, "OK: 0; Sent queued message") {
return errors.New(errSMSNotSent)
}
return nil
}

View File

@@ -1,138 +0,0 @@
package smsglobal
import (
"log"
"testing"
)
func TestNew(t *testing.T) {
result := New("", "", "", nil)
if result != nil {
t.Error("Test failed. New: Expected nil result")
}
contact := Contact{Name: "bob", Number: "1234", Enabled: true}
var contacts []Contact
contacts = append(contacts, contact)
result = New("bob", "pw", "Skynet", contacts)
if !result.ContactExists(contact) {
t.Error("Test failed. New: Expected contact not found")
}
}
func TestGetEnabledContacts(t *testing.T) {
contact := Contact{Name: "bob", Number: "1234", Enabled: true}
var contacts []Contact
contacts = append(contacts, contact)
result := New("bob", "pw", "Skynet", contacts)
expected := 1
actual := result.GetEnabledContacts()
if expected != actual {
t.Errorf("Test failed. TestGetEnabledContacts expected %d, got %d",
expected, actual)
}
}
func TestGetContactByNumber(t *testing.T) {
contact := Contact{Name: "bob", Number: "1234", Enabled: true}
var contacts []Contact
contacts = append(contacts, contact)
result := New("bob", "pw", "Skynet", contacts)
actual, err := result.GetContactByNumber(contact.Number)
if err != nil {
t.Fatalf("Test failed. TestGetContactByNumber: %s", err)
}
if actual.Name != contact.Name && actual.Number != contact.Number && actual.Enabled != contact.Enabled {
t.Fatal("Test failed. TestGetContactByNumber: Incorrect values")
}
_, err = result.GetContactByNumber("ASDASDASD")
if err == nil {
t.Fatal("Test failed. TestGetContactByNumber: Returned nil err on non-existent number")
}
}
func TestGetContactByName(t *testing.T) {
contact := Contact{Name: "bob", Number: "1234", Enabled: true}
var contacts []Contact
contacts = append(contacts, contact)
result := New("bob", "pw", "Skynet", contacts)
actual, err := result.GetContactByName(contact.Name)
if err != nil {
t.Fatalf("Test failed. TestGetContactByName: %s", err)
}
if actual.Name != contact.Name && actual.Number != contact.Number && actual.Enabled != contact.Enabled {
t.Fatal("Test failed. TestGetContactByName: Incorrect values")
}
_, err = result.GetContactByName("ASDASDASD")
if err == nil {
t.Fatal("Test failed. TestGetContactByName: Returned nil err on non-existent number")
}
}
func TestAddContact(t *testing.T) {
contact := Contact{Name: "bob", Number: "1234", Enabled: true}
var contacts []Contact
contacts = append(contacts, contact)
result := New("bob", "pw", "Skynet", contacts)
// Test adding same contact
result.AddContact(contact)
if result.GetEnabledContacts() > 1 {
t.Fatal("Test failed. TestAddContact: Incorrect values")
}
invalidContact := Contact{Name: "", Number: "", Enabled: true}
result.AddContact(invalidContact)
if result.GetEnabledContacts() > 1 {
t.Fatal("Test failed. TestAddContact: Incorrect values")
}
newContact := Contact{Name: "newContact", Number: "12345", Enabled: true}
result.AddContact(newContact)
if result.GetEnabledContacts() != 2 {
t.Fatal("Test failed. TestAddContact: Incorrect values")
}
}
func TestRemoveContact(t *testing.T) {
contact := Contact{Name: "bob", Number: "1234", Enabled: true}
var contacts []Contact
contacts = append(contacts, contact)
result := New("bob", "pw", "Skynet", contacts)
result.RemoveContact(Contact{Name: "blah", Number: "1234"})
if result.GetEnabledContacts() != 1 {
t.Fatal("Test failed. TestRemoveContact: Incorrect values")
}
result.RemoveContact(contact)
if result.GetEnabledContacts() != 0 {
t.Fatal("Test failed. TestRemoveContact: Incorrect values")
}
}
func TestSendMessageToAll(t *testing.T) {
contact := Contact{Name: "bob", Number: "1234", Enabled: true}
var contacts []Contact
contacts = append(contacts, contact)
result := New("bob", "pw", "Skynet", contacts)
result.SendMessageToAll("hello world")
}
func TestSendMessage(t *testing.T) {
contact := Contact{Name: "bob", Number: "1234", Enabled: true}
var contacts []Contact
contacts = append(contacts, contact)
result := New("bob", "pw", "Skynet", contacts)
err := result.SendMessage(contact.Number, "hello world")
log.Println(err)
t.Log(err)
}

View File

@@ -1,6 +1,7 @@
{
"Name": "Skynet",
"EncryptConfig": 0,
"GlobalHTTPTimeout": 15000000000,
"CurrencyConfig": {
"ForexProviders": [
{
@@ -47,7 +48,45 @@
},
"FiatDisplayCurrency": "USD"
},
"GlobalHTTPTimeout": 15000000000,
"Communications": {
"Slack": {
"Name": "Slack",
"Enabled": false,
"Verbose": false,
"TargetChannel": "general",
"VerificationToken": "testtest"
},
"SMSGlobal": {
"Name": "SMSGlobal",
"Enabled": true,
"Verbose": false,
"Username": "1234",
"Password": "12334",
"Contacts": [
{
"Name": "StyleGherkin",
"Number": "1231424",
"Enabled": true
}
]
},
"SMTP": {
"Name": "SMTP",
"Enabled": false,
"Verbose": false,
"Host": "smtp.google.com",
"Port": "537",
"AccountName": "some",
"AccountPassword": "password",
"RecipientList": "lol123@gmail.com"
},
"Telegram": {
"Name": "Telegram",
"Enabled": false,
"Verbose": false,
"VerificationToken": "testest"
}
},
"PortfolioAddresses": {
"Addresses": [
{
@@ -76,18 +115,6 @@
}
]
},
"SMSGlobal": {
"Enabled": true,
"Username": "1234",
"Password": "12334",
"Contacts": [
{
"name": "StyleGherkin",
"number": "1231424",
"enabled": true
}
]
},
"Webserver": {
"Enabled": false,
"AdminUsername": "admin",