feat: initial whatsmeow bot with command handler and context helpers
Core framework (Handler, Command, Default muxer) in core/ package. Command self-registration via init() with blank imports in cmds/. Context helpers (send, reply, download, upload) in context/ package. Permission system with GROUP_ONLY / PRIVATE_ONLY checks. WhatsApp client configured as Safari with history sync disabled.
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
*.db
|
||||||
|
.env
|
||||||
|
doc.json
|
||||||
|
neo
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package misc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
hc "neo/context"
|
||||||
|
"neo/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { core.Default.Register(Ping) }
|
||||||
|
|
||||||
|
var Ping = &core.Command{
|
||||||
|
Name: "ping",
|
||||||
|
Aliases: []string{"test", "tes", "p"},
|
||||||
|
Category: "general",
|
||||||
|
Description: "Check bot response time",
|
||||||
|
Permissions: []core.Permission{core.PermissionGroupOnly},
|
||||||
|
Run: func(ctx *hc.Ctx) {
|
||||||
|
t := ctx.Event().Info.Timestamp
|
||||||
|
speed := time.Since(t).Milliseconds()
|
||||||
|
ctx.Reply(fmt.Sprintf("Pong! 🏓\n\nSpeed: %d ms", speed))
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package cmds
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "neo/cmds/misc"
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.mau.fi/whatsmeow"
|
||||||
|
waE2E "go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
waTypes "go.mau.fi/whatsmeow/types"
|
||||||
|
"go.mau.fi/whatsmeow/types/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ctx struct {
|
||||||
|
client *whatsmeow.Client
|
||||||
|
event *events.Message
|
||||||
|
message *waE2E.Message
|
||||||
|
arguments []string
|
||||||
|
prefix string
|
||||||
|
cmd string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCtx(client *whatsmeow.Client, evt *events.Message, prefix, cmd string, args []string) *Ctx {
|
||||||
|
var msg *waE2E.Message
|
||||||
|
if evt.IsViewOnce {
|
||||||
|
msg = evt.Message.GetViewOnceMessage().GetMessage()
|
||||||
|
} else if evt.IsEphemeral {
|
||||||
|
msg = evt.Message.GetEphemeralMessage().GetMessage()
|
||||||
|
} else {
|
||||||
|
msg = evt.Message
|
||||||
|
}
|
||||||
|
return &Ctx{
|
||||||
|
client: client,
|
||||||
|
event: evt,
|
||||||
|
message: msg,
|
||||||
|
arguments: args,
|
||||||
|
prefix: prefix,
|
||||||
|
cmd: cmd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Client() *whatsmeow.Client { return c.client }
|
||||||
|
func (c *Ctx) Event() *events.Message { return c.event }
|
||||||
|
func (c *Ctx) Message() *waE2E.Message { return c.message }
|
||||||
|
func (c *Ctx) SenderJID() waTypes.JID { return c.event.Info.Sender.ToNonAD() }
|
||||||
|
func (c *Ctx) ChatJID() waTypes.JID { return c.event.Info.Chat }
|
||||||
|
func (c *Ctx) IsGroup() bool { return c.event.Info.IsGroup }
|
||||||
|
func (c *Ctx) IsFromMe() bool { return c.event.Info.IsFromMe }
|
||||||
|
func (c *Ctx) Number() string { return c.event.Info.Sender.ToNonAD().String() }
|
||||||
|
func (c *Ctx) Prefix() string { return c.prefix }
|
||||||
|
func (c *Ctx) Cmd() string { return c.cmd }
|
||||||
|
func (c *Ctx) Arguments() []string { return c.arguments }
|
||||||
|
func (c *Ctx) FullArgs() string { return strings.Join(c.arguments, " ") }
|
||||||
|
func (c *Ctx) Arg(index int) string {
|
||||||
|
if index < len(c.arguments) {
|
||||||
|
return c.arguments[index]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.mau.fi/whatsmeow"
|
||||||
|
waE2E "go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Ctx) Download() ([]byte, error) {
|
||||||
|
msg := getDownloadable(c.message)
|
||||||
|
if msg == nil {
|
||||||
|
return nil, fmt.Errorf("message has no downloadable media")
|
||||||
|
}
|
||||||
|
return c.client.Download(stdctx.Background(), msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) DownloadQuoted() ([]byte, error) {
|
||||||
|
quoted := GetQuotedMessage(c.event.Message)
|
||||||
|
if quoted == nil {
|
||||||
|
return nil, fmt.Errorf("no quoted message")
|
||||||
|
}
|
||||||
|
msg := getDownloadable(quoted)
|
||||||
|
if msg == nil {
|
||||||
|
return nil, fmt.Errorf("quoted message has no downloadable media")
|
||||||
|
}
|
||||||
|
return c.client.Download(stdctx.Background(), msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) DownloadMessage(msg whatsmeow.DownloadableMessage) ([]byte, error) {
|
||||||
|
return c.client.Download(stdctx.Background(), msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDownloadable(m *waE2E.Message) whatsmeow.DownloadableMessage {
|
||||||
|
switch {
|
||||||
|
case m.GetImageMessage() != nil:
|
||||||
|
return m.GetImageMessage()
|
||||||
|
case m.GetVideoMessage() != nil:
|
||||||
|
return m.GetVideoMessage()
|
||||||
|
case m.GetAudioMessage() != nil:
|
||||||
|
return m.GetAudioMessage()
|
||||||
|
case m.GetDocumentMessage() != nil:
|
||||||
|
return m.GetDocumentMessage()
|
||||||
|
case m.GetStickerMessage() != nil:
|
||||||
|
return m.GetStickerMessage()
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
waE2E "go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
waTypes "go.mau.fi/whatsmeow/types"
|
||||||
|
"go.mau.fi/whatsmeow/types/events"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseMessageText extracts text content from a message event.
|
||||||
|
func ParseMessageText(evt *events.Message) string {
|
||||||
|
var msg *waE2E.Message
|
||||||
|
if evt.IsViewOnce {
|
||||||
|
msg = evt.Message.GetViewOnceMessage().GetMessage()
|
||||||
|
} else if evt.IsEphemeral {
|
||||||
|
msg = evt.Message.GetEphemeralMessage().GetMessage()
|
||||||
|
} else {
|
||||||
|
msg = evt.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case msg.GetConversation() != "":
|
||||||
|
return msg.GetConversation()
|
||||||
|
case msg.GetExtendedTextMessage().GetText() != "":
|
||||||
|
return msg.GetExtendedTextMessage().GetText()
|
||||||
|
case msg.GetImageMessage().GetCaption() != "":
|
||||||
|
return msg.GetImageMessage().GetCaption()
|
||||||
|
case msg.GetVideoMessage().GetCaption() != "":
|
||||||
|
return msg.GetVideoMessage().GetCaption()
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuotedMessage extracts the quoted message from context info.
|
||||||
|
func GetQuotedMessage(m *waE2E.Message) *waE2E.Message {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case m.GetExtendedTextMessage().GetContextInfo() != nil:
|
||||||
|
return m.GetExtendedTextMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case m.GetImageMessage().GetContextInfo() != nil:
|
||||||
|
return m.GetImageMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case m.GetVideoMessage().GetContextInfo() != nil:
|
||||||
|
return m.GetVideoMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case m.GetDocumentMessage().GetContextInfo() != nil:
|
||||||
|
return m.GetDocumentMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case m.GetAudioMessage().GetContextInfo() != nil:
|
||||||
|
return m.GetAudioMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case m.GetStickerMessage().GetContextInfo() != nil:
|
||||||
|
return m.GetStickerMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithReply creates ContextInfo that quotes the original message.
|
||||||
|
func WithReply(evt *events.Message) *waE2E.ContextInfo {
|
||||||
|
return &waE2E.ContextInfo{
|
||||||
|
StanzaID: &evt.Info.ID,
|
||||||
|
Participant: proto.String(evt.Info.Sender.String()),
|
||||||
|
QuotedMessage: evt.Message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJID parses a JID string, adding @s.whatsapp.net if no server is specified.
|
||||||
|
func ParseJID(arg string) (waTypes.JID, bool) {
|
||||||
|
if arg == "" {
|
||||||
|
return waTypes.JID{}, false
|
||||||
|
}
|
||||||
|
if arg[0] == '+' {
|
||||||
|
arg = arg[1:]
|
||||||
|
}
|
||||||
|
if !strings.ContainsRune(arg, '@') {
|
||||||
|
return waTypes.NewJID(arg, waTypes.DefaultUserServer), true
|
||||||
|
}
|
||||||
|
recipient, err := waTypes.ParseJID(arg)
|
||||||
|
if err != nil || recipient.User == "" {
|
||||||
|
return recipient, false
|
||||||
|
}
|
||||||
|
return recipient, true
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
|
||||||
|
waCommon "go.mau.fi/whatsmeow/proto/waCommon"
|
||||||
|
waE2E "go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
waTypes "go.mau.fi/whatsmeow/types"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Ctx) Reply(text string) {
|
||||||
|
msg := &waE2E.Message{
|
||||||
|
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
|
||||||
|
Text: proto.String(text),
|
||||||
|
ContextInfo: &waE2E.ContextInfo{
|
||||||
|
StanzaID: &c.event.Info.ID,
|
||||||
|
Participant: proto.String(c.event.Info.Sender.String()),
|
||||||
|
QuotedMessage: c.event.Message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.client.SendMessage(stdctx.Background(), c.event.Info.Chat, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Send(text string) {
|
||||||
|
msg := &waE2E.Message{
|
||||||
|
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
|
||||||
|
Text: proto.String(text),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.client.SendMessage(stdctx.Background(), c.event.Info.Chat, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SendMessage(msg *waE2E.Message) {
|
||||||
|
c.client.SendMessage(stdctx.Background(), c.event.Info.Chat, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) React(emoji string) {
|
||||||
|
msg := &waE2E.Message{
|
||||||
|
ReactionMessage: &waE2E.ReactionMessage{
|
||||||
|
Key: &waCommon.MessageKey{
|
||||||
|
RemoteJID: proto.String(c.event.Info.Chat.String()),
|
||||||
|
FromMe: proto.Bool(c.event.Info.IsFromMe),
|
||||||
|
ID: proto.String(c.event.Info.ID),
|
||||||
|
},
|
||||||
|
Text: proto.String(emoji),
|
||||||
|
SenderTimestampMS: proto.Int64(c.event.Info.Timestamp.UnixMilli()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.client.SendMessage(stdctx.Background(), c.event.Info.Chat, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Edit(text string) error {
|
||||||
|
msgKey := &waCommon.MessageKey{
|
||||||
|
RemoteJID: proto.String(c.ChatJID().String()),
|
||||||
|
FromMe: proto.Bool(true),
|
||||||
|
ID: proto.String(c.event.Info.ID),
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.message.GetExtendedTextMessage() != nil {
|
||||||
|
c.message.ExtendedTextMessage.Text = proto.String(text)
|
||||||
|
} else {
|
||||||
|
c.message = &waE2E.Message{
|
||||||
|
Conversation: proto.String(text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editMsg := &waE2E.Message{
|
||||||
|
ProtocolMessage: &waE2E.ProtocolMessage{
|
||||||
|
Key: msgKey,
|
||||||
|
Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
|
||||||
|
EditedMessage: c.message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.client.SendMessage(stdctx.Background(), c.event.Info.Chat, editMsg)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Revoke() {
|
||||||
|
_, _ = c.client.SendMessage(
|
||||||
|
stdctx.Background(),
|
||||||
|
c.event.Info.Chat,
|
||||||
|
c.client.BuildRevoke(c.event.Info.Chat, c.event.Info.Sender, c.event.Info.ID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) MarkRead() {
|
||||||
|
_ = c.client.MarkRead(
|
||||||
|
stdctx.Background(),
|
||||||
|
[]string{c.event.Info.ID},
|
||||||
|
c.event.Info.Timestamp,
|
||||||
|
c.event.Info.Chat,
|
||||||
|
c.event.Info.Sender,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SendPresence(composing bool) {
|
||||||
|
state := waTypes.ChatPresencePaused
|
||||||
|
if composing {
|
||||||
|
state = waTypes.ChatPresenceComposing
|
||||||
|
}
|
||||||
|
_ = c.client.SubscribePresence(stdctx.Background(), c.event.Info.Chat)
|
||||||
|
_ = c.client.SendChatPresence(stdctx.Background(), c.event.Info.Chat, state, waTypes.ChatPresenceMediaText)
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"go.mau.fi/whatsmeow"
|
||||||
|
waE2E "go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Ctx) UploadImage(bytes []byte, caption string) (*waE2E.ImageMessage, error) {
|
||||||
|
mime := mimetype.Detect(bytes)
|
||||||
|
resp, err := c.client.Upload(stdctx.Background(), bytes, whatsmeow.MediaImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &waE2E.ImageMessage{
|
||||||
|
Caption: proto.String(caption),
|
||||||
|
Mimetype: proto.String(mime.String()),
|
||||||
|
URL: &resp.URL,
|
||||||
|
DirectPath: &resp.DirectPath,
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: &resp.FileLength,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) UploadVideo(bytes []byte, caption string) (*waE2E.VideoMessage, error) {
|
||||||
|
mime := mimetype.Detect(bytes)
|
||||||
|
resp, err := c.client.Upload(stdctx.Background(), bytes, whatsmeow.MediaVideo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &waE2E.VideoMessage{
|
||||||
|
Caption: proto.String(caption),
|
||||||
|
Mimetype: proto.String(mime.String()),
|
||||||
|
URL: &resp.URL,
|
||||||
|
DirectPath: &resp.DirectPath,
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: &resp.FileLength,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) UploadAudio(bytes []byte) (*waE2E.AudioMessage, error) {
|
||||||
|
mime := mimetype.Detect(bytes)
|
||||||
|
resp, err := c.client.Upload(stdctx.Background(), bytes, whatsmeow.MediaAudio)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &waE2E.AudioMessage{
|
||||||
|
Mimetype: proto.String(mime.String()),
|
||||||
|
URL: &resp.URL,
|
||||||
|
DirectPath: &resp.DirectPath,
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: &resp.FileLength,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) UploadDocument(bytes []byte, title, filename string) (*waE2E.DocumentMessage, error) {
|
||||||
|
mime := mimetype.Detect(bytes)
|
||||||
|
resp, err := c.client.Upload(stdctx.Background(), bytes, whatsmeow.MediaDocument)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &waE2E.DocumentMessage{
|
||||||
|
Title: proto.String(title),
|
||||||
|
FileName: proto.String(filename),
|
||||||
|
Mimetype: proto.String(mime.String()),
|
||||||
|
URL: &resp.URL,
|
||||||
|
DirectPath: &resp.DirectPath,
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: &resp.FileLength,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) UploadSticker(bytes []byte) (*waE2E.StickerMessage, error) {
|
||||||
|
resp, err := c.client.Upload(stdctx.Background(), bytes, whatsmeow.MediaImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &waE2E.StickerMessage{
|
||||||
|
Mimetype: proto.String("image/webp"),
|
||||||
|
URL: &resp.URL,
|
||||||
|
DirectPath: &resp.DirectPath,
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: &resp.FileLength,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SendImage(bytes []byte, caption string) {
|
||||||
|
img, err := c.UploadImage(bytes, caption)
|
||||||
|
if err != nil {
|
||||||
|
c.Reply("Failed to upload image: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SendMessage(&waE2E.Message{ImageMessage: img})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SendVideo(bytes []byte, caption string) {
|
||||||
|
vid, err := c.UploadVideo(bytes, caption)
|
||||||
|
if err != nil {
|
||||||
|
c.Reply("Failed to upload video: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SendMessage(&waE2E.Message{VideoMessage: vid})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SendAudio(bytes []byte) {
|
||||||
|
aud, err := c.UploadAudio(bytes)
|
||||||
|
if err != nil {
|
||||||
|
c.Reply("Failed to upload audio: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SendMessage(&waE2E.Message{AudioMessage: aud})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SendDocument(bytes []byte, title, filename string) {
|
||||||
|
doc, err := c.UploadDocument(bytes, title, filename)
|
||||||
|
if err != nil {
|
||||||
|
c.Reply("Failed to upload document: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SendMessage(&waE2E.Message{DocumentMessage: doc})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SendSticker(bytes []byte) {
|
||||||
|
sticker, err := c.UploadSticker(bytes)
|
||||||
|
if err != nil {
|
||||||
|
c.Reply("Failed to upload sticker: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SendMessage(&waE2E.Message{StickerMessage: sticker})
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchExtension(ext string, list []string) bool {
|
||||||
|
for _, e := range list {
|
||||||
|
if strings.EqualFold(ext, e) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
hc "neo/context"
|
||||||
|
|
||||||
|
"go.mau.fi/whatsmeow"
|
||||||
|
"go.mau.fi/whatsmeow/types/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandFunc func(ctx *hc.Ctx)
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
Name string
|
||||||
|
Aliases []string
|
||||||
|
Description string
|
||||||
|
Category string
|
||||||
|
Permissions []Permission
|
||||||
|
Run CommandFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default is the global command muxer. Each command registers itself
|
||||||
|
// via init() — see neo/cmds for the registration pattern.
|
||||||
|
var Default = New()
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
commands map[string]*Command
|
||||||
|
prefixes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(prefixes ...string) *Handler {
|
||||||
|
if len(prefixes) == 0 {
|
||||||
|
prefixes = []string{"!", "/", "."}
|
||||||
|
}
|
||||||
|
return &Handler{
|
||||||
|
commands: make(map[string]*Command),
|
||||||
|
prefixes: prefixes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Register(cmds ...*Command) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if cmd.Name == "" {
|
||||||
|
panic("core: command name cannot be empty")
|
||||||
|
}
|
||||||
|
if cmd.Run == nil {
|
||||||
|
panic(fmt.Sprintf("core: command %q has no Run function", cmd.Name))
|
||||||
|
}
|
||||||
|
h.commands[cmd.Name] = cmd
|
||||||
|
for _, alias := range cmd.Aliases {
|
||||||
|
h.commands[alias] = cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetCommands() []*Command {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var result []*Command
|
||||||
|
for _, cmd := range h.commands {
|
||||||
|
if !seen[cmd.Name] {
|
||||||
|
seen[cmd.Name] = true
|
||||||
|
result = append(result, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) EventHandler(client *whatsmeow.Client) func(any) {
|
||||||
|
return func(rawEvt any) {
|
||||||
|
evt, ok := rawEvt.(*events.Message)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go h.processMessage(client, evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) processMessage(client *whatsmeow.Client, evt *events.Message) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
fmt.Printf("[CMD] panic from %s: %v\n", formatSenderInfo(evt), r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
text := hc.ParseMessageText(evt)
|
||||||
|
prefix, cmd, args, isCmd := parseCommand(text, h.prefixes)
|
||||||
|
if !isCmd {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.RLock()
|
||||||
|
command, exists := h.commands[cmd]
|
||||||
|
h.mu.RUnlock()
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg := CheckCommandPermissions(command, evt); msg != "" {
|
||||||
|
sendPermissionDenied(client, evt, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := hc.NewCtx(client, evt, prefix, cmd, args)
|
||||||
|
fmt.Printf("[CMD] %s > %s%s\n", formatSenderInfo(evt), prefix, cmd)
|
||||||
|
command.Run(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCommand(text string, prefixes []string) (prefix, cmd string, args []string, ok bool) {
|
||||||
|
if text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, p := range prefixes {
|
||||||
|
if strings.HasPrefix(text, p) {
|
||||||
|
rest := text[len(p):]
|
||||||
|
parts := strings.Fields(rest)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return p, strings.ToLower(parts[0]), parts[1:], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSenderInfo(evt *events.Message) string {
|
||||||
|
sender := evt.Info.Sender.ToNonAD().String()
|
||||||
|
if evt.Info.IsGroup {
|
||||||
|
return fmt.Sprintf("[%s] %s", evt.Info.Chat.String(), sender)
|
||||||
|
}
|
||||||
|
return sender
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.mau.fi/whatsmeow"
|
||||||
|
waE2E "go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
"go.mau.fi/whatsmeow/types/events"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Permission string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PermissionGroupOnly Permission = "GROUP_ONLY"
|
||||||
|
PermissionPrivateOnly Permission = "PRIVATE_ONLY"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckCommandPermissions returns an error message if the command's
|
||||||
|
// permissions deny the event context, or empty string if allowed.
|
||||||
|
func CheckCommandPermissions(cmd *Command, evt *events.Message) string {
|
||||||
|
for _, p := range cmd.Permissions {
|
||||||
|
switch p {
|
||||||
|
case PermissionGroupOnly:
|
||||||
|
if !evt.Info.IsGroup {
|
||||||
|
return "This command can only be used in groups"
|
||||||
|
}
|
||||||
|
case PermissionPrivateOnly:
|
||||||
|
if evt.Info.IsGroup {
|
||||||
|
return "This command can only be used in private chat"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendPermissionDenied sends a plain text reply indicating why the
|
||||||
|
// command was blocked, so the caller gets feedback instead of silence.
|
||||||
|
func sendPermissionDenied(client *whatsmeow.Client, evt *events.Message, text string) {
|
||||||
|
client.SendMessage(context.Background(), evt.Info.Chat, &waE2E.Message{
|
||||||
|
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
|
||||||
|
Text: proto.String(text),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
module neo
|
||||||
|
|
||||||
|
go 1.25.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.47
|
||||||
|
github.com/mdp/qrterminal/v3 v3.2.1
|
||||||
|
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
|
github.com/beeper/argo-go v1.1.2 // indirect
|
||||||
|
github.com/coder/websocket v1.8.15 // indirect
|
||||||
|
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||||
|
github.com/rs/zerolog v1.35.1 // indirect
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
||||||
|
go.mau.fi/libsignal v0.2.2 // indirect
|
||||||
|
go.mau.fi/util v0.9.10 // indirect
|
||||||
|
golang.org/x/crypto v0.53.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
|
||||||
|
golang.org/x/net v0.56.0 // indirect
|
||||||
|
golang.org/x/sync v0.21.0 // indirect
|
||||||
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
|
golang.org/x/term v0.44.0 // indirect
|
||||||
|
golang.org/x/text v0.38.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
rsc.io/qr v0.2.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
|
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||||
|
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||||
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
|
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
||||||
|
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
||||||
|
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||||
|
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||||
|
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||||
|
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||||
|
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||||
|
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
||||||
|
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||||
|
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||||
|
go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU=
|
||||||
|
go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0=
|
||||||
|
go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg=
|
||||||
|
go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A=
|
||||||
|
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz2gkqA4S1bh8zjaSRj7N+Y=
|
||||||
|
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
|
||||||
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
|
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
|
||||||
|
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
|
||||||
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
|
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
_ "neo/cmds"
|
||||||
|
"neo/core"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/mdp/qrterminal/v3"
|
||||||
|
"go.mau.fi/whatsmeow"
|
||||||
|
"go.mau.fi/whatsmeow/proto/waCompanionReg"
|
||||||
|
"go.mau.fi/whatsmeow/store"
|
||||||
|
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||||
|
waLog "go.mau.fi/whatsmeow/util/log"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
godotenv.Load()
|
||||||
|
|
||||||
|
store.SetOSInfo("macOS", [3]uint32{10, 15, 7})
|
||||||
|
store.DeviceProps.PlatformType = waCompanionReg.DeviceProps_SAFARI.Enum()
|
||||||
|
store.DeviceProps.HistorySyncConfig.InlineInitialPayloadInE2EeMsg = proto.Bool(false)
|
||||||
|
store.DeviceProps.HistorySyncConfig.RecentSyncDaysLimit = nil
|
||||||
|
store.DeviceProps.HistorySyncConfig.FullSyncDaysLimit = nil
|
||||||
|
store.DeviceProps.HistorySyncConfig.FullSyncSizeMbLimit = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dbLog := waLog.Stdout("Database", "DEBUG", true)
|
||||||
|
ctx := context.Background()
|
||||||
|
container, err := sqlstore.New(ctx, "sqlite3", fmt.Sprintf("file:%s?_foreign_keys=on", os.Getenv("SESSION_PATH")), dbLog)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceStore, err := container.GetFirstDevice(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientLog := waLog.Stdout("Client", "DEBUG", true)
|
||||||
|
client := whatsmeow.NewClient(deviceStore, clientLog)
|
||||||
|
|
||||||
|
client.ManualHistorySyncDownload = false
|
||||||
|
client.DisableManualHistorySyncReceipt = true
|
||||||
|
|
||||||
|
client.AddEventHandler(core.Default.EventHandler(client))
|
||||||
|
|
||||||
|
if client.Store.ID == nil {
|
||||||
|
qrChan, _ := client.GetQRChannel(context.Background())
|
||||||
|
err = client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for evt := range qrChan {
|
||||||
|
if evt.Event == "code" {
|
||||||
|
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Login event:", evt.Event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-c
|
||||||
|
|
||||||
|
client.Disconnect()
|
||||||
|
}
|
||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
waProto "go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
waTypes "go.mau.fi/whatsmeow/types"
|
||||||
|
"go.mau.fi/whatsmeow/types/events"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseMessageText(m *events.Message) string {
|
||||||
|
var msg *waE2E.Message
|
||||||
|
if m.IsViewOnce {
|
||||||
|
msg = m.Message.GetViewOnceMessage().GetMessage()
|
||||||
|
} else if m.IsEphemeral {
|
||||||
|
msg = m.Message.GetEphemeralMessage().GetMessage()
|
||||||
|
} else {
|
||||||
|
msg = m.Message
|
||||||
|
}
|
||||||
|
if msg.GetConversation() != "" {
|
||||||
|
return msg.GetConversation()
|
||||||
|
} else if msg.GetVideoMessage().GetCaption() != "" {
|
||||||
|
return msg.GetVideoMessage().GetCaption()
|
||||||
|
} else if msg.GetImageMessage().GetCaption() != "" {
|
||||||
|
return msg.GetImageMessage().GetCaption()
|
||||||
|
} else if msg.GetExtendedTextMessage().GetText() != "" {
|
||||||
|
return msg.GetExtendedTextMessage().GetText()
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseQuotedMessage(m *waProto.Message) *waProto.Message {
|
||||||
|
if m.GetExtendedTextMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetExtendedTextMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetImageMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetImageMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetVideoMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetVideoMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetDocumentMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetDocumentMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetAudioMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetAudioMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetStickerMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetStickerMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetButtonsMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetButtonsMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetGroupInviteMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetGroupInviteMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetProductMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetProductMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetListMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetListMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetTemplateMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetTemplateMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else if m.GetContactMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetContactMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
} else {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseQuotedRemoteJid(m *waProto.Message) *string {
|
||||||
|
if m.GetExtendedTextMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetExtendedTextMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetImageMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetImageMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetVideoMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetVideoMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetDocumentMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetDocumentMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetAudioMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetAudioMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetStickerMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetStickerMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetButtonsMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetButtonsMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetGroupInviteMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetGroupInviteMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetProductMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetProductMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetListMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetListMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetTemplateMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetTemplateMessage().GetContextInfo().Participant
|
||||||
|
} else if m.GetContactMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetContactMessage().GetContextInfo().Participant
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMentionedJid(m *waProto.Message) []string {
|
||||||
|
if m.GetExtendedTextMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetExtendedTextMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetImageMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetImageMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetVideoMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetVideoMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetDocumentMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetDocumentMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetAudioMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetAudioMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetStickerMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetStickerMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetButtonsMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetButtonsMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetGroupInviteMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetGroupInviteMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetProductMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetProductMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetListMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetListMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetTemplateMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetTemplateMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else if m.GetContactMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetContactMessage().GetContextInfo().GetMentionedJID()
|
||||||
|
} else {
|
||||||
|
return make([]string, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseQuotedMessageId(m *waProto.Message) *string {
|
||||||
|
if m.GetExtendedTextMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetExtendedTextMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetImageMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetImageMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetVideoMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetVideoMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetDocumentMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetDocumentMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetAudioMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetAudioMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetStickerMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetStickerMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetButtonsMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetButtonsMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetGroupInviteMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetGroupInviteMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetProductMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetProductMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetListMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetListMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetTemplateMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetTemplateMessage().GetContextInfo().StanzaID
|
||||||
|
} else if m.GetContactMessage().GetContextInfo() != nil {
|
||||||
|
return m.GetContactMessage().GetContextInfo().StanzaID
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseQuotedBy(m *waProto.Message, str string) *waProto.Message {
|
||||||
|
switch str {
|
||||||
|
case "text":
|
||||||
|
return m.GetExtendedTextMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case "image":
|
||||||
|
return m.GetImageMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case "video":
|
||||||
|
return m.GetVideoMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case "sticker":
|
||||||
|
return m.GetStickerMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case "document":
|
||||||
|
return m.GetDocumentMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case "audio":
|
||||||
|
return m.GetAudioMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
case "location":
|
||||||
|
return m.GetAudioMessage().GetContextInfo().GetQuotedMessage()
|
||||||
|
default:
|
||||||
|
return ParseQuotedMessage(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithReply(m *events.Message) *waProto.ContextInfo {
|
||||||
|
return &waProto.ContextInfo{
|
||||||
|
StanzaID: &m.Info.ID,
|
||||||
|
Participant: proto.String(m.Info.Sender.String()),
|
||||||
|
QuotedMessage: m.Message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseJID(arg string) (waTypes.JID, bool) {
|
||||||
|
if arg[0] == '+' {
|
||||||
|
arg = arg[1:]
|
||||||
|
}
|
||||||
|
if !strings.ContainsRune(arg, '@') {
|
||||||
|
return waTypes.NewJID(arg, waTypes.DefaultUserServer), true
|
||||||
|
} else {
|
||||||
|
recipient, err := waTypes.ParseJID(arg)
|
||||||
|
if err != nil {
|
||||||
|
return recipient, false
|
||||||
|
} else if recipient.User == "" {
|
||||||
|
return recipient, false
|
||||||
|
}
|
||||||
|
return recipient, true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user