Skip to content
Open
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
646 changes: 642 additions & 4 deletions proto/gows.proto

Large diffs are not rendered by default.

49 changes: 24 additions & 25 deletions src/go.mod
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
module github.com/devlikeapro/gows

go 1.23.3
go 1.25.0

require (
github.com/golang/protobuf v1.5.4
github.com/mattn/go-sqlite3 v1.14.24
github.com/mdp/qrterminal/v3 v3.2.0
go.mau.fi/whatsmeow v0.0.0-20250104105216-918c879fcd19
github.com/google/uuid v1.6.0
github.com/h2non/bimg v1.1.9
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.45
github.com/u2takey/ffmpeg-go v0.5.0
go.mau.fi/whatsmeow v0.0.0-20260622185415-5f04eac6dbbb
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.36.1
google.golang.org/protobuf v1.36.11
)

require (
filippo.io/edwards25519 v1.1.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/aws/aws-sdk-go v1.55.5 // indirect
github.com/cettoana/go-waveform v0.0.0-20210107122202-35aaec2de427 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/h2non/bimg v1.1.9 // 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/jmespath/go-jmespath v0.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/u2takey/ffmpeg-go v0.5.0 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/rs/zerolog v1.35.1 // indirect
github.com/u2takey/go-utils v0.3.1 // indirect
github.com/xiph/ogg v1.3.5 // indirect
go.mau.fi/libsignal v0.1.1 // indirect
go.mau.fi/util v0.8.3 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // 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/text v0.38.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
mccoy.space/g/ogg v0.0.0-20221103053400-1ea94e6f3152 // indirect
rsc.io/qr v0.2.0 // indirect
)
174 changes: 52 additions & 122 deletions src/go.sum

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/gows/gows.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ type ConnectedEventData struct {

func BuildSession(ctx context.Context, log waLog.Logger, dialect string, address string) (*GoWS, error) {
// Prepare the database
container, err := sqlstore.New(dialect, address, log.Sub("Database"))
container, err := sqlstore.New(ctx, dialect, address, log.Sub("Database"))
if err != nil {
return nil, err
}
deviceStore, err := container.GetFirstDevice()
deviceStore, err := container.GetFirstDevice(ctx)
if err != nil {
_ = container.Close()
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions src/server/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (s *Server) GetProfilePicture(ctx context.Context, req *__.ProfilePictureRe
if err != nil {
return nil, err
}
info, err := cli.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{
info, err := cli.GetProfilePictureInfo(ctx, jid, &whatsmeow.GetProfilePictureParams{
Preview: false,
})
if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) {
Expand Down Expand Up @@ -49,7 +49,7 @@ func (s *Server) CheckPhones(ctx context.Context, req *__.CheckPhonesRequest) (*
phones[i] = p
}

res, err := cli.IsOnWhatsApp(phones)
res, err := cli.IsOnWhatsApp(ctx, phones)
if err != nil {
return nil, err
}
Expand Down
24 changes: 24 additions & 0 deletions src/server/business.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package server

import (
"context"

__ "github.com/devlikeapro/gows/proto"
"go.mau.fi/whatsmeow/types"
)

func (s *Server) GetBusinessProfile(ctx context.Context, req *__.JidRequest) (*__.Json, error) {
cli, err := s.Sm.Get(req.GetSession().GetId())
if err != nil {
return nil, err
}
jid, err := types.ParseJID(req.GetJid())
if err != nil {
return nil, err
}
profile, err := cli.GetBusinessProfile(ctx, jid)
if err != nil {
return nil, err
}
return s.toJson(profile), nil
}
275 changes: 275 additions & 0 deletions src/server/chat_actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package server

import (
"context"
"errors"
"time"

__ "github.com/devlikeapro/gows/proto"
"github.com/golang/protobuf/proto"
"go.mau.fi/whatsmeow/appstate"
waCommon "go.mau.fi/whatsmeow/proto/waCommon"
"go.mau.fi/whatsmeow/proto/waE2E"
waSyncAction "go.mau.fi/whatsmeow/proto/waSyncAction"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)

func (s *Server) MuteChat(ctx context.Context, req *__.ChatMuteRequest) (*__.Empty, error) {
cli, err := s.Sm.Get(req.GetSession().GetId())
if err != nil {
return nil, err
}
jid, err := types.ParseJID(req.GetJid())
if err != nil {
return nil, err
}

var muteDuration time.Duration
switch {
case req.GetMuteEndMs() != nil:
muteEndMs := req.GetMuteEndMs().GetValue()
if muteEndMs > 0 && muteEndMs < 1_000_000_000_000 {
muteEndMs *= 1000
}
remaining := time.Duration(muteEndMs-time.Now().UnixMilli()) * time.Millisecond
if remaining <= 0 {
patch := appstate.BuildMute(jid, false, 0)
if err := cli.SendAppState(ctx, patch); err != nil {
return nil, err
}
return &__.Empty{}, nil
}
muteDuration = remaining
case req.GetDurationSeconds() != nil:
sec := req.GetDurationSeconds().GetValue()
muteDuration = time.Duration(sec) * time.Second
default:
muteDuration = 8 * time.Hour
}

patch := appstate.BuildMute(jid, true, muteDuration)
if err := cli.SendAppState(ctx, patch); err != nil {
return nil, err
}
return &__.Empty{}, nil
}

func (s *Server) UnmuteChat(ctx context.Context, req *__.JidRequest) (*__.Empty, error) {
cli, err := s.Sm.Get(req.GetSession().GetId())
if err != nil {
return nil, err
}
jid, err := types.ParseJID(req.GetJid())
if err != nil {
return nil, err
}
patch := appstate.BuildMute(jid, false, 0)
if err := cli.SendAppState(ctx, patch); err != nil {
return nil, err
}
return &__.Empty{}, nil
}

func (s *Server) UpdateBlockStatus(ctx context.Context, req *__.UpdateBlockStatusRequest) (*__.Empty, error) {
cli, err := s.Sm.Get(req.GetSession().GetId())
if err != nil {
return nil, err
}
jid, err := types.ParseJID(req.GetJid())
if err != nil {
return nil, err
}
var action events.BlocklistChangeAction
switch req.GetAction() {
case __.BlockAction_BLOCK:
action = events.BlocklistChangeActionBlock
case __.BlockAction_UNBLOCK:
action = events.BlocklistChangeActionUnblock
default:
return nil, errors.New("invalid block action")
}
_, err = cli.UpdateBlocklist(ctx, jid, action)
if err != nil {
return nil, err
}
return &__.Empty{}, nil
}

func (s *Server) GetBlocklist(ctx context.Context, req *__.Session) (*__.BlocklistResponse, error) {
cli, err := s.Sm.Get(req.GetId())
if err != nil {
return nil, err
}
list, err := cli.GetBlocklist(ctx)
if err != nil {
return nil, err
}
jids := make([]string, 0, len(list.JIDs))
for _, j := range list.JIDs {
jids = append(jids, j.String())
}
return &__.BlocklistResponse{Jids: jids}, nil
}

func syncActionMessageRange(lastMessageTimestamp time.Time, lastMessageKey *waCommon.MessageKey) *waSyncAction.SyncActionMessageRange {
if lastMessageTimestamp.IsZero() {
lastMessageTimestamp = time.Now()
}
messageRange := &waSyncAction.SyncActionMessageRange{
LastMessageTimestamp: proto.Int64(lastMessageTimestamp.Unix()),
}
if lastMessageKey != nil {
messageRange.Messages = []*waSyncAction.SyncActionMessage{{
Key: lastMessageKey,
Timestamp: proto.Int64(lastMessageTimestamp.Unix()),
}}
}
return messageRange
}

func buildClearChat(target types.JID, lastMessageTimestamp time.Time, lastMessageKey *waCommon.MessageKey) appstate.PatchInfo {
action := &waSyncAction.ClearChatAction{
MessageRange: syncActionMessageRange(lastMessageTimestamp, lastMessageKey),
}
return appstate.PatchInfo{
Type: appstate.WAPatchRegularHigh,
Mutations: []appstate.MutationInfo{{
Index: []string{appstate.IndexClearChat, target.String()},
Version: 6,
Value: &waSyncAction.SyncActionValue{
ClearChatAction: action,
},
}},
}
}

func (s *Server) DeleteChat(ctx context.Context, req *__.JidRequest) (*__.Empty, error) {
cli, err := s.Sm.Get(req.GetSession().GetId())
if err != nil {
return nil, err
}
jid, err := types.ParseJID(req.GetJid())
if err != nil {
return nil, err
}
patch := appstate.BuildDeleteChat(jid, time.Now(), nil, false)
if err := cli.SendAppState(ctx, patch); err != nil {
return nil, err
}
return &__.Empty{}, nil
}

func (s *Server) ClearChat(ctx context.Context, req *__.JidRequest) (*__.Empty, error) {
cli, err := s.Sm.Get(req.GetSession().GetId())
if err != nil {
return nil, err
}
jid, err := types.ParseJID(req.GetJid())
if err != nil {
return nil, err
}
patch := buildClearChat(jid, time.Now(), nil)
if err := cli.SendAppState(ctx, patch); err != nil {
return nil, err
}
return &__.Empty{}, nil
}

func (s *Server) StarMessage(ctx context.Context, req *__.StarMessageRequest) (*__.Empty, error) {
cli, err := s.Sm.Get(req.GetSession().GetId())
if err != nil {
return nil, err
}
target, err := types.ParseJID(req.GetJid())
if err != nil {
return nil, err
}
sender, err := types.ParseJID(req.GetSender())
if err != nil {
return nil, err
}
fromMe := false
if cli.Store.ID != nil && sender.User == cli.Store.ID.User {
fromMe = true
} else if sender.User == cli.Store.LID.User && cli.Store.LID.User != "" {
fromMe = true
}
patch := appstate.BuildStar(target, sender, req.GetMessageId(), fromMe, req.GetStar())
if err := cli.SendAppState(ctx, patch); err != nil {
return nil, err
}
return &__.Empty{}, nil
}

func buildPinInChatMessage(
cli interface {
BuildMessageKey(chat, sender types.JID, id types.MessageID) *waCommon.MessageKey
},
chat types.JID,
sender types.JID,
messageID types.MessageID,
durationSeconds int64,
pin bool,
) *waE2E.Message {
pinType := waE2E.PinInChatMessage_PIN_FOR_ALL
if !pin {
pinType = waE2E.PinInChatMessage_UNPIN_FOR_ALL
}
message := &waE2E.Message{
PinInChatMessage: &waE2E.PinInChatMessage{
Key: cli.BuildMessageKey(chat, sender, messageID),
Type: pinType.Enum(),
SenderTimestampMS: proto.Int64(time.Now().UnixMilli()),
},
}
if pin && durationSeconds > 0 {
message.MessageContextInfo = &waE2E.MessageContextInfo{
MessageAddOnDurationInSecs: proto.Uint32(uint32(durationSeconds)),
MessageAddOnExpiryType: waE2E.MessageContextInfo_STATIC.Enum(),
}
}
return message
}

func (s *Server) PinMessage(ctx context.Context, req *__.PinMessageRequest) (*__.Empty, error) {
cli, err := s.Sm.Get(req.GetSession().GetId())
if err != nil {
return nil, err
}
chat, err := types.ParseJID(req.GetJid())
if err != nil {
return nil, err
}
sender, err := types.ParseJID(req.GetSender())
if err != nil {
return nil, err
}
message := buildPinInChatMessage(cli, chat, sender, req.GetMessageId(), req.GetDurationSeconds(), true)
_, err = cli.SendMessage(ctx, chat, message)
if err != nil {
return nil, err
}
return &__.Empty{}, nil
}

func (s *Server) UnpinMessage(ctx context.Context, req *__.UnpinMessageRequest) (*__.Empty, error) {
cli, err := s.Sm.Get(req.GetSession().GetId())
if err != nil {
return nil, err
}
chat, err := types.ParseJID(req.GetJid())
if err != nil {
return nil, err
}
sender, err := types.ParseJID(req.GetSender())
if err != nil {
return nil, err
}
message := buildPinInChatMessage(cli, chat, sender, req.GetMessageId(), 0, false)
_, err = cli.SendMessage(ctx, chat, message)
if err != nil {
return nil, err
}
return &__.Empty{}, nil
}
Loading