Skip to content
Snippets Groups Projects
Commit a9a41c9d authored by Tulir Asokan's avatar Tulir Asokan :cat2:
Browse files

Initial commit with iMessage database reader and sending with AppleScript

parents
No related branches found
No related tags found
No related merge requests found
.idea
*.yaml
!example-config.yaml
*.session
*.json
*.db
*.log
/mautrix-imessage
This diff is collapsed.
go.mod 0 → 100644
module go.mau.fi/mautrix-imessage
go 1.14
require (
github.com/fsnotify/fsnotify v1.4.9
github.com/mattn/go-sqlite3 v1.14.6
//maunium.net/go/mauflag v1.0.0
//maunium.net/go/mautrix v0.8.0
)
go.sum 0 → 100644
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs=
golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/maulogger/v2 v2.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0XA=
maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.8.0 h1:G1jlVslNUTWEqaxuatHAMmzTWnGyoCIc4tAF5GpQJd8=
maunium.net/go/mautrix v0.8.0/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY=
// mautrix-imessage - A Matrix-iMessage puppeting bridge.
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package imessage
import (
"fmt"
"time"
)
type API interface {
Start() error
Stop()
GetMessages(chatID string, minDate time.Time) ([]Message, error)
MessageChan() <-chan Message
GetContactInfo(identifier string) *Contact
GetGroupMembers(chatID string) ([]string, error)
SendMessage(chatID, text string) error
}
var AppleEpoch = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
var Implementations = make(map[string]func() (API, error))
func NewAPI(platform string) (API, error) {
impl, ok := Implementations[platform]
if !ok {
return nil, fmt.Errorf("no such platform \"%s\"", platform)
}
return impl()
}
type Contact struct {
FirstName string
LastName string
Avatar []byte
Phones []string
Emails []string
}
func (contact *Contact) Name() string {
if len(contact.FirstName) > 0 {
if len(contact.LastName) > 0 {
return fmt.Sprintf("%s %s", contact.FirstName, contact.LastName)
} else {
return contact.FirstName
}
} else if len(contact.LastName) > 0 {
return contact.LastName
} else if len(contact.Emails) > 0 {
return contact.Emails[0]
} else if len(contact.Phones) > 0 {
return contact.Phones[0]
}
return ""
}
type Attachment interface {
GetMimeType() string
GetFileName() string
Read() ([]byte, error)
}
type Identifier struct {
LocalID string
Service string
}
type Message struct {
GUID string
Time time.Time
Subject string
Text string
Service string
ChatGUID string
Chat Identifier
Sender Identifier
IsFromMe bool
IsRead bool
IsDelivered bool
IsSent bool
IsEmote bool
IsAudioMessage bool
ThreadOriginatorGUID string
Attachment Attachment
}
// mautrix-imessage - A Matrix-iMessage puppeting bridge.
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package mac
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
"go.mau.fi/mautrix-imessage/imessage"
)
var phoneNumberCleaner = strings.NewReplacer("(", "", ")", "", " ", "", "-", "")
const contactInfoQuery = `
SELECT ZABCDRECORD.Z_PK, ZABCDPHONENUMBER.ZFULLNUMBER, ZABCDEMAILADDRESS.ZADDRESS, ZABCDRECORD.ZIMAGEDATA,
ZABCDRECORD.ZFIRSTNAME, ZABCDRECORD.ZLASTNAME
FROM ZABCDRECORD
LEFT JOIN ZABCDPHONENUMBER ON ZABCDRECORD.Z_PK = ZABCDPHONENUMBER.ZOWNER
LEFT JOIN ZABCDEMAILADDRESS ON ZABCDRECORD.Z_PK = ZABCDEMAILADDRESS.ZOWNER
`
func (imdb *Database) loadAddressBook() error {
path, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
addressBookDir := filepath.Join(path, "Library", "Application Support", "AddressBook", "Sources")
var addressDatabases []string
err = filepath.Walk(addressBookDir, func(path string, info os.FileInfo, err error) error {
name := info.Name()
if !info.IsDir() && strings.HasPrefix(name, "Address") && strings.HasSuffix(name, ".abcddb") {
addressDatabases = append(addressDatabases, path)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to walk address book directory: %w", err)
}
imdb.Contacts = make(map[string]*imessage.Contact)
for _, dbPath := range addressDatabases {
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=ro", dbPath))
if err != nil {
return fmt.Errorf("failed to open address book database: %w", err)
}
res, err := db.Query(contactInfoQuery)
if err != nil {
return fmt.Errorf("error querying address book database: %w", err)
}
contacts := make(map[int]*imessage.Contact)
for res.Next() {
var id int
var number, email, firstName, lastName sql.NullString
var avatar []byte
err = res.Scan(&id, &number, &email, &avatar, &firstName, &lastName)
if err != nil {
return fmt.Errorf("error scanning row: %w", err)
}
contact, ok := contacts[id]
if !ok {
contact = &imessage.Contact{FirstName: firstName.String, LastName: lastName.String, Avatar: avatar}
contacts[id] = contact
}
if number.Valid && len(number.String) > 0 {
numberStr := phoneNumberCleaner.Replace(number.String)
_, phoneExists := imdb.Contacts[numberStr]
if !phoneExists {
contact.Phones = append(contact.Phones, numberStr)
imdb.Contacts[numberStr] = contact
}
}
if email.Valid && len(number.String) > 0 {
_, emailExists := imdb.Contacts[email.String]
if !emailExists {
contact.Emails = append(contact.Emails, email.String)
imdb.Contacts[email.String] = contact
}
}
}
}
return nil
}
func (imdb *Database) GetContactInfo(identifier string) *imessage.Contact {
contact, ok := imdb.Contacts[identifier]
if !ok {
return nil
}
return contact
}
// mautrix-imessage - A Matrix-iMessage puppeting bridge.
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package mac
import (
"database/sql"
"fmt"
"go.mau.fi/mautrix-imessage/imessage"
)
type Database struct {
chatDBPath string
chatDB *sql.DB
messagesQuery *sql.Stmt
Messages chan imessage.Message
stopWatching chan struct{}
ppDB *sql.DB
groupMemberQuery *sql.Stmt
Contacts map[string]*imessage.Contact
}
func NewChatDatabase() (imessage.API, error) {
imdb := &Database{}
err := imdb.prepareMessages()
if err != nil {
return nil, fmt.Errorf("failed to open message database: %w", err)
}
err = imdb.prepareGroups()
if err != nil {
return nil, fmt.Errorf("failed to open group database: %w", err)
}
err = imdb.loadAddressBook()
if err != nil {
return nil, fmt.Errorf("failed to read address book: %w", err)
}
return imdb, nil
}
func init() {
imessage.Implementations["mac"] = NewChatDatabase
}
// mautrix-imessage - A Matrix-iMessage puppeting bridge.
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package mac
import (
"database/sql"
"fmt"
"os"
"path/filepath"
)
const groupMemberQuery = `
SELECT value FROM cn_handles
JOIN cn_handles_sources ON cn_handles.id = cn_handles_sources.cn_handle_id
JOIN sources ON cn_handles_sources.source_id = sources.id
WHERE sources.group_id = $1
`
func (imdb *Database) prepareGroups() error {
path, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
ppPath := filepath.Join(path, "Library", "PersonalizationPortrait", "PPSQLDatabase.db")
imdb.ppDB, err = sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=ro", ppPath))
if err != nil {
return err
}
imdb.groupMemberQuery, err = imdb.ppDB.Prepare(groupMemberQuery)
if err != nil {
return fmt.Errorf("failed to prepare group member query: %w", err)
}
return nil
}
func (imdb *Database) GetGroupMembers(chatID string) ([]string, error) {
res, err := imdb.groupMemberQuery.Query(chatID)
if err != nil {
return nil, fmt.Errorf("error querying group members: %w", err)
}
var users []string
for res.Next() {
var user string
err = res.Scan(&user)
if err != nil {
return users, fmt.Errorf("error scanning row: %w", err)
}
if user[0] == '+' {
user = phoneNumberCleaner.Replace(user)
}
users = append(users, user)
}
return users, nil
}
// mautrix-imessage - A Matrix-iMessage puppeting bridge.
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package mac
import (
"database/sql"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsnotify"
_ "github.com/mattn/go-sqlite3"
"go.mau.fi/mautrix-imessage/imessage"
)
const messagesQuery = `
SELECT
message.guid, message.date, COALESCE(message.subject, ''), COALESCE(message.text, ''), message.service, chat.guid, chat.chat_identifier, chat.service_name,
handle.id, handle.service, message.is_from_me, message.is_read, message.is_delivered, message.is_sent, message.is_emote, message.is_audio_message,
COALESCE(message.thread_originator_guid, ''),
COALESCE(attachment.filename, ''), COALESCE(attachment.mime_type, ''), COALESCE(attachment.transfer_name, '')
FROM message
JOIN chat_message_join ON chat_message_join.message_id = message.ROWID
JOIN chat ON chat_message_join.chat_id = chat.ROWID
JOIN handle ON message.handle_id = handle.ROWID
LEFT JOIN message_attachment_join ON message_attachment_join.message_id = message.ROWID
LEFT JOIN attachment ON message_attachment_join.attachment_id = attachment.ROWID
WHERE (chat.guid=$1 OR $1='') AND message.date>$2
ORDER BY message.date ASC
`
func (imdb *Database) prepareMessages() error {
path, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
imdb.chatDBPath = filepath.Join(path, "Library", "Messages", "chat.db")
imdb.chatDB, err = sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=ro", imdb.chatDBPath))
if err != nil {
return err
}
imdb.messagesQuery, err = imdb.chatDB.Prepare(messagesQuery)
if err != nil {
return fmt.Errorf("failed to prepare message query: %w", err)
}
messageChan := make(chan imessage.Message)
imdb.Messages = messageChan
return nil
}
type AttachmentInfo struct {
FileName string
MimeType string
TransferName string
}
func (ai *AttachmentInfo) GetMimeType() string {
return ai.MimeType
}
func (ai *AttachmentInfo) GetFileName() string {
return ai.TransferName
}
func (ai *AttachmentInfo) Read() ([]byte, error) {
return ioutil.ReadFile(ai.FileName)
}
func (imdb *Database) GetMessages(chatID string, minDate time.Time) ([]imessage.Message, error) {
res, err := imdb.messagesQuery.Query(chatID, minDate.UnixNano()-imessage.AppleEpoch.UnixNano())
if err != nil {
return nil, fmt.Errorf("error querying messages: %w", err)
}
var messages []imessage.Message
for res.Next() {
var message imessage.Message
var attachment AttachmentInfo
var timestamp int64
err = res.Scan(&message.GUID, &timestamp, &message.Subject, &message.Text, &message.Service, &message.ChatGUID,
&message.Chat.LocalID, &message.Chat.Service, &message.Sender.LocalID, &message.Sender.Service,
&message.IsFromMe, &message.IsRead, &message.IsDelivered, &message.IsSent, &message.IsEmote, &message.IsAudioMessage,
&message.ThreadOriginatorGUID,
&attachment.FileName, &attachment.MimeType, &attachment.TransferName)
if err != nil {
return messages, fmt.Errorf("error scanning row: %w", err)
}
message.Time = time.Unix(imessage.AppleEpoch.Unix(), timestamp)
if len(attachment.FileName) > 0 {
message.Attachment = &attachment
}
messages = append(messages, message)
}
return messages, nil
}
func (imdb *Database) Stop() {
imdb.stopWatching <- struct{}{}
}
func (imdb *Database) MessageChan() <-chan imessage.Message {
return imdb.Messages
}
func (imdb *Database) Start() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create fsnotify watcher: %w", err)
}
defer watcher.Close()
stop := make(chan struct{}, 1)
imdb.stopWatching = stop
err = watcher.Add(filepath.Dir(imdb.chatDBPath))
if err != nil {
return fmt.Errorf("failed to add chat DB to fsnotify watcher: %w", err)
}
var dropEvents bool
var handleLock sync.Mutex
nonSentMessages := make(map[string]bool)
lastMessageTimestamp := time.Now()
Loop:
for {
select {
case _, ok := <-watcher.Events:
if !ok {
break Loop
} else if dropEvents {
continue
}
dropEvents = true
go func() {
handleLock.Lock()
defer handleLock.Unlock()
time.Sleep(50 * time.Millisecond)
newMessages, err := imdb.GetMessages("", lastMessageTimestamp)
if err != nil {
fmt.Println("Error reading messages after fsevent:", err)
//return fmt.Errorf("error reading messages after fsevent: %w", err)
}
dropEvents = false
for _, message := range newMessages {
if message.Time.After(lastMessageTimestamp) {
lastMessageTimestamp = message.Time
}
if !message.IsSent {
nonSentMessages[message.GUID] = true
} else if _, ok := nonSentMessages[message.GUID]; ok {
delete(nonSentMessages, message.GUID)
continue
}
imdb.Messages <- message
}
}()
case err := <-watcher.Errors:
return fmt.Errorf("error in watcher: %w", err)
case <-stop:
break Loop
}
}
return nil
}
// mautrix-imessage - A Matrix-iMessage puppeting bridge.
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package mac
import (
"fmt"
"os"
"os/exec"
)
const sendMessage = `
on run {targetChatId, targetMessage}
tell application "Messages"
set theBuddy to a reference to chat id targetChatId
send targetMessage to theBuddy
end tell
end run
`
func (imdb *Database) SendMessage(chatID, text string) error {
cmd := exec.Command("osascript", "-", chatID, text)
// TODO make these go somewhere else
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("failed to open stdin pipe: %w", err)
}
err = cmd.Start()
if err != nil {
return fmt.Errorf("failed to run osascript: %w", err)
}
_, err = stdin.Write([]byte(sendMessage))
if err != nil {
return fmt.Errorf("failed to send script to osascript: %w", err)
}
err = stdin.Close()
if err != nil {
return fmt.Errorf("failed to close stdin pipe: %w", err)
}
err = cmd.Wait()
if err != nil {
return fmt.Errorf("failed to wait for osascript: %w", err)
}
return nil
}
// mautrix-imessage - A Matrix-iMessage puppeting bridge.
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"go.mau.fi/mautrix-imessage/imessage"
_ "go.mau.fi/mautrix-imessage/imessage/mac"
)
func printMessage(db imessage.API, message imessage.Message) {
var sender string
contact := db.GetContactInfo(message.Sender.LocalID)
if contact != nil {
sender = contact.Name()
}
if len(sender) == 0 {
sender = message.Sender.LocalID
}
if message.IsFromMe {
if message.Chat.LocalID == message.Sender.LocalID {
sender = fmt.Sprintf("you -> %s", sender)
} else {
sender = fmt.Sprintf("you -> %v", message.Chat)
}
} else if message.Chat.LocalID == message.Sender.LocalID {
sender = fmt.Sprintf("%s -> you", sender)
} else {
sender = fmt.Sprintf("%s -> %v", sender, message.Chat)
}
fmt.Printf("%s <%s> %s\n", message.Time.Format("2006-01-02 15:04:05"), sender, strings.ReplaceAll(message.Text, "\n", "\\n"))
if message.Attachment != nil {
fmt.Println(message.Attachment)
}
}
func read(r io.Reader) <-chan string {
lines := make(chan string)
go func() {
defer close(lines)
scan := bufio.NewScanner(r)
for scan.Scan() {
lines <- scan.Text()
}
}()
return lines
}
func main() {
db, err := imessage.NewAPI("mac")
if err != nil {
fmt.Println(err)
return
}
//for _, contact := range db.Contacts {
// fmt.Println(contact.FirstName, contact.LastName, contact.Phones, contact.Emails, http.DetectContentType(contact.Avatar))
//}
messages, err := db.GetMessages("", imessage.AppleEpoch)
if err != nil {
fmt.Println(err)
return
}
var lastChatID string
for _, message := range messages {
printMessage(db, message)
lastChatID = message.ChatGUID
}
go func() {
err := db.Start()
fmt.Println("Watcher error:", err)
}()
fmt.Println("Listening to new messages")
messageChan := db.MessageChan()
stdin := read(os.Stdin)
for {
select {
case message := <-messageChan:
printMessage(db, message)
lastChatID = message.ChatGUID
case input := <-stdin:
err = db.SendMessage(lastChatID, input)
if err != nil {
fmt.Println("Error sending message:", err)
} else {
fmt.Println("Message sent to", lastChatID)
}
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment