721 lines
25 KiB
Go
721 lines
25 KiB
Go
package whatsapp
|
|
|
|
import (
|
|
// Standard library.
|
|
"context"
|
|
"fmt"
|
|
"mime"
|
|
"os"
|
|
"strings"
|
|
|
|
// Third-party libraries.
|
|
"go.mau.fi/whatsmeow"
|
|
"go.mau.fi/whatsmeow/binary/proto"
|
|
"go.mau.fi/whatsmeow/types"
|
|
"go.mau.fi/whatsmeow/types/events"
|
|
)
|
|
|
|
// EventKind represents all event types recognized by the Python session adapter, as emitted by the
|
|
// Go session adapter.
|
|
type EventKind int
|
|
|
|
// The event types handled by the overarching session adapter handler.
|
|
const (
|
|
EventUnknown EventKind = iota
|
|
EventQRCode
|
|
EventPair
|
|
EventConnected
|
|
EventLoggedOut
|
|
EventContact
|
|
EventPresence
|
|
EventMessage
|
|
EventChatState
|
|
EventReceipt
|
|
EventGroup
|
|
EventCall
|
|
)
|
|
|
|
// EventPayload represents the collected payloads for all event types handled by the overarching
|
|
// session adapter handler. Only specific fields will be populated in events emitted by internal
|
|
// handlers, see documentation for specific types for more information.
|
|
type EventPayload struct {
|
|
QRCode string
|
|
PairDeviceID string
|
|
ConnectedJID string
|
|
Contact Contact
|
|
Presence Presence
|
|
Message Message
|
|
ChatState ChatState
|
|
Receipt Receipt
|
|
Group Group
|
|
Call Call
|
|
}
|
|
|
|
// A Avatar represents a small image representing a Contact or Group.
|
|
type Avatar struct {
|
|
ID string // The unique ID for this avatar, used for persistent caching.
|
|
URL string // The HTTP URL over which this avatar might be retrieved. Can change for the same ID.
|
|
}
|
|
|
|
// A Contact represents any entity that be communicated with directly in WhatsApp. This typically
|
|
// represents people, but may represent a business or bot as well, but not a group-chat.
|
|
type Contact struct {
|
|
JID string // The WhatsApp JID for this contact.
|
|
Name string // The user-set, human-readable name for this contact.
|
|
}
|
|
|
|
// NewContactEvent returns event data meant for [Session.propagateEvent] for the contact information
|
|
// given. Unknown or invalid contact information will return an [EventUnknown] event with nil data.
|
|
func newContactEvent(c *whatsmeow.Client, jid types.JID, info types.ContactInfo) (EventKind, *EventPayload) {
|
|
var contact = Contact{
|
|
JID: jid.ToNonAD().String(),
|
|
}
|
|
|
|
for _, n := range []string{info.FullName, info.FirstName, info.BusinessName, info.PushName} {
|
|
if n != "" {
|
|
contact.Name = n
|
|
break
|
|
}
|
|
}
|
|
|
|
// Don't attempt to synchronize contacts with no user-readable name.
|
|
if contact.Name == "" {
|
|
return EventUnknown, nil
|
|
}
|
|
|
|
return EventContact, &EventPayload{Contact: contact}
|
|
}
|
|
|
|
// PresenceKind represents the different kinds of activity states possible in WhatsApp.
|
|
type PresenceKind int
|
|
|
|
// The presences handled by the overarching session event handler.
|
|
const (
|
|
PresenceAvailable PresenceKind = 1 + iota
|
|
PresenceUnavailable
|
|
)
|
|
|
|
// Precence represents a contact's general state of activity, and is periodically updated as
|
|
// contacts start or stop paying attention to their client of choice.
|
|
type Presence struct {
|
|
JID string
|
|
Kind PresenceKind
|
|
LastSeen int64
|
|
}
|
|
|
|
// NewPresenceEvent returns event data meant for [Session.propagateEvent] for the primitive presence
|
|
// event given.
|
|
func newPresenceEvent(evt *events.Presence) (EventKind, *EventPayload) {
|
|
var presence = Presence{
|
|
JID: evt.From.ToNonAD().String(),
|
|
Kind: PresenceAvailable,
|
|
LastSeen: evt.LastSeen.Unix(),
|
|
}
|
|
|
|
if evt.Unavailable {
|
|
presence.Kind = PresenceUnavailable
|
|
}
|
|
|
|
return EventPresence, &EventPayload{Presence: presence}
|
|
}
|
|
|
|
// MessageKind represents all concrete message types (plain-text messages, edit messages, reactions)
|
|
// recognized by the Python session adapter.
|
|
type MessageKind int
|
|
|
|
// The message types handled by the overarching session event handler.
|
|
const (
|
|
MessagePlain MessageKind = 1 + iota
|
|
MessageEdit
|
|
MessageRevoke
|
|
MessageReaction
|
|
MessageAttachment
|
|
)
|
|
|
|
// A Message represents one of many kinds of bidirectional communication payloads, for example, a
|
|
// text message, a file (image, video) attachment, an emoji reaction, etc. Messages of different
|
|
// kinds are denoted as such, and re-use fields where the semantics overlap.
|
|
type Message struct {
|
|
Kind MessageKind // The concrete message kind being sent or received.
|
|
ID string // The unique message ID, used for referring to a specific Message instance.
|
|
JID string // The JID this message concerns, semantics can change based on IsCarbon.
|
|
GroupJID string // The JID of the group-chat this message was sent in, if any.
|
|
OriginJID string // For reactions and replies in groups, the JID of the original user.
|
|
Body string // The plain-text message body. For attachment messages, this can be a caption.
|
|
Timestamp int64 // The Unix timestamp denoting when this message was created.
|
|
IsCarbon bool // Whether or not this message concerns the gateway user themselves.
|
|
ReplyID string // The unique message ID this message is in reply to, if any.
|
|
ReplyBody string // The full body of the message this message is in reply to, if any.
|
|
Attachments []Attachment // The list of file (image, video, etc.) attachments contained in this message.
|
|
Preview Preview // A short description for the URL provided in the message body, if any.
|
|
MentionJIDs []string // A list of JIDs mentioned in this message, if any.
|
|
}
|
|
|
|
// A Attachment represents additional binary data (e.g. images, videos, documents) provided alongside
|
|
// a message, for display or storage on the recepient client.
|
|
type Attachment struct {
|
|
MIME string // The MIME type for attachment.
|
|
Filename string // The recommended file name for this attachment. May be an auto-generated name.
|
|
Caption string // The user-provided caption, provided alongside this attachment.
|
|
Path string // Local path to the file is stored on disk.
|
|
|
|
// Internal fields.
|
|
meta attachmentMetadata // Metadata specific to audio/video files, used in processing.
|
|
}
|
|
|
|
// A Preview represents a short description for a URL provided in a message body, as usually derived
|
|
// from the content of the page pointed at.
|
|
type Preview struct {
|
|
URL string // The original (or canonical) URL this preview was generated for.
|
|
Title string // The short title for the URL preview.
|
|
Description string // The (optional) long-form description for the URL preview.
|
|
ImagePath string // The local path for the image associated with the URL.
|
|
}
|
|
|
|
// NewMessageEvent returns event data meant for [Session.propagateEvent] for the primive message
|
|
// event given. Unknown or invalid messages will return an [EventUnknown] event with nil data.
|
|
func newMessageEvent(client *whatsmeow.Client, evt *events.Message) (EventKind, *EventPayload) {
|
|
// Set basic data for message, to be potentially amended depending on the concrete version of
|
|
// the underlying message.
|
|
var message = Message{
|
|
Kind: MessagePlain,
|
|
ID: evt.Info.ID,
|
|
JID: evt.Info.Sender.ToNonAD().String(),
|
|
Body: evt.Message.GetConversation(),
|
|
Timestamp: evt.Info.Timestamp.Unix(),
|
|
IsCarbon: evt.Info.IsFromMe,
|
|
}
|
|
|
|
if evt.Info.IsGroup {
|
|
message.GroupJID = evt.Info.Chat.ToNonAD().String()
|
|
} else if message.IsCarbon {
|
|
message.JID = evt.Info.Chat.ToNonAD().String()
|
|
}
|
|
|
|
// Handle handle protocol messages (such as message deletion or editing).
|
|
if p := evt.Message.GetProtocolMessage(); p != nil {
|
|
switch p.GetType() {
|
|
case proto.ProtocolMessage_MESSAGE_EDIT:
|
|
if m := p.GetEditedMessage(); m != nil {
|
|
message.Kind = MessageEdit
|
|
message.ID = p.Key.GetId()
|
|
message.Body = m.GetConversation()
|
|
} else {
|
|
return EventUnknown, nil
|
|
}
|
|
case proto.ProtocolMessage_REVOKE:
|
|
message.Kind = MessageRevoke
|
|
message.ID = p.Key.GetId()
|
|
message.OriginJID = p.Key.GetParticipant()
|
|
return EventMessage, &EventPayload{Message: message}
|
|
}
|
|
}
|
|
|
|
// Handle emoji reaction to existing message.
|
|
if r := evt.Message.GetReactionMessage(); r != nil {
|
|
message.Kind = MessageReaction
|
|
message.ID = r.Key.GetId()
|
|
message.Body = r.GetText()
|
|
return EventMessage, &EventPayload{Message: message}
|
|
}
|
|
|
|
// Handle message attachments, if any.
|
|
if attach, err := getMessageAttachments(client, evt.Message); err != nil {
|
|
client.Log.Errorf("Failed getting message attachments: %s", err)
|
|
return EventUnknown, nil
|
|
} else if len(attach) > 0 {
|
|
message.Attachments = append(message.Attachments, attach...)
|
|
message.Kind = MessageAttachment
|
|
}
|
|
|
|
// Get contact vCard from message, if any, converting it into an inline attachment.
|
|
if c := evt.Message.GetContactMessage(); c != nil {
|
|
tmp, err := createTempFile([]byte(c.GetVcard()))
|
|
if err != nil {
|
|
client.Log.Errorf("Failed getting contact message: %s", err)
|
|
return EventUnknown, nil
|
|
}
|
|
message.Attachments = append(message.Attachments, Attachment{
|
|
MIME: "text/vcard",
|
|
Filename: c.GetDisplayName() + ".vcf",
|
|
Path: tmp,
|
|
})
|
|
message.Kind = MessageAttachment
|
|
message = getMessageWithContext(message, c.GetContextInfo())
|
|
}
|
|
|
|
// Get extended information from message, if available. Extended messages typically represent
|
|
// messages with additional context, such as replies, forwards, etc.
|
|
if e := evt.Message.GetExtendedTextMessage(); e != nil {
|
|
if message.Body == "" {
|
|
message.Body = e.GetText()
|
|
}
|
|
|
|
message = getMessageWithContext(message, e.GetContextInfo())
|
|
}
|
|
|
|
// Ignore obviously invalid messages.
|
|
if message.Kind == MessagePlain && message.Body == "" {
|
|
return EventUnknown, nil
|
|
}
|
|
|
|
return EventMessage, &EventPayload{Message: message}
|
|
}
|
|
|
|
// GetMessageWithContext processes the given [Message] and applies any context metadata might be
|
|
// useful; examples of context include messages being quoted. If no context is found, the original
|
|
// message is returned unchanged.
|
|
func getMessageWithContext(message Message, info *proto.ContextInfo) Message {
|
|
if info == nil {
|
|
return message
|
|
}
|
|
|
|
message.ReplyID = info.GetStanzaId()
|
|
message.OriginJID = info.GetParticipant()
|
|
|
|
if q := info.GetQuotedMessage(); q != nil {
|
|
if qe := q.GetExtendedTextMessage(); qe != nil {
|
|
message.ReplyBody = qe.GetText()
|
|
} else {
|
|
message.ReplyBody = q.GetConversation()
|
|
}
|
|
}
|
|
|
|
return message
|
|
}
|
|
|
|
// GetMessageAttachments fetches and decrypts attachments (images, audio, video, or documents) sent
|
|
// via WhatsApp. Any failures in retrieving any attachment will return an error immediately.
|
|
func getMessageAttachments(client *whatsmeow.Client, message *proto.Message) ([]Attachment, error) {
|
|
var result []Attachment
|
|
var kinds = []whatsmeow.DownloadableMessage{
|
|
message.GetImageMessage(),
|
|
message.GetAudioMessage(),
|
|
message.GetVideoMessage(),
|
|
message.GetDocumentMessage(),
|
|
message.GetStickerMessage(),
|
|
}
|
|
|
|
for _, msg := range kinds {
|
|
// Handle data for specific attachment type.
|
|
var a Attachment
|
|
switch msg := msg.(type) {
|
|
case *proto.ImageMessage:
|
|
a.MIME, a.Caption = msg.GetMimetype(), msg.GetCaption()
|
|
case *proto.AudioMessage:
|
|
a.MIME = msg.GetMimetype()
|
|
case *proto.VideoMessage:
|
|
a.MIME, a.Caption = msg.GetMimetype(), msg.GetCaption()
|
|
case *proto.DocumentMessage:
|
|
a.MIME, a.Caption, a.Filename = msg.GetMimetype(), msg.GetCaption(), msg.GetFileName()
|
|
case *proto.StickerMessage:
|
|
a.MIME = msg.GetMimetype()
|
|
}
|
|
|
|
// Ignore attachments with empty or unknown MIME types.
|
|
if a.MIME == "" {
|
|
continue
|
|
}
|
|
|
|
// Set filename from SHA256 checksum and MIME type, if none is already set.
|
|
if a.Filename == "" {
|
|
a.Filename = fmt.Sprintf("%x%s", msg.GetFileSha256(), extensionByType(a.MIME))
|
|
}
|
|
|
|
// Attempt to download and decrypt raw attachment data, if any.
|
|
data, err := client.Download(msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tmp, err := createTempFile(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed writing to temporary file: %w", err)
|
|
}
|
|
|
|
a.Path = tmp
|
|
result = append(result, a)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// KnownMediaTypes represents MIME type to WhatsApp media types known to be handled by WhatsApp in a
|
|
// special way (that is, not as generic file uploads).
|
|
var knownMediaTypes = map[string]whatsmeow.MediaType{
|
|
"image/jpeg": whatsmeow.MediaImage,
|
|
"audio/mpeg": whatsmeow.MediaAudio,
|
|
"audio/mp4": whatsmeow.MediaAudio,
|
|
"audio/aac": whatsmeow.MediaAudio,
|
|
"audio/ogg": whatsmeow.MediaAudio,
|
|
"video/mp4": whatsmeow.MediaVideo,
|
|
}
|
|
|
|
// UploadAttachment attempts to push the given attachment data to WhatsApp according to the MIME
|
|
// type specified within. Attachments are handled as generic file uploads unless they're of a
|
|
// specific format; in addition, certain MIME types may be automatically converted to a
|
|
// well-supported type via FFmpeg (if available).
|
|
func uploadAttachment(client *whatsmeow.Client, attach *Attachment) (*proto.Message, error) {
|
|
var originalMIME = attach.MIME
|
|
if err := convertAttachment(attach); err != nil {
|
|
client.Log.Warnf("failed to auto-convert attachment: %s", err)
|
|
}
|
|
|
|
mediaType := knownMediaTypes[strings.Split(attach.MIME, ";")[0]]
|
|
if mediaType == "" {
|
|
mediaType = whatsmeow.MediaDocument
|
|
}
|
|
|
|
data, err := os.ReadFile(attach.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if len(data) == 0 {
|
|
return nil, fmt.Errorf("attachment file contains no data")
|
|
}
|
|
|
|
upload, err := client.Upload(context.Background(), data, mediaType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var message *proto.Message
|
|
switch mediaType {
|
|
case whatsmeow.MediaImage:
|
|
message = &proto.Message{
|
|
ImageMessage: &proto.ImageMessage{
|
|
Url: &upload.URL,
|
|
DirectPath: &upload.DirectPath,
|
|
MediaKey: upload.MediaKey,
|
|
Mimetype: &attach.MIME,
|
|
FileEncSha256: upload.FileEncSHA256,
|
|
FileSha256: upload.FileSHA256,
|
|
FileLength: ptrTo(uint64(len(data))),
|
|
},
|
|
}
|
|
case whatsmeow.MediaAudio:
|
|
if attach.meta == (attachmentMetadata{}) {
|
|
if err = populateAttachmentMetadata(attach); err != nil {
|
|
client.Log.Warnf("failed fetching attachment metadata: %s", err)
|
|
}
|
|
}
|
|
message = &proto.Message{
|
|
AudioMessage: &proto.AudioMessage{
|
|
Url: &upload.URL,
|
|
DirectPath: &upload.DirectPath,
|
|
MediaKey: upload.MediaKey,
|
|
Mimetype: &attach.MIME,
|
|
FileEncSha256: upload.FileEncSHA256,
|
|
FileSha256: upload.FileSHA256,
|
|
FileLength: ptrTo(uint64(len(data))),
|
|
Seconds: ptrTo(uint32(attach.meta.duration.Seconds())),
|
|
},
|
|
}
|
|
if attach.MIME == voiceMessageMIME {
|
|
message.AudioMessage.Ptt = ptrTo(true)
|
|
if wave, err := getAttachmentWaveform(attach); err != nil {
|
|
client.Log.Warnf("failed generating attachment waveform: %s", err)
|
|
} else {
|
|
message.AudioMessage.Waveform = wave
|
|
}
|
|
}
|
|
case whatsmeow.MediaVideo:
|
|
if attach.meta == (attachmentMetadata{}) {
|
|
if err = populateAttachmentMetadata(attach); err != nil {
|
|
client.Log.Warnf("failed fetching attachment metadata: %s", err)
|
|
}
|
|
}
|
|
message = &proto.Message{
|
|
VideoMessage: &proto.VideoMessage{
|
|
Url: &upload.URL,
|
|
DirectPath: &upload.DirectPath,
|
|
MediaKey: upload.MediaKey,
|
|
Mimetype: &attach.MIME,
|
|
FileEncSha256: upload.FileEncSHA256,
|
|
FileSha256: upload.FileSHA256,
|
|
FileLength: ptrTo(uint64(len(data))),
|
|
Seconds: ptrTo(uint32(attach.meta.duration.Seconds())),
|
|
Width: ptrTo(uint32(attach.meta.width)),
|
|
Height: ptrTo(uint32(attach.meta.height)),
|
|
}}
|
|
if thumb, err := getAttachmentThumbnail(attach); err != nil {
|
|
client.Log.Warnf("failed generating attachment thumbnail: %s", err)
|
|
} else {
|
|
message.VideoMessage.JpegThumbnail = thumb
|
|
}
|
|
if originalMIME == animatedImageMIME {
|
|
message.VideoMessage.GifPlayback = ptrTo(true)
|
|
}
|
|
case whatsmeow.MediaDocument:
|
|
message = &proto.Message{
|
|
DocumentMessage: &proto.DocumentMessage{
|
|
Url: &upload.URL,
|
|
DirectPath: &upload.DirectPath,
|
|
MediaKey: upload.MediaKey,
|
|
Mimetype: &attach.MIME,
|
|
FileEncSha256: upload.FileEncSHA256,
|
|
FileSha256: upload.FileSHA256,
|
|
FileLength: ptrTo(uint64(len(data))),
|
|
FileName: &attach.Filename,
|
|
}}
|
|
}
|
|
|
|
return message, nil
|
|
}
|
|
|
|
// KnownExtensions represents MIME type to file-extension mappings for basic, known media types.
|
|
var knownExtensions = map[string]string{
|
|
"image/jpeg": ".jpg",
|
|
"audio/ogg": ".oga",
|
|
"video/mp4": ".mp4",
|
|
}
|
|
|
|
// ExtensionByType returns the file extension for the given MIME type, or a generic extension if the
|
|
// MIME type is unknown.
|
|
func extensionByType(typ string) string {
|
|
// Handle common, known MIME types first.
|
|
if ext := knownExtensions[typ]; ext != "" {
|
|
return ext
|
|
}
|
|
if ext, _ := mime.ExtensionsByType(typ); len(ext) > 0 {
|
|
return ext[0]
|
|
}
|
|
return ".bin"
|
|
}
|
|
|
|
// ChatStateKind represents the different kinds of chat-states possible in WhatsApp.
|
|
type ChatStateKind int
|
|
|
|
// The chat states handled by the overarching session event handler.
|
|
const (
|
|
ChatStateComposing ChatStateKind = 1 + iota
|
|
ChatStatePaused
|
|
)
|
|
|
|
// A ChatState represents the activity of a contact within a certain discussion, for instance,
|
|
// whether the contact is currently composing a message. This is separate to the concept of a
|
|
// Presence, which is the contact's general state across all discussions.
|
|
type ChatState struct {
|
|
Kind ChatStateKind
|
|
JID string
|
|
GroupJID string
|
|
}
|
|
|
|
// NewChatStateEvent returns event data meant for [Session.propagateEvent] for the primitive
|
|
// chat-state event given.
|
|
func newChatStateEvent(evt *events.ChatPresence) (EventKind, *EventPayload) {
|
|
var state = ChatState{JID: evt.MessageSource.Sender.ToNonAD().String()}
|
|
if evt.MessageSource.IsGroup {
|
|
state.GroupJID = evt.MessageSource.Chat.ToNonAD().String()
|
|
}
|
|
switch evt.State {
|
|
case types.ChatPresenceComposing:
|
|
state.Kind = ChatStateComposing
|
|
case types.ChatPresencePaused:
|
|
state.Kind = ChatStatePaused
|
|
}
|
|
return EventChatState, &EventPayload{ChatState: state}
|
|
}
|
|
|
|
// ReceiptKind represents the different types of delivery receipts possible in WhatsApp.
|
|
type ReceiptKind int
|
|
|
|
// The delivery receipts handled by the overarching session event handler.
|
|
const (
|
|
ReceiptDelivered ReceiptKind = 1 + iota
|
|
ReceiptRead
|
|
)
|
|
|
|
// A Receipt represents a notice of delivery or presentation for [Message] instances sent or
|
|
// received. Receipts can be delivered for many messages at once, but are generally all delivered
|
|
// under one specific state at a time.
|
|
type Receipt struct {
|
|
Kind ReceiptKind // The distinct kind of receipt presented.
|
|
MessageIDs []string // The list of message IDs to mark for receipt.
|
|
JID string
|
|
GroupJID string
|
|
Timestamp int64
|
|
IsCarbon bool
|
|
}
|
|
|
|
// NewReceiptEvent returns event data meant for [Session.propagateEvent] for the primive receipt
|
|
// event given. Unknown or invalid receipts will return an [EventUnknown] event with nil data.
|
|
func newReceiptEvent(evt *events.Receipt) (EventKind, *EventPayload) {
|
|
var receipt = Receipt{
|
|
MessageIDs: append([]string{}, evt.MessageIDs...),
|
|
JID: evt.MessageSource.Sender.ToNonAD().String(),
|
|
Timestamp: evt.Timestamp.Unix(),
|
|
IsCarbon: evt.MessageSource.IsFromMe,
|
|
}
|
|
|
|
if len(receipt.MessageIDs) == 0 {
|
|
return EventUnknown, nil
|
|
}
|
|
|
|
if evt.MessageSource.IsGroup {
|
|
receipt.GroupJID = evt.MessageSource.Chat.ToNonAD().String()
|
|
} else if receipt.IsCarbon {
|
|
receipt.JID = evt.MessageSource.Chat.ToNonAD().String()
|
|
}
|
|
|
|
switch evt.Type {
|
|
case events.ReceiptTypeDelivered:
|
|
receipt.Kind = ReceiptDelivered
|
|
case events.ReceiptTypeRead:
|
|
receipt.Kind = ReceiptRead
|
|
}
|
|
|
|
return EventReceipt, &EventPayload{Receipt: receipt}
|
|
}
|
|
|
|
// GroupAffiliation represents the set of privilidges given to a specific participant in a group.
|
|
type GroupAffiliation int
|
|
|
|
const (
|
|
GroupAffiliationNone GroupAffiliation = iota // None, or normal member group affiliation.
|
|
GroupAffiliationAdmin // Can perform some management operations.
|
|
GroupAffiliationOwner // Can manage group fully, including destroying the group.
|
|
)
|
|
|
|
// A Group represents a named, many-to-many chat space which may be joined or left at will. All
|
|
// fields apart from the group JID, are considered to be optional, and may not be set in cases where
|
|
// group information is being updated against previous assumed state. Groups in WhatsApp are
|
|
// generally invited to out-of-band with respect to overarching adaptor; see the documentation for
|
|
// [Session.GetGroups] for more information.
|
|
type Group struct {
|
|
JID string // The WhatsApp JID for this group.
|
|
Name string // The user-defined, human-readable name for this group.
|
|
Subject GroupSubject // The longer-form, user-defined description for this group.
|
|
Nickname string // Our own nickname in this group-chat.
|
|
Participants []GroupParticipant // The list of participant contacts for this group, including ourselves.
|
|
}
|
|
|
|
// A GroupSubject represents the user-defined group description and attached metadata thereof, for a
|
|
// given [Group].
|
|
type GroupSubject struct {
|
|
Subject string // The user-defined group description.
|
|
SetAt int64 // The exact time this group description was set at, as a timestamp.
|
|
SetByJID string // The JID of the user that set the subject.
|
|
}
|
|
|
|
// GroupParticipantAction represents the distinct set of actions that can be taken when encountering
|
|
// a group participant, typically to add or remove.
|
|
type GroupParticipantAction int
|
|
|
|
const (
|
|
GroupParticipantActionAdd GroupParticipantAction = iota // Default action; add participant to list.
|
|
GroupParticipantActionUpdate // Update existing participant information.
|
|
GroupParticipantActionRemove // Remove participant from list, if existing.
|
|
)
|
|
|
|
// A GroupParticipant represents a contact who is currently joined in a given group. Participants in
|
|
// WhatsApp can always be derived back to their individual [Contact]; there are no anonymous groups
|
|
// in WhatsApp.
|
|
type GroupParticipant struct {
|
|
JID string // The WhatsApp JID for this participant.
|
|
Affiliation GroupAffiliation // The set of priviledges given to this specific participant.
|
|
Action GroupParticipantAction // The specific action to take for this participant; typically to add.
|
|
}
|
|
|
|
// NewReceiptEvent returns event data meant for [Session.propagateEvent] for the primive group
|
|
// event given. Group data returned by this function can be partial, and callers should take care
|
|
// to only handle non-empty values.
|
|
func newGroupEvent(evt *events.GroupInfo) (EventKind, *EventPayload) {
|
|
var group = Group{JID: evt.JID.ToNonAD().String()}
|
|
if evt.Name != nil {
|
|
group.Name = evt.Name.Name
|
|
}
|
|
if evt.Topic != nil {
|
|
group.Subject = GroupSubject{
|
|
Subject: evt.Topic.Topic,
|
|
SetAt: evt.Topic.TopicSetAt.Unix(),
|
|
SetByJID: evt.Topic.TopicSetBy.ToNonAD().String(),
|
|
}
|
|
}
|
|
for _, p := range evt.Join {
|
|
group.Participants = append(group.Participants, GroupParticipant{
|
|
JID: p.ToNonAD().String(),
|
|
Action: GroupParticipantActionAdd,
|
|
})
|
|
}
|
|
for _, p := range evt.Leave {
|
|
group.Participants = append(group.Participants, GroupParticipant{
|
|
JID: p.ToNonAD().String(),
|
|
Action: GroupParticipantActionRemove,
|
|
})
|
|
}
|
|
for _, p := range evt.Promote {
|
|
group.Participants = append(group.Participants, GroupParticipant{
|
|
JID: p.ToNonAD().String(),
|
|
Action: GroupParticipantActionUpdate,
|
|
Affiliation: GroupAffiliationAdmin,
|
|
})
|
|
}
|
|
for _, p := range evt.Demote {
|
|
group.Participants = append(group.Participants, GroupParticipant{
|
|
JID: p.ToNonAD().String(),
|
|
Action: GroupParticipantActionUpdate,
|
|
Affiliation: GroupAffiliationNone,
|
|
})
|
|
}
|
|
return EventGroup, &EventPayload{Group: group}
|
|
}
|
|
|
|
// NewGroup returns a concrete [Group] for the primitive data given. This function will generally
|
|
// populate fields with as much data as is available from the remote, and is therefore should not
|
|
// be called when partial data is to be returned.
|
|
func newGroup(client *whatsmeow.Client, info *types.GroupInfo) Group {
|
|
var participants []GroupParticipant
|
|
for _, p := range info.Participants {
|
|
if p.Error > 0 {
|
|
continue
|
|
}
|
|
var affiliation = GroupAffiliationNone
|
|
if p.IsSuperAdmin {
|
|
affiliation = GroupAffiliationOwner
|
|
} else if p.IsAdmin {
|
|
affiliation = GroupAffiliationAdmin
|
|
}
|
|
participants = append(participants, GroupParticipant{
|
|
JID: p.JID.ToNonAD().String(),
|
|
Affiliation: affiliation,
|
|
})
|
|
}
|
|
return Group{
|
|
JID: info.JID.ToNonAD().String(),
|
|
Name: info.GroupName.Name,
|
|
Subject: GroupSubject{
|
|
Subject: info.Topic,
|
|
SetAt: info.TopicSetAt.Unix(),
|
|
SetByJID: info.TopicSetBy.ToNonAD().String(),
|
|
},
|
|
Nickname: client.Store.PushName,
|
|
Participants: participants,
|
|
}
|
|
}
|
|
|
|
// CallState represents the state of the call to synchronize with.
|
|
type CallState int
|
|
|
|
// The call states handled by the overarching session event handler.
|
|
const (
|
|
CallMissed CallState = 1 + iota
|
|
)
|
|
|
|
// A Call represents an incoming or outgoing voice/video call made over WhatsApp. Full support for
|
|
// calls is currently not implemented, and this structure contains the bare minimum data required
|
|
// for notifying on missed calls.
|
|
type Call struct {
|
|
State CallState
|
|
JID string
|
|
Timestamp int64
|
|
}
|
|
|
|
// NewCallEvent returns event data meant for [Session.propagateEvent] for the call metadata given.
|
|
func newCallEvent(state CallState, meta types.BasicCallMeta) (EventKind, *EventPayload) {
|
|
return EventCall, &EventPayload{Call: Call{
|
|
State: state,
|
|
JID: meta.From.ToNonAD().String(),
|
|
Timestamp: meta.Timestamp.Unix(),
|
|
}}
|
|
}
|