Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bot Commands #179

Merged
merged 4 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// mautrix-imessage - A Matrix-iMessage puppeting bridge.
// Copyright (C) 2022 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 (
"fmt"
"strings"

"go.mau.fi/mautrix-imessage/imessage"
"maunium.net/go/mautrix/bridge/commands"
)

var (
HelpSectionCreatingPortals = commands.HelpSection{Name: "Creating portals", Order: 15}
)

type WrappedCommandEvent struct {
*commands.Event
Bridge *IMBridge
User *User
Portal *Portal
}

func (br *IMBridge) RegisterCommands() {
proc := br.CommandProcessor.(*commands.Processor)
proc.AddHandlers(
cmdPM,
cmdSearchContacts,
)
}

func wrapCommand(handler func(*WrappedCommandEvent)) func(*commands.Event) {
return func(ce *commands.Event) {
user := ce.User.(*User)
var portal *Portal
if ce.Portal != nil {
portal = ce.Portal.(*Portal)
}
br := ce.Bridge.Child.(*IMBridge)
handler(&WrappedCommandEvent{ce, br, user, portal})
}
}

var cmdPM = &commands.FullHandler{
Func: wrapCommand(fnPM),
Name: "pm",
Help: commands.HelpMeta{
Section: HelpSectionCreatingPortals,
Description: "Creates a new PM with the specified number or address.",
},
RequiresPortal: false,
RequiresLogin: false,
}

func fnPM(ce *WrappedCommandEvent) {
ce.Bridge.ZLog.Trace().Interface("args", ce.Args).Str("cmd", ce.Command).Msg("fnPM")

if len(ce.Args) == 0 {
ce.Reply("**Usage:** `pm <international phone number>` OR `pm <apple id email address>`")
return
}

startedDm, err := ce.Bridge.WebsocketHandler.StartChat(*&StartDMRequest{
Identifier: ce.RawArgs,
Force: false,
ActuallyStart: true,
})

if err != nil {
ce.Reply("Failed to start PM: %s", err)
} else {
ce.Reply("Created portal room [%s](%s) and invited you to it.", startedDm.RoomID, startedDm.RoomID.URI(ce.Bridge.Config.Homeserver.Domain).MatrixToURL())
}
}

var cmdSearchContacts = &commands.FullHandler{
Func: wrapCommand(fnSearchContacts),
Name: "search-contacts",
Help: commands.HelpMeta{
Section: HelpSectionCreatingPortals,
Description: "Searches contacts based on name, phone, and email.",
},
RequiresPortal: false,
RequiresLogin: false,
}

func fnSearchContacts(ce *WrappedCommandEvent) {
ce.Bridge.ZLog.Trace().Interface("args", ce.Args).Str("cmd", ce.Command).Msg("fnSearchContacts")

if len(ce.Args) == 0 {
ce.Reply("**Usage:** `search-contacts <search terms>`")
return
}

contacts, err := ce.Bridge.IM.SearchContactList(ce.RawArgs)
if err != nil {
ce.Reply("Failed to search contacts: %s", err)
} else {
if contacts == nil || len(contacts) == 0 {
ce.Reply("No contacts found for search `%s`", ce.RawArgs)
} else {
replyMessage := fmt.Sprintf("Found %d contacts:\n", len(contacts))

for _, contact := range contacts {
markdownString := buildContactString(contact)
replyMessage += markdownString
replyMessage += strings.Repeat("-", 40) + "\n"
}

ce.Reply(replyMessage)
}
}
}

func buildContactString(contact *imessage.Contact) string {
name := contact.Nickname
if name == "" {
name = fmt.Sprintf("%s %s", contact.FirstName, contact.LastName)
}

contactInfo := fmt.Sprintf("**%s**\n", name)

if len(contact.Phones) > 0 {
contactInfo += "- **Phones:**\n"
for _, phone := range contact.Phones {
contactInfo += fmt.Sprintf(" - %s\n", phone)
}
}

if len(contact.Emails) > 0 {
contactInfo += "- **Emails:**\n"
for _, email := range contact.Emails {
contactInfo += fmt.Sprintf(" - %s\n", email)
}
}

return contactInfo
}

// TODO: potentially add the following commands
// start-group-chat
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/strukturag/libheif v1.17.6 h1:UFz4FI7kKLINWyL7bcNEBu4gZxK7rHRkwq49IOzHyvE=
github.com/strukturag/libheif v1.17.6/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks=
Expand Down
108 changes: 76 additions & 32 deletions imessage/bluebubbles/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"github.com/sahilm/fuzzy"
"maunium.net/go/mautrix/id"

"go.mau.fi/mautrix-imessage/imessage"
Expand Down Expand Up @@ -49,6 +50,9 @@ type blueBubbles struct {
contactChan chan *imessage.Contact
messageStatusChan chan *imessage.SendMessageStatus
backfillTaskChan chan *imessage.BackfillTask

contactsLastRefresh time.Time
contacts []Contact
}

func NewBlueBubblesConnector(bridge imessage.Bridge) (imessage.API, error) {
Expand Down Expand Up @@ -629,7 +633,7 @@ func (bb *blueBubbles) matchHandleToContact(address string) *Contact {

var contact *Contact

bb.getContactList()
contacts := bb.getContactList()

numericAddress := numericOnly(address)

Expand Down Expand Up @@ -669,21 +673,52 @@ func (bb *blueBubbles) matchHandleToContact(address string) *Contact {
return contact
}

func (bb *blueBubbles) SearchContactList(input string) ([]*imessage.Contact, error) {
bb.log.Trace().Str("input", input).Msg("SearchContactList")

var matchedContacts []*imessage.Contact

contacts := bb.getContactList()

for _, contact := range contacts {

contactFields := []string{
strings.ToLower(contact.FirstName + " " + contact.LastName),
strings.ToLower(contact.DisplayName),
strings.ToLower(contact.Nickname),
strings.ToLower(contact.Nickname),
strings.ToLower(contact.Nickname),
}

for _, phoneNumber := range contact.PhoneNumbers {
contactFields = append(contactFields, phoneNumber.Address)
}

for _, email := range contact.Emails {
contactFields = append(contactFields, email.Address)
}

matches := fuzzy.Find(strings.ToLower(input), contactFields)

bb.log.Trace().Interface("matches", matches).Str("input", input).Str("name", contact.FirstName+" "+contact.LastName).Msg("Fuzzy Match test")

if len(matches) > 0 { //&& matches[0].Score >= 0
imessageContact, _ := bb.convertBBContactToiMessageContact(contact)
matchedContacts = append(matchedContacts, imessageContact)
continue
}
}

return matchedContacts, nil
}

func (bb *blueBubbles) GetContactInfo(identifier string) (resp *imessage.Contact, err error) {
bb.log.Trace().Str("identifier", identifier).Msg("GetContactInfo")

contact := bb.matchHandleToContact(identifier)

// Convert to imessage.Contact type
if contact != nil {
resp = &imessage.Contact{
FirstName: contact.FirstName,
LastName: contact.LastName,
Nickname: contact.Nickname,
Phones: convertPhones(contact.PhoneNumbers),
Emails: convertEmails(contact.Emails),
UserGUID: contact.ID,
}
resp, _ = bb.convertBBContactToiMessageContact(*contact)
return resp, nil

}
Expand All @@ -697,49 +732,40 @@ func (bb *blueBubbles) GetContactList() (resp []*imessage.Contact, err error) {

contactResponse := bb.getContactList()

// Convert to imessage.Contact type
for _, contact := range contactResponse {
imessageContact := &imessage.Contact{
FirstName: contact.FirstName,
LastName: contact.LastName,
Nickname: contact.Nickname,
Phones: convertPhones(contact.PhoneNumbers),
Emails: convertEmails(contact.Emails),
UserGUID: contact.ID,
}
imessageContact, _ := bb.convertBBContactToiMessageContact(contact)
resp = append(resp, imessageContact)
}

return resp, nil
}

var contactsLastRefresh time.Time
var contacts []Contact

// Updates the cache if necessary, and returns the list
func (bb *blueBubbles) getContactList() (contacts []Contact) {
func (bb *blueBubbles) getContactList() []Contact {

if contacts == nil {
bb.refreshContactsList()
} else if contactsLastRefresh.Add(1*time.Hour).Compare(time.Now()) < 0 { // if the last refresh was > 1 hour ago
if bb.contacts == nil ||
bb.contactsLastRefresh.Add(1*time.Hour).Compare(time.Now()) < 0 {
bb.refreshContactsList()
}

return contacts

return bb.contacts
}

func (bb *blueBubbles) refreshContactsList() error {
bb.log.Trace().Msg("refreshContactsList")

var contactResponse ContactResponse

err := bb.apiGet("/api/v1/contact", nil, &contactResponse)
if err != nil {
return err
}

bb.log.Trace().Int("bbContactCount", len(contactResponse.Data)).Msg("refreshContactsList")

// save contacts for later
contacts = contactResponse.Data
contactsLastRefresh = time.Now()
bb.contacts = contactResponse.Data
bb.contactsLastRefresh = time.Now()

return nil
}
Expand Down Expand Up @@ -1037,12 +1063,18 @@ func (bb *blueBubbles) SendTypingNotification(chatID string, typing bool) error

func (bb *blueBubbles) ResolveIdentifier(identifier string) (string, error) {
bb.log.Trace().Str("identifier", identifier).Msg("ResolveIdentifier")
return "", ErrNotImplemented

// if the identifier is a phone number, remove dashes and prepend the +
if !strings.Contains(identifier, "@") {
identifier = "+" + numericOnly(identifier)
}

return "iMessage;-;" + identifier, nil
}

func (bb *blueBubbles) PrepareDM(guid string) error {
bb.log.Trace().Str("guid", guid).Msg("PrepareDM")
return ErrNotImplemented
return nil
}

func (bb *blueBubbles) CreateGroup(users []string) (*imessage.CreateGroupResponse, error) {
Expand Down Expand Up @@ -1227,6 +1259,18 @@ func (bb *blueBubbles) apiPostAsFormData(path string, formData map[string]interf
return nil
}

func (bb *blueBubbles) convertBBContactToiMessageContact(bbContact Contact) (*imessage.Contact, error) {
return &imessage.Contact{
FirstName: bbContact.FirstName,
LastName: bbContact.LastName,
Nickname: bbContact.Nickname,
Phones: convertPhones(bbContact.PhoneNumbers),
Emails: convertEmails(bbContact.Emails),
UserGUID: bbContact.ID,
// TODO Avatar: ,
}, nil
}

func (bb *blueBubbles) convertBBMessageToiMessage(bbMessage Message) (*imessage.Message, error) {

var message imessage.Message
Expand Down
1 change: 1 addition & 0 deletions imessage/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var (
type ContactAPI interface {
GetContactInfo(identifier string) (*Contact, error)
GetContactList() ([]*Contact, error)
SearchContactList(searchTerms string) ([]*Contact, error)
}

type ChatInfoAPI interface {
Expand Down
5 changes: 5 additions & 0 deletions imessage/ios/ipc.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package ios
import (
"context"
"encoding/json"
"errors"
"math"
"os"
"strings"
Expand Down Expand Up @@ -476,6 +477,10 @@ func (ios *iOSConnector) GetContactList() ([]*imessage.Contact, error) {
return resp.Contacts, err
}

func (ios *iOSConnector) SearchContactList(searchTerms string) ([]*imessage.Contact, error) {
return nil, errors.New("not implemented")
}

func (ios *iOSConnector) GetChatInfo(chatID, threadID string) (*imessage.ChatInfo, error) {
var resp imessage.ChatInfo
err := ios.IPC.Request(context.Background(), ReqGetChat, &GetChatRequest{ChatGUID: chatID, ThreadID: threadID}, &resp)
Expand Down
Loading