├── LICENSE ├── README.md ├── abcs.go ├── go.mod ├── go.sum └── imessage ├── imessage.go ├── incoming.go ├── outgoing.go └── protocol.go /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, Operand, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **ABCS** is a simple HTTP server which can be used to send and receive iMessages 2 | programmatically on a machine running macOS. ABCS totally isn't an acronym for 3 | "Apple Business Chat Sucks". The great thing about this approach is that you 4 | don't need a human on the other end, making it an excellent tool for bots and 5 | virtual assistants. 6 | 7 | The tool itself is super simple. Whenever a new iMessage is received, it will post 8 | a small JSON blob to an endpoint of your choosing. When you want to send a message, 9 | you can configure a listening address (defaults to `localhost:11106`) and hit the 10 | root endpoint with a small JSON blob of the message you want to send, and to whom. 11 | 12 | **Example Usage** 13 | 14 | To build the tool, just do `go build .` in the project folder. 15 | 16 | `./abcs -endpoint=https://example.com/cool_endpoint -listen=127.0.0.1:8080` 17 | 18 | This command will start a web server on `127.0.0.1:8080` listening for requests. 19 | If you send a JSON blob like the following to this endpoint, the iMessage will 20 | be sent. 21 | 22 | ``` 23 | { 24 | "to":"1234567890", 25 | "message":"ABCS is not an acronym. Remember this." 26 | } 27 | ``` 28 | 29 | When you receive an iMessage, a JSON blob like the following will be `POST`ed to 30 | the endpoint `https://example.com/cool_endpoint`. 31 | 32 | ``` 33 | { 34 | "from":"1234567890", 35 | "message":"Yeah, I get it. ABCS isn't an acronym." 36 | } 37 | ``` 38 | 39 | **Other Stuff** 40 | 41 | This tool is inherently insecure, since there is no authentication baked in. Rather, 42 | it is reccomended to use a VPC or some sort of private network (I recommend [Tailscale](https://tailscale.com)) to ensure that this server exposed publicly. This tool is used 43 | in production for [Operand](https://operand.ai), though if reliability is important 44 | I'd reccomend building some additional stuff on top of this. It would be great 45 | to eventually have multiple hosted Mac minis and automatically detect machine failures, 46 | all that jazz. 47 | 48 | Tools shouldn't limit, they should empower. We at Operand disagree that a human 49 | should be required on the other end of an iMessage conversation with a business. 50 | But of course, ABCS isn't an acronym for "Apple Business Chat Sucks" and in no 51 | way intends to disparage a product of another company. 52 | 53 | Have a beautiful day <3. 54 | 55 | P.S. Big thanks to https://github.com/golift/imessage, I spent a bunch of time 56 | last night figuring out the iMessage stuff myself, only to discover that someone 57 | had done it really well and open sourced it as a tool. This tool is simply a wrapper 58 | of this existing work, and adds server functionality. 59 | -------------------------------------------------------------------------------- /abcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "os" 11 | "os/user" 12 | "time" 13 | 14 | "github.com/getsentry/sentry-go" 15 | "github.com/operandinc/abcs/imessage" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | var ( 20 | listenAddr = flag.String("listen", "localhost:11106", "address to listen on") 21 | endpointAddr = flag.String("endpoint", "https://example.com/abcs", "endpoint to send messages to") 22 | ) 23 | 24 | const serverName = "abcs" 25 | 26 | func init() { 27 | dsn, ok := os.LookupEnv("SENTRY_DSN") 28 | if ok { 29 | if err := sentry.Init(sentry.ClientOptions{ 30 | Dsn: dsn, 31 | ServerName: serverName, 32 | }); err != nil { 33 | log.Panic("failed to initialize sentry") 34 | } 35 | } 36 | } 37 | 38 | func main() { 39 | if err := run(); err != nil { 40 | log.Fatalf("error: %v", err) 41 | } 42 | } 43 | 44 | type server struct { 45 | endpoint string 46 | msgs *imessage.Messages 47 | logger imessage.Logger 48 | incoming chan imessage.Incoming 49 | underlying *http.Server 50 | } 51 | 52 | var attachmentExtensions = map[string]string{ 53 | "image/png": "png", 54 | "image/jpeg": "jpeg", 55 | "image/heic": "heic", 56 | "application/pdf": "pdf", 57 | "text/vcard": "vcf", 58 | } 59 | 60 | func (s *server) sendMessageHandler() http.HandlerFunc { 61 | type request struct { 62 | To string `json:"to"` 63 | Message string `json:"message"` 64 | Attachment []byte `json:"attachment,omitempty"` 65 | AttachmentType string `json:"attachment_type,omitempty"` 66 | } 67 | return func(w http.ResponseWriter, r *http.Request) { 68 | var req request 69 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 70 | http.Error(w, err.Error(), http.StatusBadRequest) 71 | return 72 | } 73 | out := imessage.Outgoing{To: req.To} 74 | if req.Attachment != nil && req.AttachmentType != "" { 75 | log.Printf("(to %s) [file] %d bytes (%s)", req.To, len(req.Attachment), req.AttachmentType) 76 | ext, ok := attachmentExtensions[req.AttachmentType] 77 | if !ok { 78 | http.Error(w, "unknown attachment type", http.StatusBadRequest) 79 | return 80 | } 81 | file, err := os.CreateTemp("", fmt.Sprintf("abcs-*.%s", ext)) 82 | if err != nil { 83 | http.Error(w, err.Error(), http.StatusBadRequest) 84 | return 85 | } 86 | defer file.Close() 87 | if _, err := file.Write(req.Attachment); err != nil { 88 | http.Error(w, err.Error(), http.StatusInternalServerError) 89 | return 90 | } 91 | out.Text = file.Name() 92 | out.File = true 93 | out.Call = func(_ *imessage.Response) { 94 | os.Remove(file.Name()) 95 | } 96 | } else { 97 | log.Printf("(to %s) %s", req.To, req.Message) 98 | out.Text = req.Message 99 | } 100 | s.msgs.Send(out) 101 | w.WriteHeader(http.StatusOK) 102 | } 103 | } 104 | 105 | type endpointRequest struct { 106 | From string `json:"from"` 107 | Message string `json:"message"` 108 | Attachment []byte `json:"attachment,omitempty"` 109 | AttachmentType string `json:"attachment_type,omitempty"` 110 | Token *string `json:"token,omitempty"` 111 | } 112 | 113 | func (s *server) notifyEndpoint(incoming imessage.Incoming) error { 114 | er := endpointRequest{ 115 | From: incoming.From, 116 | Message: incoming.Text, 117 | Attachment: incoming.Attachment, 118 | AttachmentType: incoming.AttachmentType, 119 | } 120 | buf, err := json.Marshal(er) 121 | if err != nil { 122 | return err 123 | } 124 | req, err := http.NewRequest("POST", s.endpoint, bytes.NewBuffer(buf)) 125 | if err != nil { 126 | return err 127 | } 128 | req.Header.Set("Content-Type", "application/json") 129 | resp, err := http.DefaultClient.Do(req) 130 | if err != nil { 131 | return err 132 | } 133 | defer resp.Body.Close() 134 | if resp.StatusCode != http.StatusOK { 135 | return errors.Errorf("endpoint returned non-200 code %d: %s", resp.StatusCode, resp.Status) 136 | } 137 | return nil 138 | } 139 | 140 | func (s *server) handleIncoming() { 141 | for msg := range s.incoming { 142 | if msg.Attachment != nil { 143 | log.Printf("(from %s) %s (w/ attachment %s - size %d)", msg.From, msg.Text, msg.AttachmentType, len(msg.Attachment)) 144 | } else { 145 | log.Printf("(from %s) %s", msg.From, msg.Text) 146 | } 147 | if err := s.notifyEndpoint(msg); err != nil { 148 | s.logger.Printf("failed to notify endpoint: %v", err) 149 | } 150 | } 151 | } 152 | 153 | func currentUser() (string, error) { 154 | u, err := user.Current() 155 | if err != nil { 156 | return "", err 157 | } 158 | return u.Username, nil 159 | } 160 | 161 | func getHomePath() (string, error) { 162 | user, err := currentUser() 163 | if err != nil { 164 | return "", err 165 | } 166 | return fmt.Sprintf("/Users/%s", user), nil 167 | } 168 | 169 | func getiChatDBLocation() (string, error) { 170 | user, err := currentUser() 171 | if err != nil { 172 | return "", err 173 | } 174 | return fmt.Sprintf("/Users/%s/Library/Messages/chat.db", user), nil 175 | } 176 | 177 | const ( 178 | queueSize = 10 // This should be tuned to the busyness of your server. 179 | retries = 3 // The number of times to retry sending a message. 180 | ) 181 | 182 | type sentryLogger struct{} 183 | 184 | func (sl *sentryLogger) log(err error) { 185 | log.Printf("(error) %v", err) 186 | sentry.CaptureException(err) 187 | } 188 | 189 | func (sl *sentryLogger) Print(v ...interface{}) { 190 | sl.log(errors.New(fmt.Sprint(v...))) 191 | } 192 | 193 | func (sl *sentryLogger) Printf(fmt string, v ...interface{}) { 194 | sl.log(errors.Errorf(fmt, v...)) 195 | } 196 | 197 | func (sl *sentryLogger) Println(v ...interface{}) { 198 | sl.Print(v...) 199 | } 200 | 201 | func newServer(listen, endpoint string) (*server, error) { 202 | dbpath, err := getiChatDBLocation() 203 | if err != nil { 204 | return nil, err 205 | } 206 | homepath, err := getHomePath() 207 | if err != nil { 208 | return nil, err 209 | } 210 | var logger imessage.Logger = &sentryLogger{} 211 | c := &imessage.Config{ 212 | SQLPath: dbpath, 213 | HomePath: homepath, 214 | QueueSize: queueSize, 215 | Retries: retries, 216 | ErrorLog: logger, 217 | } 218 | im, err := imessage.Init(c) 219 | if err != nil { 220 | return nil, err 221 | } 222 | incoming := make(chan imessage.Incoming) 223 | im.IncomingChan(".*", incoming) 224 | if err := im.Start(); err != nil { 225 | return nil, err 226 | } 227 | s := &server{ 228 | endpoint: endpoint, 229 | msgs: im, 230 | logger: logger, 231 | incoming: incoming, 232 | } 233 | r := http.NewServeMux() 234 | r.HandleFunc("/", s.sendMessageHandler()) 235 | s.underlying = &http.Server{ 236 | Addr: listen, 237 | Handler: r, 238 | ReadTimeout: time.Second, 239 | WriteTimeout: 10 * time.Second, 240 | } 241 | go s.handleIncoming() 242 | return s, nil 243 | } 244 | 245 | func (s *server) start() error { 246 | return s.underlying.ListenAndServe() 247 | } 248 | 249 | func run() error { 250 | flag.Parse() 251 | s, err := newServer(*listenAddr, *endpointAddr) 252 | if err != nil { 253 | return err 254 | } 255 | return s.start() 256 | } 257 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/operandinc/abcs 2 | 3 | go 1.15 4 | 5 | require ( 6 | crawshaw.io/sqlite v0.3.2 7 | github.com/fsnotify/fsnotify v1.5.0 8 | github.com/getsentry/sentry-go v0.10.0 9 | github.com/pkg/errors v0.8.1 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 h1:yDf7ARQc637HoxDho7xjqdvO5ZA2Yb+xzv/fOnnvZzw= 2 | crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= 3 | crawshaw.io/sqlite v0.3.2 h1:N6IzTjkiw9FItHAa0jp+ZKC6tuLzXqAYIv+ccIWos1I= 4 | crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= 5 | github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 6 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 7 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= 8 | github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= 9 | github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= 10 | github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= 11 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 12 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 13 | github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 14 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 15 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 16 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 17 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 18 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= 22 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 23 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 24 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 25 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= 26 | github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= 27 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= 28 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 29 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 30 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 31 | github.com/fsnotify/fsnotify v1.5.0 h1:NO5hkcB+srp1x6QmwvNZLeaOgbM8cmBTN32THzjvu2k= 32 | github.com/fsnotify/fsnotify v1.5.0/go.mod h1:BX0DCEr5pT4jm2CnQdVP1lFV521fcCNcyEeNp4DQQDk= 33 | github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= 34 | github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g= 35 | github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws= 36 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 37 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 38 | github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= 39 | github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= 40 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 41 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= 42 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 43 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 44 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 45 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 46 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 48 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 49 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 51 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 52 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 53 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 54 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 55 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 56 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 57 | github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= 58 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 59 | github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= 60 | github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= 61 | github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= 62 | github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= 63 | github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= 64 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 65 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 66 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 67 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 68 | github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= 69 | github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= 70 | github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= 71 | github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= 72 | github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= 73 | github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 74 | github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 75 | github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 76 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 77 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 78 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 79 | github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= 80 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 81 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 82 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 83 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 84 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 85 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 86 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 87 | github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= 88 | github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= 89 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 90 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 91 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 92 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 93 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 94 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 95 | github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= 96 | github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= 97 | github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= 98 | github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 99 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 100 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 101 | github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 102 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 103 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 104 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 105 | github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 106 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 107 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 108 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 109 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 110 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 111 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 112 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 113 | github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= 114 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 115 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 116 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 117 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 118 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 119 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 120 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 121 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 122 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 123 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 124 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 125 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 126 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 127 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 128 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 129 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 130 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 131 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 132 | github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 133 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 134 | github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= 135 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 136 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 137 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 138 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 139 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 140 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 141 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 142 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= 143 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 144 | github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= 145 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 146 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 147 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 148 | golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 149 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 150 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 151 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 152 | golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 153 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 154 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 155 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 156 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 157 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 158 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 161 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 162 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 163 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 164 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 167 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 169 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 170 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 171 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 172 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 173 | golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 174 | golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 175 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 176 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 177 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 178 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 179 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 180 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 181 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 182 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 183 | gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 184 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 185 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 186 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 187 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 188 | gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 189 | -------------------------------------------------------------------------------- /imessage/imessage.go: -------------------------------------------------------------------------------- 1 | // Package imessage is used to interact with iMessage (Messages.app) on macOS 2 | // 3 | // Use this library to send and receive messages using iMessage. 4 | // Can be used to make a chat bot or something similar. You can bind either a 5 | // function or a channel to any or all messages. The Send() method uses 6 | // AppleScript, which is likely going to require some tinkering. You got this 7 | // far, so I trust you'll figure that out. Let me know how it works out. 8 | // 9 | // The library uses `fsnotify` to poll for db updates, then checks the database for changes. 10 | // Only new messages are processed. If somehow `fsnotify` fails it will fall back to polling 11 | // the database. Pay attention to the debug/error logs. 12 | package imessage 13 | 14 | import ( 15 | "errors" 16 | "fmt" 17 | "io/ioutil" 18 | "log" 19 | "os" 20 | 21 | "crawshaw.io/sqlite" 22 | ) 23 | 24 | // Config is our input data, data store, and interface to methods. 25 | // Fill out this struct and pass it into imessage.Init() 26 | type Config struct { 27 | // ClearMsgs will cause this library to clear all iMessage conversations. 28 | ClearMsgs bool `xml:"clear_messages" json:"clear_messages,omitempty" toml:"clear_messages,_omitempty" yaml:"clear_messages"` 29 | // This is the channel buffer size. 30 | QueueSize int `xml:"queue_size" json:"queue_size,omitempty" toml:"queue_size,_omitempty" yaml:"queue_size"` 31 | // How many applescript retries to perform. 32 | Retries int `xml:"retries" json:"retries,omitempty" toml:"retries,_omitempty" yaml:"retries"` 33 | // Timeout in seconds for AppleScript Exec commands. 34 | Timeout int `xml:"timeout" json:"timeout,omitempty" toml:"timeout,_omitempty" yaml:"timeout"` 35 | // SQLPath is the location if the iMessage database. 36 | SQLPath string `xml:"sql_path" json:"sql_path,omitempty" toml:"sql_path,_omitempty" yaml:"sql_path"` 37 | // HomePath is the location of the home directory of the user. 38 | // Basically, what ever ~ maps to. If your user is `mg`, then this should be `/Users/mg`. 39 | HomePath string `xml:"home_path" json:"home_path,omitempty" toml:"home_path,_omitempty" yaml:"home_path"` 40 | // Loggers. 41 | ErrorLog Logger `xml:"-" json:"-" toml:"-" yaml:"-"` 42 | DebugLog Logger `xml:"-" json:"-" toml:"-" yaml:"-"` 43 | } 44 | 45 | // Messages is the interface into this module. Init() returns this struct. 46 | // All of the important library methods are bound to this type. 47 | // ErrorLog and DebugLog can be set directly, or use the included methods to set them. 48 | type Messages struct { 49 | *Config // Input config. 50 | running bool // Only used in Start() and Stop() 51 | currentID int64 // Constantly growing 52 | outChan chan Outgoing // send 53 | inChan chan Incoming // recieve 54 | binds // incoming message handlers 55 | } 56 | 57 | // Logger is a base interface to deal with changing log outs. 58 | // Pass a matching interface (like log.Printf) to capture 59 | // messages from the running background go routines. 60 | type Logger interface { 61 | Print(v ...interface{}) 62 | Printf(fmt string, v ...interface{}) 63 | Println(v ...interface{}) 64 | } 65 | 66 | // Init is the primary function to retrieve a Message handler. 67 | // Pass a Config struct in and use the returned Messages struct to send 68 | // and respond to incoming messages. 69 | func Init(config *Config) (*Messages, error) { 70 | if _, err := os.Stat(config.SQLPath); err != nil { 71 | return nil, fmt.Errorf("sql file access error: %v", err) 72 | } 73 | config.setDefaults() 74 | m := &Messages{ 75 | Config: config, 76 | outChan: make(chan Outgoing, config.QueueSize), 77 | inChan: make(chan Incoming, config.QueueSize), 78 | } 79 | // Try to open, query and close the database. 80 | return m, m.getCurrentID() 81 | } 82 | 83 | func (c *Config) setDefaults() { 84 | if c.Retries == 0 { 85 | c.Retries = 3 86 | } else if c.Retries > 10 { 87 | c.Retries = 10 88 | } 89 | if c.QueueSize < 10 { 90 | c.QueueSize = 10 91 | } 92 | if c.Timeout < 10 { 93 | c.Timeout = 10 94 | } 95 | if c.ErrorLog == nil { 96 | c.ErrorLog = log.New(ioutil.Discard, "[ERROR] ", log.LstdFlags) 97 | } 98 | if c.DebugLog == nil { 99 | c.DebugLog = log.New(ioutil.Discard, "[DEBUG] ", log.LstdFlags) 100 | } 101 | } 102 | 103 | // Start starts the iMessage-sqlite3 db and outgoing message watcher routine(s). 104 | // Outgoing messages wont work and incoming message are ignored until Start() runs. 105 | func (m *Messages) Start() error { 106 | if m.running { 107 | return errors.New("already running") 108 | } else if err := m.getCurrentID(); err != nil { 109 | return err 110 | } 111 | m.running = true 112 | m.DebugLog.Printf("starting with id %d", m.currentID) 113 | go m.processOutgoingMessages() 114 | return m.processIncomingMessages() 115 | } 116 | 117 | // Stop cancels the iMessage-sqlite3 db and outgoing message watcher routine(s). 118 | // Outgoing messages stop working when the routines are stopped. 119 | // Incoming messages are ignored after this runs. 120 | func (m *Messages) Stop() { 121 | defer func() { m.running = false }() 122 | if m.running { 123 | close(m.inChan) 124 | close(m.outChan) 125 | } 126 | } 127 | 128 | // getDB opens a database connection and locks access, so only one reader can 129 | // access the db at once. 130 | func (m *Messages) getDB() (*sqlite.Conn, error) { 131 | m.Lock() 132 | m.DebugLog.Println("opening database:", m.SQLPath) 133 | db, err := sqlite.OpenConn(m.SQLPath, sqlite.SQLITE_OPEN_READONLY) 134 | m.checkErr(err, "opening database") 135 | return db, err 136 | } 137 | 138 | // closeDB stops reading the sqlite db and unlocks the read lock. 139 | func (m *Messages) closeDB(db *sqlite.Conn) { 140 | m.DebugLog.Println("closing database:", m.SQLPath) 141 | if db == nil { 142 | m.DebugLog.Print("db was nil? not closed") 143 | return 144 | } 145 | defer m.Unlock() 146 | m.checkErr(db.Close(), "closing database: "+m.SQLPath) 147 | } 148 | 149 | // checkErr writes an error to Logger if it exists. 150 | func (m *Messages) checkErr(err error, msg string) { 151 | if err != nil { 152 | m.ErrorLog.Printf("%s: %q\n", msg, err) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /imessage/incoming.go: -------------------------------------------------------------------------------- 1 | package imessage 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/fsnotify/fsnotify" 14 | ) 15 | 16 | // DefaultDuration is the minimum interval that must pass before opening the database again. 17 | var DefaultDuration = 200 * time.Millisecond 18 | 19 | // Incoming is represents a message from someone. This struct is filled out 20 | // and sent to incoming callback methods and/or to bound channels. 21 | type Incoming struct { 22 | RowID int64 // RowID is the unique database row id. 23 | From string // From is the handle of the user who sent the message. 24 | Text string // Text is the body of the message. 25 | Attachment []byte // Attachment data. 26 | AttachmentType string // Attachment MIME type. 27 | } 28 | 29 | // Callback is the type used to return an incoming message to the consuming app. 30 | // Create a function that matches this interface to process incoming messages 31 | // using a callback (as opposed to a channel). 32 | type Callback func(msg Incoming) 33 | 34 | type chanBinding struct { 35 | Match string 36 | Chan chan Incoming 37 | } 38 | 39 | type funcBinding struct { 40 | Match string 41 | Func Callback 42 | } 43 | 44 | type binds struct { 45 | Funcs []*funcBinding 46 | Chans []*chanBinding 47 | // locks either or both slices 48 | sync.RWMutex 49 | } 50 | 51 | // IncomingChan connects a channel to a matched string in a message. 52 | // Similar to the IncomingCall method, this will send an incoming message 53 | // to a channel. Any message with text matching `match` is sent. Regexp supported. 54 | // Use '.*' for all messages. The channel blocks, so avoid long operations. 55 | func (m *Messages) IncomingChan(match string, channel chan Incoming) { 56 | m.binds.Lock() 57 | defer m.binds.Unlock() 58 | m.Chans = append(m.Chans, &chanBinding{Match: match, Chan: channel}) 59 | } 60 | 61 | // IncomingCall connects a callback function to a matched string in a message. 62 | // This methods creates a callback that is run in a go routine any time 63 | // a message containing `match` is found. Use '.*' for all messages. Supports regexp. 64 | func (m *Messages) IncomingCall(match string, callback Callback) { 65 | m.binds.Lock() 66 | defer m.binds.Unlock() 67 | m.Funcs = append(m.Funcs, &funcBinding{Match: match, Func: callback}) 68 | } 69 | 70 | // RemoveChan deletes a message match to channel made with IncomingChan() 71 | func (m *Messages) RemoveChan(match string) int { 72 | m.binds.Lock() 73 | defer m.binds.Unlock() 74 | removed := 0 75 | for i, rlen := 0, len(m.Chans); i < rlen; i++ { 76 | j := i - removed 77 | if m.Chans[j].Match == match { 78 | m.Chans = append(m.Chans[:j], m.Chans[j+1:]...) 79 | removed++ 80 | } 81 | } 82 | return removed 83 | } 84 | 85 | // RemoveCall deletes a message match to function callback made with IncomingCall() 86 | func (m *Messages) RemoveCall(match string) int { 87 | m.binds.Lock() 88 | defer m.binds.Unlock() 89 | removed := 0 90 | for i, rlen := 0, len(m.Funcs); i < rlen; i++ { 91 | j := i - removed 92 | if m.Funcs[j].Match == match { 93 | m.Funcs = append(m.Funcs[:j], m.Funcs[j+1:]...) 94 | removed++ 95 | } 96 | } 97 | return removed 98 | } 99 | 100 | // processIncomingMessages starts the iMessage-sqlite3 db watcher routine(s). 101 | func (m *Messages) processIncomingMessages() error { 102 | watcher, err := fsnotify.NewWatcher() 103 | if err != nil { 104 | return err 105 | } 106 | go func() { 107 | m.fsnotifySQL(watcher, time.NewTicker(DefaultDuration)) 108 | _ = watcher.Close() 109 | }() 110 | return watcher.Add(filepath.Dir(m.SQLPath)) 111 | } 112 | 113 | func (m *Messages) fsnotifySQL(watcher *fsnotify.Watcher, ticker *time.Ticker) { 114 | for checkDB := false; ; { 115 | select { 116 | case msg, ok := <-m.inChan: 117 | if !ok { 118 | return 119 | } 120 | m.handleIncoming(msg) 121 | 122 | case <-ticker.C: 123 | if checkDB { 124 | m.checkForNewMessages() 125 | } 126 | 127 | case event, ok := <-watcher.Events: 128 | if !ok { 129 | m.ErrorLog.Print("fsnotify watcher failed. message routines stopped") 130 | m.Stop() 131 | return 132 | } 133 | checkDB = event.Op&fsnotify.Write == fsnotify.Write 134 | 135 | case err, ok := <-watcher.Errors: 136 | if !ok { 137 | m.ErrorLog.Print("fsnotify watcher errors failed. message routines stopped.") 138 | m.Stop() 139 | return 140 | } 141 | m.checkErr(err, "fsnotify watcher") 142 | } 143 | } 144 | } 145 | 146 | func (m *Messages) checkForNewMessages() { 147 | db, err := m.getDB() 148 | if err != nil || db == nil { 149 | return // error 150 | } 151 | defer m.closeDB(db) 152 | sql := `SELECT m.rowid as rowid, handle.id as handle, m.text as text, m.service as service, ` + 153 | `CASE cache_has_attachments ` + 154 | `WHEN 0 THEN Null ` + 155 | `WHEN 1 THEN filename ` + 156 | `END AS attachment, ` + 157 | `CASE cache_has_attachments ` + 158 | `WHEN 0 THEN Null ` + 159 | `WHEN 1 THEN mime_type ` + 160 | `END as attachment_type ` + 161 | `FROM message AS m ` + 162 | `LEFT JOIN message_attachment_join AS maj ON message_id = m.rowid ` + 163 | `LEFT JOIN attachment AS a ON a.rowid = maj.attachment_id ` + 164 | `LEFT JOIN handle ON m.handle_id = handle.ROWID ` + 165 | `WHERE is_from_me=0 AND m.rowid > $id ORDER BY m.date ASC` 166 | query, _, err := db.PrepareTransient(sql) 167 | if err != nil { 168 | return 169 | } 170 | query.SetInt64("$id", m.currentID) 171 | 172 | for { 173 | if hasRow, err := query.Step(); err != nil { 174 | m.ErrorLog.Printf("%s: %q\n", sql, err) 175 | return 176 | } else if !hasRow { 177 | m.checkErr(query.Finalize(), "query reset") 178 | return 179 | } 180 | 181 | // Update Current ID (for the next SELECT), and send this message to the processors. 182 | m.currentID = query.GetInt64("rowid") 183 | 184 | // Extract the text from the incoming message. 185 | text := strings.TrimSpace(query.GetText("text")) 186 | 187 | // If there is an attachment, handle it. 188 | var attachData []byte 189 | attach, attachType := query.GetText("attachment"), query.GetText("attachment_type") 190 | if attach != "" && attachType != "" { 191 | attach = strings.ReplaceAll(attach, "~", m.Config.HomePath) 192 | f, err := os.Open(attach) 193 | if err != nil { 194 | m.ErrorLog.Printf("failed to open file %s: %v", attach, err) 195 | return 196 | } 197 | defer f.Close() 198 | attachData, err = io.ReadAll(f) 199 | if err != nil { 200 | m.ErrorLog.Printf("failed to read data from attachment: %v", err) 201 | return 202 | } 203 | } 204 | 205 | // Record the protocol used for the message. 206 | // This is used for replying to messages to make sure we send messages 207 | // over the same protocol that we are receiving them on. 208 | from := strings.TrimSpace(query.GetText("handle")) 209 | var proto protocol = imessage 210 | if query.GetText("service") == "SMS" { 211 | proto = sms 212 | } 213 | recordChatProtocol(from, proto) 214 | 215 | m.inChan <- Incoming{ 216 | RowID: m.currentID, 217 | From: from, 218 | Text: text, 219 | Attachment: attachData, 220 | AttachmentType: attachType, 221 | } 222 | } 223 | } 224 | 225 | // getCurrentID opens the iMessage DB and gets the last written / current ID. 226 | func (m *Messages) getCurrentID() error { 227 | sql := `SELECT MAX(rowid) AS id FROM message` 228 | db, err := m.getDB() 229 | if err != nil { 230 | return err 231 | } 232 | defer m.closeDB(db) 233 | query, _, err := db.PrepareTransient(sql) 234 | if err != nil { 235 | return err 236 | } 237 | 238 | m.DebugLog.Print("querying current id") 239 | if hasrow, err := query.Step(); err != nil { 240 | m.ErrorLog.Printf("%s: %q\n", sql, err) 241 | return err 242 | } else if !hasrow { 243 | _ = query.Finalize() 244 | return errors.New("no message rows found") 245 | } 246 | m.currentID = query.GetInt64("id") 247 | return query.Finalize() 248 | } 249 | 250 | // handleIncoming runs the call back funcs and notifies the call back channels. 251 | func (m *Messages) handleIncoming(msg Incoming) { 252 | m.DebugLog.Printf("new message id %d from: %s size: %d", msg.RowID, msg.From, len(msg.Text)) 253 | m.binds.RLock() 254 | defer m.binds.RUnlock() 255 | // Handle call back functions. 256 | for _, bind := range m.Funcs { 257 | if matched, err := regexp.MatchString(bind.Match, msg.Text); err != nil { 258 | m.ErrorLog.Printf("%s: %q\n", bind.Match, err) 259 | continue 260 | } else if !matched { 261 | continue 262 | } 263 | 264 | m.DebugLog.Printf("found matching message handler func: %v", bind.Match) 265 | go bind.Func(msg) 266 | } 267 | // Handle call back channels. 268 | for _, bind := range m.Chans { 269 | if matched, err := regexp.MatchString(bind.Match, msg.Text); err != nil { 270 | m.ErrorLog.Printf("%s: %q\n", bind.Match, err) 271 | continue 272 | } else if !matched { 273 | continue 274 | } 275 | 276 | m.DebugLog.Printf("found matching message handler chan: %v", bind.Match) 277 | bind.Chan <- msg 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /imessage/outgoing.go: -------------------------------------------------------------------------------- 1 | package imessage 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // OSAScriptPath is the path to the osascript binary. macOS only. 14 | var OSAScriptPath = "/usr/bin/osascript" 15 | 16 | // Outgoing struct is used to send a message to someone. 17 | // Fll it out and pass it into Messages.Send() to fire off a new iMessage. 18 | type Outgoing struct { 19 | ID string // ID is only used in logging and in the Response callback. 20 | To string // To represents the message recipient. 21 | Text string // Text is the body of the message or file path. 22 | File bool // If File is true, then Text is assume to be a filepath to send. 23 | Call func(*Response) // Call is the function that is run after a message is sent off. 24 | } 25 | 26 | // Response is the outgoing-message response provided to a callback function. 27 | // An outgoing callback function will receive this type. It represents "what happeened" 28 | // when trying to send a message. If `Sent` is false, `Errs` should contain error(s). 29 | type Response struct { 30 | ID string 31 | To string 32 | Text string 33 | Sent bool 34 | Errs []error 35 | } 36 | 37 | // Send is the method used to send an iMessage. Thread/routine safe. 38 | // The messages are queued in a channel and sent 1 at a time with a small 39 | // delay between. Each message may have a callback attached that is kicked 40 | // off in a go routine after the message is sent. 41 | func (m *Messages) Send(msg Outgoing) { 42 | m.outChan <- msg 43 | } 44 | 45 | // RunAppleScript runs a script on the local system. While not directly related to 46 | // iMessage and Messages.app, this library uses AppleScript to send messages using 47 | // imessage. To that end, the method to run scripts is also exposed for convenience. 48 | func (m *Messages) RunAppleScript(scripts []string) (success bool, errs []error) { 49 | arg := []string{OSAScriptPath} 50 | for _, s := range scripts { 51 | arg = append(arg, "-e", s) 52 | } 53 | m.DebugLog.Printf("AppleScript Command: %v", strings.Join(arg, " ")) 54 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(m.Config.Timeout)*time.Second) 55 | defer cancel() 56 | 57 | for i := 1; i <= m.Config.Retries && !success; i++ { 58 | if i > 1 { 59 | // we had an error, don't be so quick to try again. 60 | time.Sleep(500 * time.Millisecond) 61 | } 62 | var out bytes.Buffer 63 | cmd := exec.CommandContext(ctx, arg[0], arg[1:]...) 64 | cmd.Stdout = &out 65 | cmd.Stderr = &out 66 | 67 | if err := cmd.Run(); err != nil { 68 | errs = append(errs, fmt.Errorf("exec: %v: %v", err, out.String())) 69 | continue 70 | } 71 | success = true 72 | } 73 | return 74 | } 75 | 76 | // ClearMessages deletes all conversations in MESSAGES.APP. 77 | // Use this only if Messages is behaving poorly. Or, never use it at all. 78 | // This probably doesn't do anything you want to do. 79 | func (m *Messages) ClearMessages() error { 80 | arg := `tell application "Messages" 81 | activate 82 | try 83 | repeat (count of (get every chat)) times 84 | tell application "System Events" to tell process "Messages" to keystroke return 85 | delete item 1 of (get every chat) 86 | tell application "System Events" to tell process "Messages" to keystroke return 87 | end repeat 88 | end try 89 | close every window 90 | end tell 91 | ` 92 | if sent, err := m.RunAppleScript([]string{arg}); !sent && err != nil { 93 | return err[0] 94 | } 95 | time.Sleep(100 * time.Millisecond) 96 | return nil 97 | } 98 | 99 | // processOutgoingMessages keeps an eye out for outgoing messages; then processes them. 100 | func (m *Messages) processOutgoingMessages() { 101 | clearTicker := time.NewTicker(2 * time.Minute) 102 | defer clearTicker.Stop() 103 | newMsg := true 104 | for { 105 | select { 106 | case msg, ok := <-m.outChan: 107 | if !ok { 108 | return 109 | } 110 | newMsg = true 111 | response := m.sendiMessage(msg) 112 | if msg.Call != nil { 113 | go msg.Call(response) 114 | } 115 | case <-clearTicker.C: 116 | if m.ClearMsgs && newMsg { 117 | newMsg = false 118 | m.DebugLog.Print("Clearing Messages.app Conversations") 119 | m.checkErr(m.ClearMessages(), "clearing messages") 120 | } 121 | } 122 | } 123 | } 124 | 125 | // sendiMessage runs the applesripts to send a message and close the iMessage windows. 126 | func (m *Messages) sendiMessage(msg Outgoing) *Response { 127 | proto := getChatProtocol(msg.To) 128 | arg := []string{fmt.Sprintf(`tell application "Messages" to send "%s" to participant "%s" of (1st account whose service type = %s)`, msg.Text, msg.To, proto.String())} 129 | if _, err := os.Stat(msg.Text); err == nil && msg.File { 130 | arg = []string{fmt.Sprintf(`tell application "Messages" to send (POSIX file ("%s")) to participant "%s" of (1st account whose service type = %s)`, msg.Text, msg.To, proto.String())} 131 | } 132 | arg = append(arg, `tell application "Messages" to close every window`) 133 | sent, errs := m.RunAppleScript(arg) 134 | // Messages can go out so quickly we need to sleep a bit to avoid sending duplicates. 135 | time.Sleep(100 * time.Millisecond) 136 | return &Response{ID: msg.ID, To: msg.To, Text: msg.Text, Errs: errs, Sent: sent} 137 | } 138 | -------------------------------------------------------------------------------- /imessage/protocol.go: -------------------------------------------------------------------------------- 1 | package imessage 2 | 3 | // This file is used to differentiate between the different protocols available 4 | // (e.g. iMessage, SMS), and to store the protocol in use for a particular chat. 5 | 6 | type protocol int 7 | 8 | const ( 9 | imessage protocol = iota 10 | sms 11 | ) 12 | 13 | func (p protocol) String() string { 14 | switch p { 15 | case imessage: 16 | return "iMessage" 17 | case sms: 18 | return "SMS" 19 | default: 20 | panic("unknown protocol") 21 | } 22 | } 23 | 24 | var protocolCache = make(map[string]protocol) 25 | 26 | func recordChatProtocol(from string, proto protocol) { 27 | protocolCache[from] = proto 28 | } 29 | 30 | func getChatProtocol(to string) protocol { 31 | if v, ok := protocolCache[to]; ok { 32 | return v 33 | } 34 | return imessage 35 | } 36 | --------------------------------------------------------------------------------