Skip to content

Commit 83015fc

Browse files
authored
Merge pull request #1 from rakshabesafe/feat/chat-autoreply
feat: Implement chat auto-reply feature
2 parents 28fb335 + 4ae3f53 commit 83015fc

File tree

5 files changed

+211
-0
lines changed

5 files changed

+211
-0
lines changed

API.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,64 @@ curl -s -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data
705705

706706
---
707707

708+
### Add/Update Auto-Reply
709+
* **Method:** `POST`
710+
* **Path:** `/chat/autoreply`
711+
* **Description:** Sets up an auto-reply for a specific phone number. If an auto-reply for the given phone number already exists for the user, it will be updated. If not, a new one will be created.
712+
* **Authentication:** Requires user token.
713+
* **Request Body (JSON):**
714+
```json
715+
{
716+
"Phone": "1234567890", // Target phone number (normalized, e.g., digits only or international format used by the system)
717+
"Body": "Hello! I am currently unavailable and will get back to you soon."
718+
}
719+
```
720+
* **Responses:**
721+
* `201 Created`: If a new auto-reply was successfully created.
722+
```json
723+
{
724+
"code": 201,
725+
"data": {
726+
"detail": "Auto-reply added successfully",
727+
"id": "generated-unique-id-for-the-rule"
728+
},
729+
"success": true
730+
}
731+
```
732+
* `400 Bad Request`: If `Phone` or `Body` is missing or invalid.
733+
* `409 Conflict`: If an auto-reply for this phone number already exists for the user.
734+
* `500 Internal Server Error`: For other server-side errors.
735+
736+
---
737+
738+
### Delete Auto-Reply
739+
* **Method:** `DELETE`
740+
* **Path:** `/chat/autoreply`
741+
* **Description:** Deletes an existing auto-reply for a specific phone number for the authenticated user.
742+
* **Authentication:** Requires user token.
743+
* **Request Body (JSON):**
744+
```json
745+
{
746+
"Phone": "1234567890" // Target phone number whose auto-reply rule should be deleted.
747+
}
748+
```
749+
* **Responses:**
750+
* `200 OK`: If the auto-reply was successfully deleted.
751+
```json
752+
{
753+
"code": 200,
754+
"data": {
755+
"detail": "Auto-reply deleted successfully"
756+
},
757+
"success": true
758+
}
759+
```
760+
* `400 Bad Request`: If `Phone` is missing or invalid.
761+
* `404 Not Found`: If no auto-reply rule exists for the given phone number for this user.
762+
* `500 Internal Server Error`: For other server-side errors.
763+
764+
---
765+
708766
## Download Document
709767

710768
Downloads a Document from a message and retrieves it Base64 media encoded. Required request parameters are: Url, MediaKey, Mimetype, FileSHA256 and FileLength

handlers.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ import (
3131
"google.golang.org/protobuf/proto"
3232
)
3333

34+
type AutoReplyRequest struct {
35+
Phone string `json:"Phone"`
36+
Body string `json:"Body"`
37+
}
38+
39+
type DeleteAutoReplyRequest struct {
40+
Phone string `json:"Phone"`
41+
}
42+
3443
type Values struct {
3544
m map[string]string
3645
}
@@ -202,6 +211,103 @@ func (s *server) Connect() http.HandlerFunc {
202211
}
203212
}
204213

214+
// AddAutoReply handles adding a new auto-reply entry for a user.
215+
func (s *server) AddAutoReply() http.HandlerFunc {
216+
return func(w http.ResponseWriter, r *http.Request) {
217+
txtid := r.Context().Value("userinfo").(Values).Get("Id")
218+
219+
var req AutoReplyRequest
220+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
221+
s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode payload"))
222+
return
223+
}
224+
225+
if req.Phone == "" {
226+
s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload"))
227+
return
228+
}
229+
if req.Body == "" {
230+
s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Body in Payload"))
231+
return
232+
}
233+
234+
newId, err := GenerateRandomID() // Assuming GenerateRandomID is accessible from migrations.go
235+
if err != nil {
236+
log.Error().Err(err).Msg("Failed to generate random ID for auto-reply")
237+
s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to create auto-reply entry"))
238+
return
239+
}
240+
241+
_, err = s.db.Exec("INSERT INTO autoreplies (id, user_id, phone_number, reply_body) VALUES ($1, $2, $3, $4)", newId, txtid, req.Phone, req.Body)
242+
if err != nil {
243+
// Check for unique constraint violation (specific error code might depend on DB: PostgreSQL uses "23505")
244+
// This is a simplified check; a more robust way involves checking pq.Error.Code or sqlite3.ErrConstraintUnique
245+
if strings.Contains(err.Error(), "UNIQUE constraint failed") || strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
246+
s.Respond(w, r, http.StatusConflict, errors.New("Auto-reply for this phone number already exists for the user"))
247+
return
248+
}
249+
log.Error().Err(err).Str("user_id", txtid).Str("phone", req.Phone).Msg("Failed to add auto-reply")
250+
s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to add auto-reply"))
251+
return
252+
}
253+
254+
response := map[string]string{"detail": "Auto-reply added successfully", "id": newId}
255+
responseJson, err := json.Marshal(response)
256+
if err != nil {
257+
log.Error().Err(err).Msg("Failed to marshal success response for AddAutoReply")
258+
s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to create auto-reply entry"))
259+
return
260+
}
261+
s.Respond(w, r, http.StatusCreated, string(responseJson))
262+
}
263+
}
264+
265+
// DeleteAutoReply handles deleting an auto-reply entry for a user.
266+
func (s *server) DeleteAutoReply() http.HandlerFunc {
267+
return func(w http.ResponseWriter, r *http.Request) {
268+
txtid := r.Context().Value("userinfo").(Values).Get("Id")
269+
270+
var req DeleteAutoReplyRequest
271+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
272+
s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode payload"))
273+
return
274+
}
275+
276+
if req.Phone == "" {
277+
s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload"))
278+
return
279+
}
280+
281+
result, err := s.db.Exec("DELETE FROM autoreplies WHERE user_id = $1 AND phone_number = $2", txtid, req.Phone)
282+
if err != nil {
283+
log.Error().Err(err).Str("user_id", txtid).Str("phone", req.Phone).Msg("Failed to delete auto-reply")
284+
s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to delete auto-reply"))
285+
return
286+
}
287+
288+
rowsAffected, err := result.RowsAffected()
289+
if err != nil {
290+
log.Error().Err(err).Str("user_id", txtid).Str("phone", req.Phone).Msg("Failed to check affected rows after delete")
291+
s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to confirm deletion status"))
292+
return
293+
}
294+
295+
if rowsAffected == 0 {
296+
s.Respond(w, r, http.StatusNotFound, errors.New("Auto-reply not found for this user and phone number"))
297+
return
298+
}
299+
300+
response := map[string]string{"detail": "Auto-reply deleted successfully"}
301+
responseJson, err := json.Marshal(response)
302+
if err != nil {
303+
log.Error().Err(err).Msg("Failed to marshal success response for DeleteAutoReply")
304+
s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to process deletion confirmation")) // Should ideally not happen
305+
return
306+
}
307+
s.Respond(w, r, http.StatusOK, string(responseJson))
308+
}
309+
}
310+
205311
// Disconnects from Whatsapp websocket, does not log out device
206312
func (s *server) Disconnect() http.HandlerFunc {
207313
return func(w http.ResponseWriter, r *http.Request) {

main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import (
1414
"syscall"
1515
"time"
1616

17+
"database/sql" // Added for sql.ErrNoRows
18+
"go.mau.fi/whatsmeow" // Added for whatsmeow.SendRequestExtra
19+
"go.mau.fi/whatsmeow/proto/waE2E" // Added for waE2E.Message
20+
"google.golang.org/protobuf/proto" // Added for proto.String
21+
1722
"go.mau.fi/whatsmeow/store/sqlstore"
1823
waLog "go.mau.fi/whatsmeow/util/log"
1924

migrations.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,41 @@ var migrations = []Migration{
4545
Name: "change_id_to_string",
4646
UpSQL: changeIDToStringSQL,
4747
},
48+
{
49+
ID: 4,
50+
Name: "add_autoreplies_table",
51+
UpSQL: addAutorepliesTableSQLPostgres,
52+
},
4853
}
4954

55+
const addAutorepliesTableSQLPostgres = `
56+
DO $$
57+
BEGIN
58+
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'autoreplies') THEN
59+
CREATE TABLE autoreplies (
60+
id TEXT PRIMARY KEY,
61+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
62+
phone_number TEXT NOT NULL,
63+
reply_body TEXT NOT NULL,
64+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
65+
UNIQUE(user_id, phone_number)
66+
);
67+
END IF;
68+
END $$;
69+
`
70+
71+
const addAutorepliesTableSQLSQLite = `
72+
CREATE TABLE autoreplies (
73+
id TEXT PRIMARY KEY,
74+
user_id TEXT NOT NULL,
75+
phone_number TEXT NOT NULL,
76+
reply_body TEXT NOT NULL,
77+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
78+
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
79+
UNIQUE(user_id, phone_number)
80+
)
81+
`
82+
5083
const changeIDToStringSQL = `
5184
-- Migration to change ID from integer to random string
5285
DO $$
@@ -228,6 +261,12 @@ func applyMigration(db *sqlx.DB, migration Migration) error {
228261
} else {
229262
_, err = tx.Exec(migration.UpSQL)
230263
}
264+
} else if migration.ID == 4 {
265+
if db.DriverName() == "sqlite" {
266+
err = createTableIfNotExistsSQLite(tx, "autoreplies", addAutorepliesTableSQLSQLite)
267+
} else {
268+
_, err = tx.Exec(migration.UpSQL)
269+
}
231270
} else {
232271
_, err = tx.Exec(migration.UpSQL)
233272
}

routes.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ func (s *server) routes() {
101101
s.router.Handle("/chat/send/poll", c.Then(s.SendPoll())).Methods("POST")
102102
s.router.Handle("/chat/send/edit", c.Then(s.SendEditMessage())).Methods("POST")
103103

104+
s.router.Handle("/chat/autoreply", c.Then(s.AddAutoReply())).Methods("POST")
105+
s.router.Handle("/chat/autoreply", c.Then(s.DeleteAutoReply())).Methods("DELETE")
106+
104107
s.router.Handle("/user/presence", c.Then(s.SendPresence())).Methods("POST")
105108
s.router.Handle("/user/info", c.Then(s.GetUser())).Methods("POST")
106109
s.router.Handle("/user/check", c.Then(s.CheckUser())).Methods("POST")

0 commit comments

Comments
 (0)