From dd4ab8f01024d90b2cab111c3ece8b03ecd1cb61 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Fri, 11 Oct 2024 22:54:53 +0700 Subject: [PATCH 1/6] feat(newsletter): add newsletter service, endpoints, and UI components Add Newsletter service to support functionality for unfollowing newsletters Add Newsletter REST controller and routing Implement newsletter-related endpoints and methods in the User Service Create UnfollowRequest for the INewsletterService interface Add MyListNewsletterResponse to user's data fields Add newsletter validation Refactor JS components to support newsletter type and simplify recipient forms by moving logic to FormRecipient component Refactor window global constants to support newsletters Modify server to initialize and use the newsletter services Add UI component for listing newsletters Refactor existing components to use FormRecipient for recipient data input --- src/cmd/root.go | 2 + src/domains/newletter/newsletter.go | 11 ++ src/domains/user/account.go | 4 + src/domains/user/user.go | 1 + src/internal/rest/newsletter.go | 32 +++++ src/internal/rest/user.go | 13 ++ src/pkg/whatsapp/whatsapp.go | 22 ++-- src/services/newsletter.go | 32 +++++ src/services/user.go | 14 +++ src/validations/newsletter_validation.go | 20 +++ src/views/components/AccountAvatar.js | 25 ++-- src/views/components/AccountUserInfo.js | 24 ++-- src/views/components/MessageDelete.js | 26 ++-- src/views/components/MessageReact.js | 26 ++-- src/views/components/MessageRevoke.js | 26 ++-- src/views/components/MessageUpdate.js | 26 ++-- src/views/components/NewsletterList.js | 115 ++++++++++++++++++ src/views/components/SendAudio.js | 25 ++-- src/views/components/SendContact.js | 26 ++-- src/views/components/SendFile.js | 26 ++-- src/views/components/SendImage.js | 26 ++-- src/views/components/SendLocation.js | 26 ++-- src/views/components/SendMessage.js | 25 ++-- src/views/components/SendPoll.js | 26 ++-- src/views/components/SendVideo.js | 26 ++-- src/views/components/generic/FormRecipient.js | 52 ++++++++ src/views/index.html | 17 ++- 27 files changed, 458 insertions(+), 236 deletions(-) create mode 100644 src/domains/newletter/newsletter.go create mode 100644 src/internal/rest/newsletter.go create mode 100644 src/services/newsletter.go create mode 100644 src/validations/newsletter_validation.go create mode 100644 src/views/components/NewsletterList.js create mode 100644 src/views/components/generic/FormRecipient.js diff --git a/src/cmd/root.go b/src/cmd/root.go index e5b0406..b93c224 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -113,6 +113,7 @@ func runRest(_ *cobra.Command, _ []string) { userService := services.NewUserService(cli) messageService := services.NewMessageService(cli) groupService := services.NewGroupService(cli) + newsletterService := services.NewNewsletterService(cli) // Rest rest.InitRestApp(app, appService) @@ -120,6 +121,7 @@ func runRest(_ *cobra.Command, _ []string) { rest.InitRestUser(app, userService) rest.InitRestMessage(app, messageService) rest.InitRestGroup(app, groupService) + rest.InitRestNewsletter(app, newsletterService) app.Get("/", func(c *fiber.Ctx) error { return c.Render("views/index", fiber.Map{ diff --git a/src/domains/newletter/newsletter.go b/src/domains/newletter/newsletter.go new file mode 100644 index 0000000..093ae75 --- /dev/null +++ b/src/domains/newletter/newsletter.go @@ -0,0 +1,11 @@ +package newletter + +import "context" + +type INewsletterService interface { + Unfollow(ctx context.Context, request UnfollowRequest) (err error) +} + +type UnfollowRequest struct { + NewsletterID string `json:"newsletter_id" form:"newsletter_id"` +} diff --git a/src/domains/user/account.go b/src/domains/user/account.go index ad23e04..d424755 100644 --- a/src/domains/user/account.go +++ b/src/domains/user/account.go @@ -48,3 +48,7 @@ type MyPrivacySettingResponse struct { type MyListGroupsResponse struct { Data []types.GroupInfo `json:"data"` } + +type MyListNewsletterResponse struct { + Data []types.NewsletterMetadata `json:"data"` +} diff --git a/src/domains/user/user.go b/src/domains/user/user.go index dabca89..feb3b78 100644 --- a/src/domains/user/user.go +++ b/src/domains/user/user.go @@ -8,5 +8,6 @@ type IUserService interface { Info(ctx context.Context, request InfoRequest) (response InfoResponse, err error) Avatar(ctx context.Context, request AvatarRequest) (response AvatarResponse, err error) MyListGroups(ctx context.Context) (response MyListGroupsResponse, err error) + MyListNewsletter(ctx context.Context) (response MyListNewsletterResponse, err error) MyPrivacySetting(ctx context.Context) (response MyPrivacySettingResponse, err error) } diff --git a/src/internal/rest/newsletter.go b/src/internal/rest/newsletter.go new file mode 100644 index 0000000..bfb7507 --- /dev/null +++ b/src/internal/rest/newsletter.go @@ -0,0 +1,32 @@ +package rest + +import ( + domainNewsletter "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/newletter" + "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" + "github.com/gofiber/fiber/v2" +) + +type Newsletter struct { + Service domainNewsletter.INewsletterService +} + +func InitRestNewsletter(app *fiber.App, service domainNewsletter.INewsletterService) Newsletter { + rest := Newsletter{Service: service} + app.Post("/newsletter/unfollow", rest.Unfollow) + return rest +} + +func (controller *Newsletter) Unfollow(c *fiber.Ctx) error { + var request domainNewsletter.UnfollowRequest + err := c.BodyParser(&request) + utils.PanicIfNeeded(err) + + err = controller.Service.Unfollow(c.UserContext(), request) + utils.PanicIfNeeded(err) + + return c.JSON(utils.ResponseData{ + Status: 200, + Code: "SUCCESS", + Message: "Success unfollow newsletter", + }) +} diff --git a/src/internal/rest/user.go b/src/internal/rest/user.go index dd01f5a..6621bb2 100644 --- a/src/internal/rest/user.go +++ b/src/internal/rest/user.go @@ -17,6 +17,7 @@ func InitRestUser(app *fiber.App, service domainUser.IUserService) User { app.Get("/user/avatar", rest.UserAvatar) app.Get("/user/my/privacy", rest.UserMyPrivacySetting) app.Get("/user/my/groups", rest.UserMyListGroups) + app.Get("/user/my/newsletters", rest.UserMyListNewsletter) return rest } @@ -80,3 +81,15 @@ func (controller *User) UserMyListGroups(c *fiber.Ctx) error { Results: response, }) } + +func (controller *User) UserMyListNewsletter(c *fiber.Ctx) error { + response, err := controller.Service.MyListNewsletter(c.UserContext()) + utils.PanicIfNeeded(err) + + return c.JSON(utils.ResponseData{ + Status: 200, + Code: "SUCCESS", + Message: "Success get list newsletter", + Results: response, + }) +} diff --git a/src/pkg/whatsapp/whatsapp.go b/src/pkg/whatsapp/whatsapp.go index d0f4953..77e20f8 100644 --- a/src/pkg/whatsapp/whatsapp.go +++ b/src/pkg/whatsapp/whatsapp.go @@ -107,17 +107,19 @@ func ParseJID(arg string) (types.JID, error) { } if !strings.ContainsRune(arg, '@') { return types.NewJID(arg, types.DefaultUserServer), nil - } else { - recipient, err := types.ParseJID(arg) - if err != nil { - fmt.Printf("invalid JID %s: %v", arg, err) - return recipient, pkgError.ErrInvalidJID - } else if recipient.User == "" { - fmt.Printf("invalid JID %v: no server specified", arg) - return recipient, pkgError.ErrInvalidJID - } - return recipient, nil } + + recipient, err := types.ParseJID(arg) + if err != nil { + fmt.Printf("invalid JID %s: %v", arg, err) + return recipient, pkgError.ErrInvalidJID + } + + if recipient.User == "" { + fmt.Printf("invalid JID %v: no server specified", arg) + return recipient, pkgError.ErrInvalidJID + } + return recipient, nil } func IsOnWhatsapp(waCli *whatsmeow.Client, jid string) bool { diff --git a/src/services/newsletter.go b/src/services/newsletter.go new file mode 100644 index 0000000..2fb90ed --- /dev/null +++ b/src/services/newsletter.go @@ -0,0 +1,32 @@ +package services + +import ( + "context" + domainNewsletter "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/newletter" + "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp" + "github.com/aldinokemal/go-whatsapp-web-multidevice/validations" + "go.mau.fi/whatsmeow" +) + +type newsletterService struct { + WaCli *whatsmeow.Client +} + +func NewNewsletterService(waCli *whatsmeow.Client) domainNewsletter.INewsletterService { + return &newsletterService{ + WaCli: waCli, + } +} + +func (service newsletterService) Unfollow(ctx context.Context, request domainNewsletter.UnfollowRequest) (err error) { + if err = validations.ValidateUnfollowNewsletter(ctx, request); err != nil { + return err + } + + JID, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.NewsletterID) + if err != nil { + return err + } + + return service.WaCli.UnfollowNewsletter(JID) +} diff --git a/src/services/user.go b/src/services/user.go index a6e7b8a..19d0652 100644 --- a/src/services/user.go +++ b/src/services/user.go @@ -127,6 +127,20 @@ func (service userService) MyListGroups(_ context.Context) (response domainUser. return response, nil } +func (service userService) MyListNewsletter(_ context.Context) (response domainUser.MyListNewsletterResponse, err error) { + whatsapp.MustLogin(service.WaCli) + + datas, err := service.WaCli.GetSubscribedNewsletters() + if err != nil { + return + } + fmt.Printf("%+v\n", datas) + for _, data := range datas { + response.Data = append(response.Data, *data) + } + return response, nil +} + func (service userService) MyPrivacySetting(_ context.Context) (response domainUser.MyPrivacySettingResponse, err error) { whatsapp.MustLogin(service.WaCli) diff --git a/src/validations/newsletter_validation.go b/src/validations/newsletter_validation.go new file mode 100644 index 0000000..da61da9 --- /dev/null +++ b/src/validations/newsletter_validation.go @@ -0,0 +1,20 @@ +package validations + +import ( + "context" + domainNewsletter "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/newletter" + pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +func ValidateUnfollowNewsletter(ctx context.Context, request domainNewsletter.UnfollowRequest) error { + err := validation.ValidateStructWithContext(ctx, &request, + validation.Field(&request.NewsletterID, validation.Required), + ) + + if err != nil { + return pkgError.ValidationError(err.Error()) + } + + return nil +} diff --git a/src/views/components/AccountAvatar.js b/src/views/components/AccountAvatar.js index 2e602cb..e9ab15f 100644 --- a/src/views/components/AccountAvatar.js +++ b/src/views/components/AccountAvatar.js @@ -1,8 +1,13 @@ +import FormRecipient from "./generic/FormRecipient.js"; + export default { name: 'AccountAvatar', + components: { + FormRecipient + }, data() { return { - type: 'user', + type: window.TYPEUSER, phone: '', image: null, loading: false, @@ -12,7 +17,7 @@ export default { }, computed: { phone_id() { - return this.type === 'user' ? `${this.phone}@${window.TYPEUSER}` : `${this.phone}@${window.TYPEGROUP}` + return this.phone + this.type; } }, methods: { @@ -45,7 +50,7 @@ export default { handleReset() { this.phone = ''; this.image = null; - this.type = 'user'; + this.type = window.TYPEUSER; } }, template: ` @@ -66,19 +71,7 @@ export default {
-
- - -
-
- - - -
+
diff --git a/src/views/components/AccountUserInfo.js b/src/views/components/AccountUserInfo.js index a034990..8e307bf 100644 --- a/src/views/components/AccountUserInfo.js +++ b/src/views/components/AccountUserInfo.js @@ -1,8 +1,13 @@ +import FormRecipient from "./generic/FormRecipient.js"; + export default { name: 'AccountUserInfo', + components: { + FormRecipient + }, data() { return { - type: 'user', + type: window.TYPEUSER, phone: '', // name: null, @@ -15,7 +20,7 @@ export default { computed: { phone_id() { - return this.type === 'user' ? `${this.phone}@${window.TYPEUSER}` : `${this.phone}@${window.TYPEGROUP}` + return this.phone + this.type; } }, methods: { @@ -52,7 +57,7 @@ export default { this.name = null; this.status = null; this.devices = []; - this.type = 'user'; + this.type = window.TYPEUSER; } }, template: ` @@ -74,18 +79,7 @@ export default {
-
- - -
-
- - - -
+
-
- - -
-
- - - -
+ +
-
- - -
-
- - - -
+ +
-
- - -
-
- - - -
+ +
-
- - -
-
- - - -
+ +
+
+ Newsletter +
List Newsletters
+
+ Display all your newsletters +
+
+
+ + + + ` +} \ No newline at end of file diff --git a/src/views/components/SendAudio.js b/src/views/components/SendAudio.js index 7651ddd..c237e1a 100644 --- a/src/views/components/SendAudio.js +++ b/src/views/components/SendAudio.js @@ -1,15 +1,20 @@ +import FormRecipient from "./generic/FormRecipient.js"; + export default { name: 'Send', + components: { + FormRecipient + }, data() { return { phone: '', - type: 'user', + type: window.TYPEUSER, loading: false, } }, computed: { phone_id() { - return this.type === 'user' ? `${this.phone}@${window.TYPEUSER}` : `${this.phone}@${window.TYPEGROUP}` + return this.phone + this.type; } }, methods: { @@ -49,7 +54,7 @@ export default { }, handleReset() { this.phone = ''; - this.type = 'user'; + this.type = window.TYPEUSER; $("#file_audio").val(''); }, }, @@ -72,19 +77,7 @@ export default {
-
- - -
-
- - - -
+
diff --git a/src/views/components/SendContact.js b/src/views/components/SendContact.js index 2123962..21b899d 100644 --- a/src/views/components/SendContact.js +++ b/src/views/components/SendContact.js @@ -1,8 +1,13 @@ +import FormRecipient from "./generic/FormRecipient.js"; + export default { name: 'SendContact', + components: { + FormRecipient + }, data() { return { - type: 'user', + type: window.TYPEUSER, phone: '', card_name: '', card_phone: '', @@ -11,7 +16,7 @@ export default { }, computed: { phone_id() { - return this.type === 'user' ? `${this.phone}@${window.TYPEUSER}` : `${this.phone}@${window.TYPEGROUP}` + return this.phone + this.type; } }, methods: { @@ -58,7 +63,7 @@ export default { this.phone = ''; this.card_name = ''; this.card_phone = ''; - this.type = 'user'; + this.type = window.TYPEUSER; }, }, template: ` @@ -80,19 +85,8 @@ export default {
-
- - -
-
- - - -
+ +
-
- - -
-
- - - -
+ +