diff --git a/go.mod b/go.mod index ffe8c28..ec5379b 100644 --- a/go.mod +++ b/go.mod @@ -8,20 +8,20 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 github.com/forPelevin/gomoji v1.1.8 - github.com/go-co-op/gocron v1.34.2 + github.com/go-co-op/gocron v1.35.0 github.com/jackc/pgx/v5 v5.4.3 github.com/kolesa-team/go-webp v1.0.4 github.com/lithammer/fuzzysearch v1.1.8 github.com/mattn/go-sqlite3 v1.14.17 github.com/mdp/qrterminal/v3 v3.1.1 - go.mau.fi/whatsmeow v0.0.0-20230926223531-00abc29ba510 + go.mau.fi/whatsmeow v0.0.0-20230929093856-69d5ba6fa3e3 go.uber.org/zap v1.26.0 - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.1 gorm.io/driver/postgres v1.5.2 - gorm.io/driver/sqlite v1.5.3 + gorm.io/driver/sqlite v1.5.4 gorm.io/gorm v1.25.4 ) @@ -44,7 +44,7 @@ require ( go.mau.fi/util v0.1.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.13.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/text v0.13.0 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 5434830..4a3b4e6 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/go-co-op/gocron v1.31.2 h1:tAUW64bxYc5QlzEy2t30TnHX2+uInNDajKXxWi4SAC github.com/go-co-op/gocron v1.31.2/go.mod h1:39f6KNSGVOU1LO/ZOoZfcSxwlsJDQOKSu8erN0SH48Y= github.com/go-co-op/gocron v1.34.2 h1:vI/Up5gQDogTF7VIQQ1ynwkVDIuUwQ0oPhDR13/X/KM= github.com/go-co-op/gocron v1.34.2/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= +github.com/go-co-op/gocron v1.35.0 h1:niC91OHiSEimXgPPay02AI1gLGL4JGBgDzmWtgZ8n5A= +github.com/go-co-op/gocron v1.35.0/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= @@ -94,6 +96,8 @@ go.mau.fi/whatsmeow v0.0.0-20230817083005-1c185f033d88 h1:OyWiw4j6s8unaOhRuIYO1p go.mau.fi/whatsmeow v0.0.0-20230817083005-1c185f033d88/go.mod h1:Iv3G4uv6+HWtqL7XSLRa2dSy077Bnji14IvqUbG+bRo= go.mau.fi/whatsmeow v0.0.0-20230926223531-00abc29ba510 h1:qLaBZGD5I4cyM4DWs1JKxONXnLCfI6H3XCo3aaoII1M= go.mau.fi/whatsmeow v0.0.0-20230926223531-00abc29ba510/go.mod h1:1xFS2b5zqsg53ApsYB4FDtko7xG7r+gVgBjh9k+9/GE= +go.mau.fi/whatsmeow v0.0.0-20230929093856-69d5ba6fa3e3 h1:BLF1MlV4EBHyvaZDvngM2e0Hnsk0o991G3guN0dbWVU= +go.mau.fi/whatsmeow v0.0.0-20230929093856-69d5ba6fa3e3/go.mod h1:1xFS2b5zqsg53ApsYB4FDtko7xG7r+gVgBjh9k+9/GE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -110,10 +114,16 @@ golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA= golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20231005195138-3e424a577f31 h1:9k5exFQKQglLo+RoP+4zMjOFE14P6+vyR0baDAi0Rcs= +golang.org/x/exp v0.0.0-20231005195138-3e424a577f31/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -166,6 +176,8 @@ gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g= gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= +gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.3 h1:zi4rHZj1anhZS2EuEODMhDisGy+Daq9jtPrNGgbQYD8= gorm.io/gorm v1.25.3/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= diff --git a/sample_config.yaml b/sample_config.yaml index 35f3400..c7414e0 100644 --- a/sample_config.yaml +++ b/sample_config.yaml @@ -48,6 +48,8 @@ whatsapp: skip_status: false skip_contacts: false skip_locations: false + skip_profile_picture_updates: false + skip_group_settings_updates: false # This includes joins, leaves, name change, etc. skip_chat_details: true send_revoked_message_updates: false whatsmeow_debug_mode: false diff --git a/state/config.go b/state/config.go index 9d29639..dfff780 100644 --- a/state/config.go +++ b/state/config.go @@ -56,6 +56,8 @@ type Config struct { SkipStickers bool `yaml:"skip_stickers"` SkipContacts bool `yaml:"skip_contacts"` SkipLocations bool `yaml:"skip_locations"` + SkipProfilePictureUpdates bool `yaml:"skip_profile_picture_updates"` + SkipGroupSettingsUpdates bool `yaml:"skip_group_settings_updates"` SkipChatDetails bool `yaml:"skip_chat_details"` SendRevokedMessageUpdates bool `yaml:"send_revoked_message_updates"` WhatsmeowDebugMode bool `yaml:"whatsmeow_debug_mode"` diff --git a/state/state.go b/state/state.go index be8c32b..cd0846b 100644 --- a/state/state.go +++ b/state/state.go @@ -10,7 +10,7 @@ import ( "gorm.io/gorm" ) -const WATGBRIDGE_VERSION = "1.5.0" +const WATGBRIDGE_VERSION = "1.6.0" type state struct { Config *Config diff --git a/telegram/handlers.go b/telegram/handlers.go index e8d24b7..f14ccfc 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -308,7 +308,7 @@ func UpdateAndRestartHandler(b *gotgbot.Bot, c *ext.Context) error { RELEASE_URL_FORMAT := "https://github.com/akshettrj/watgbridge/releases/latest/download/watgbridge_linux_%s" url := fmt.Sprintf(RELEASE_URL_FORMAT, cfg.Architecture) - err := utils.DownloadFileByURL("watgbridge_temp", url) + err := utils.DownloadFileToLocalByURL("watgbridge_temp", url) if err != nil { return utils.TgReplyWithErrorByContext(b, c, "Failed to download the release", err) } diff --git a/utils/net.go b/utils/net.go index 3ce4288..8159475 100644 --- a/utils/net.go +++ b/utils/net.go @@ -6,7 +6,17 @@ import ( "os" ) -func DownloadFileByURL(filepath string, url string) error { +func DownloadFileBytesByURL(url string) ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) +} + +func DownloadFileToLocalByURL(filepath string, url string) error { resp, err := http.Get(url) if err != nil { return err diff --git a/whatsapp/handlers.go b/whatsapp/handlers.go index 2829008..633ea42 100644 --- a/whatsapp/handlers.go +++ b/whatsapp/handlers.go @@ -14,6 +14,7 @@ import ( "github.com/PaulSonOfLars/gotgbot/v2" goVCard "github.com/emersion/go-vcard" + "go.mau.fi/whatsmeow" waProto "go.mau.fi/whatsmeow/binary/proto" waTypes "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" @@ -24,11 +25,23 @@ import ( func WhatsAppEventHandler(evt interface{}) { + cfg := state.State.Config + switch v := evt.(type) { case *events.Receipt: ReceiptEventHandler(v) + case *events.Picture: + if !cfg.WhatsApp.SkipProfilePictureUpdates { + PictureEventHandler(v) + } + + case *events.GroupInfo: + if !cfg.WhatsApp.SkipGroupSettingsUpdates { + GroupInfoEventHandler(v) + } + case *events.PushName: PushNameEventHandler(v) @@ -1177,3 +1190,328 @@ func RevokedMessageEventHandler(v *events.Message) { ReplyToMessageId: tgMsgId, }) } + +func PictureEventHandler(v *events.Picture) { + var ( + cfg = state.State.Config + logger = state.State.Logger + tgBot = state.State.TelegramBot + waClient = state.State.WhatsAppClient + ) + defer logger.Sync() + + tgThreadId, threadFound, err := database.ChatThreadGetTgFromWa(v.JID.ToNonAD().String(), cfg.Telegram.TargetChatID) + if err != nil { + err = utils.TgSendTextById( + tgBot, cfg.Telegram.OwnerID, 0, + fmt.Sprintf( + "Warning: Chat thread could not be found for %s:\n\n%s", + v.JID.String(), html.EscapeString(err.Error()), + ), + ) + if err != nil { + logger.Error("failed to send message to owner", zap.Error(err)) + } + return + } + if !threadFound || tgThreadId == 0 { + err = utils.TgSendTextById( + tgBot, cfg.Telegram.OwnerID, 0, + fmt.Sprintf("Warning: Not chat thread found for %s", v.JID.String()), + ) + if err != nil { + logger.Error("failed to send message to owner", zap.Error(err)) + } + return + } + + if v.JID.Server == waTypes.GroupServer { + changer := utils.WaGetContactName(v.Author) + if v.Remove { + updateText := fmt.Sprintf("The profile picture was removed by %s", html.EscapeString(changer)) + err = utils.TgSendTextById( + tgBot, cfg.Telegram.TargetChatID, tgThreadId, + updateText, + ) + if err != nil { + logger.Error("failed to send message to the target chat", zap.Error(err)) + return + } + } else { + pictureInfo, err := waClient.GetProfilePictureInfo( + v.JID, + &whatsmeow.GetProfilePictureParams{ + Preview: false, + }, + ) + if err != nil { + logger.Error("failed to get profile picture info", zap.Error(err), zap.String("group", v.JID.String())) + return + } + if pictureInfo == nil { + logger.Error("failed to get profile picture info, received null", zap.String("group", v.JID.String())) + return + } + + newPictureBytes, err := utils.DownloadFileBytesByURL(pictureInfo.URL) + if err != nil { + logger.Error("failed to download profile picture", zap.Error(err), zap.String("group", v.JID.String())) + return + } + + _, err = tgBot.SendPhoto(cfg.Telegram.TargetChatID, newPictureBytes, &gotgbot.SendPhotoOpts{ + MessageThreadId: tgThreadId, + Caption: fmt.Sprintf("The profile picture was updated by %s", html.EscapeString(changer)), + }) + if err != nil { + logger.Error("failed to send message to the group", zap.Error(err)) + return + } + } + } else if v.JID.Server == waTypes.DefaultUserServer { + if v.Remove { + updateText := fmt.Sprintf("The profile picture was removed") + err = utils.TgSendTextById( + tgBot, cfg.Telegram.TargetChatID, tgThreadId, + updateText, + ) + if err != nil { + logger.Error("failed to send message to the target chat", zap.Error(err)) + return + } + } else { + pictureInfo, err := waClient.GetProfilePictureInfo( + v.JID, + &whatsmeow.GetProfilePictureParams{ + Preview: false, + }, + ) + if err != nil { + logger.Error("failed to get profile picture info", zap.Error(err), zap.String("group", v.JID.String())) + return + } + if pictureInfo == nil { + logger.Error("failed to get profile picture info, received null", zap.String("group", v.JID.String())) + return + } + + newPictureBytes, err := utils.DownloadFileBytesByURL(pictureInfo.URL) + if err != nil { + logger.Error("failed to download profile picture", zap.Error(err), zap.String("group", v.JID.String())) + return + } + + _, err = tgBot.SendPhoto(cfg.Telegram.TargetChatID, newPictureBytes, &gotgbot.SendPhotoOpts{ + MessageThreadId: tgThreadId, + Caption: "The profile picture was updated", + }) + if err != nil { + logger.Error("failed to send message to the group", zap.Error(err)) + return + } + } + } else { + logger.Warn( + "Received Picture event for unknown JID type", + zap.String("jid", v.JID.String()), + ) + } +} + +func GroupInfoEventHandler(v *events.GroupInfo) { + var ( + cfg = state.State.Config + logger = state.State.Logger + tgBot = state.State.TelegramBot + ) + defer logger.Sync() + + tgThreadId, threadFound, err := database.ChatThreadGetTgFromWa(v.JID.String(), cfg.Telegram.TargetChatID) + if err != nil { + err = utils.TgSendTextById( + tgBot, cfg.Telegram.OwnerID, 0, + fmt.Sprintf( + "Warning: Chat thread could not be found for %s:\n\n%s", + v.JID.String(), html.EscapeString(err.Error()), + ), + ) + if err != nil { + logger.Error("failed to send message to owner", zap.Error(err)) + } + return + } + if !threadFound || tgThreadId == 0 { + err = utils.TgSendTextById( + tgBot, cfg.Telegram.OwnerID, 0, + fmt.Sprintf("Warning: Not chat thread found for %s", v.JID.String()), + ) + if err != nil { + logger.Error("failed to send message to owner", zap.Error(err)) + } + return + } + + if v.Announce != nil { + var updateText string + if v.Announce.IsAnnounce { + updateText = "Group settings have been changed, only admins can send messages now" + } else { + updateText = "Group settings have been changed, everybody can send messages now" + } + err = utils.TgSendTextById(tgBot, cfg.Telegram.TargetChatID, tgThreadId, updateText) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } + } + + if v.Ephemeral != nil { + var updateText string + if v.Ephemeral.IsEphemeral { + updateText = "Group's auto deletion timer has been turned on:\n" + updateText += fmt.Sprintf("Timer: %s", time.Second*time.Duration(v.Ephemeral.DisappearingTimer)) + } else { + updateText = "Group's auto deletion timer has been disabled" + } + err = utils.TgSendTextById(tgBot, cfg.Telegram.TargetChatID, tgThreadId, updateText) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } + } + + if v.Delete != nil { + updateText := "The group has been deleted" + if v.Delete.DeleteReason != "" { + updateText += fmt.Sprintf( + "\nReason: %s", + html.EscapeString(v.Delete.DeleteReason), + ) + } + err = utils.TgSendTextById( + tgBot, cfg.Telegram.TargetChatID, tgThreadId, + "The group has been deleted", + ) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } + } + + if len(v.Join) > 0 { + var updateText string + if len(v.Join) == 1 { + newMemName := utils.WaGetContactName(v.Join[0]) + updateText = fmt.Sprintf("%s joined the group\n", html.EscapeString(newMemName)) + } else { + updateText = "The following people joined the group:\n" + for _, newMem := range v.Join { + newMemName := utils.WaGetContactName(newMem) + updateText += fmt.Sprintf("- %s\n", html.EscapeString(newMemName)) + } + } + if v.JoinReason != "" { + updateText += fmt.Sprintf("\nReason: %s", html.EscapeString(v.JoinReason)) + } + err = utils.TgSendTextById(tgBot, cfg.Telegram.TargetChatID, tgThreadId, updateText) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } + } + + if len(v.Leave) > 0 { + var updateText string + if len(v.Leave) == 1 { + oldMemName := utils.WaGetContactName(v.Leave[0]) + updateText = fmt.Sprintf("%s left the group\n", html.EscapeString(oldMemName)) + } else { + updateText = "The following people left the group:\n" + for _, oldMem := range v.Leave { + oldMemName := utils.WaGetContactName(oldMem) + updateText += fmt.Sprintf("- %s\n", oldMemName) + } + } + err = utils.TgSendTextById(tgBot, cfg.Telegram.TargetChatID, tgThreadId, updateText) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } + } + + if len(v.Demote) > 0 { + var updateText string + if len(v.Demote) == 1 { + demotedMemName := utils.WaGetContactName(v.Demote[0]) + updateText = fmt.Sprintf("%s was demoted in the group\n", html.EscapeString(demotedMemName)) + } else { + updateText = "The following people were demoted:\n" + for _, demotedMem := range v.Demote { + demotedMemName := utils.WaGetContactName(demotedMem) + updateText += fmt.Sprintf("- %s\n", demotedMemName) + } + } + err = utils.TgSendTextById(tgBot, cfg.Telegram.TargetChatID, tgThreadId, updateText) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } + } + + if len(v.Promote) > 0 { + var updateText string + if len(v.Promote) == 1 { + promotedMemName := utils.WaGetContactName(v.Promote[0]) + updateText = fmt.Sprintf("%s was promoted in the group\n", html.EscapeString(promotedMemName)) + } else { + updateText = "The following people were promoted:\n" + for _, promotedMem := range v.Promote { + promotedMemName := utils.WaGetContactName(promotedMem) + updateText += fmt.Sprintf("- %s\n", promotedMemName) + } + } + err = utils.TgSendTextById(tgBot, cfg.Telegram.TargetChatID, tgThreadId, updateText) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } + } + + if v.Topic != nil { + changer := utils.WaGetContactName(v.Topic.TopicSetBy) + updateText := fmt.Sprintf( + "The group description was changed by %s:\n\n%s", + html.EscapeString(changer), + html.EscapeString(v.Topic.Topic), + ) + err = utils.TgSendTextById(tgBot, cfg.Telegram.TargetChatID, tgThreadId, updateText) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } + } + + if v.Name != nil { + _, err = tgBot.EditForumTopic( + cfg.Telegram.TargetChatID, tgThreadId, + &gotgbot.EditForumTopicOpts{ + Name: v.Name.Name, + }, + ) + if err != nil { + err = utils.TgSendTextById( + tgBot, cfg.Telegram.OwnerID, 0, + fmt.Sprintf( + "Warning: Chat name could not be changed for %s to %s:\n\n%s", + v.JID.String(), html.EscapeString(v.Name.Name), html.EscapeString(err.Error()), + ), + ) + if err != nil { + logger.Error("failed to send message to owner", zap.Error(err)) + } + return + } + changer := utils.WaGetContactName(v.Name.NameSetBy) + updateText := fmt.Sprintf( + "The group name was changed by %s:\n\n%s", + html.EscapeString(changer), + html.EscapeString(v.Name.Name), + ) + err = utils.TgSendTextById(tgBot, cfg.Telegram.TargetChatID, tgThreadId, updateText) + if err != nil { + logger.Error("failed to send message", zap.Error(err)) + } + } +}