/
handlers.go
1542 lines (1472 loc) · 38.4 KB
/
handlers.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package core
import (
"crypto/hmac"
"crypto/sha512"
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html/template"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
w "golang.org/x/net/websocket"
"github.com/itsabot/abot/core/log"
"github.com/itsabot/abot/core/websocket"
"github.com/itsabot/abot/shared/datatypes"
"github.com/itsabot/abot/shared/interface/emailsender"
"github.com/itsabot/abot/shared/prefs"
"github.com/julienschmidt/httprouter"
)
var tmplLayout *template.Template
var ws = websocket.NewAtomicWebSocketSet()
// ErrInvalidUserPass reports an invalid username/password combination during
// login.
var ErrInvalidUserPass = errors.New("Invalid username/password combination")
var regexNum = regexp.MustCompile(`\D+`)
// newRouter initializes and returns a router.
func newRouter() *httprouter.Router {
router := httprouter.New()
router.ServeFiles("/public/*filepath", http.Dir("public"))
if os.Getenv("ABOT_ENV") != "production" {
initCMDGroup(router)
}
// Web routes
router.HandlerFunc("GET", "/", hIndex)
router.HandlerFunc("POST", "/", hMain)
router.HandlerFunc("OPTIONS", "/", hOptions)
// Route any unknown request to our single page app front-end
router.NotFound = http.HandlerFunc(hIndex)
// API routes (no restrictions)
router.HandlerFunc("POST", "/api/login.json", hapiLoginSubmit)
router.HandlerFunc("POST", "/api/logout.json", hapiLogoutSubmit)
router.HandlerFunc("POST", "/api/signup.json", hapiSignupSubmit)
router.HandlerFunc("POST", "/api/forgot_password.json", hapiForgotPasswordSubmit)
router.HandlerFunc("POST", "/api/reset_password.json", hapiResetPasswordSubmit)
router.HandlerFunc("GET", "/api/admin_exists.json", hapiAdminExists)
// API routes (restricted by login)
router.HandlerFunc("GET", "/api/user/profile.json", hapiProfile)
router.HandlerFunc("PUT", "/api/user/profile.json", hapiProfileView)
// API routes (restricted to admins)
router.HandlerFunc("GET", "/api/admin/plugins.json", hapiPlugins)
router.HandlerFunc("GET", "/api/admin/conversations_need_training.json", hapiConversationsNeedTraining)
router.Handle("GET", "/api/admin/conversations/:uid/:fid/:fidt/:off", hapiConversation)
router.HandlerFunc("PATCH", "/api/admin/conversations.json", hapiConversationsUpdate)
router.HandlerFunc("POST", "/api/admins/send_message.json", hapiSendMessage)
router.HandlerFunc("GET", "/api/admins.json", hapiAdmins)
router.HandlerFunc("PUT", "/api/admins.json", hapiAdminsUpdate)
router.HandlerFunc("GET", "/api/admin/remote_tokens.json", hapiRemoteTokens)
router.HandlerFunc("POST", "/api/admin/remote_tokens.json", hapiRemoteTokensSubmit)
router.HandlerFunc("DELETE", "/api/admin/remote_tokens.json", hapiRemoteTokensDelete)
router.HandlerFunc("PUT", "/api/admin/settings.json", hapiSettingsUpdate)
router.HandlerFunc("GET", "/api/admin/dashboard.json", hapiDashboard)
return router
}
// hIndex presents the homepage to the user and populates the HTML with
// server-side variables.
func hIndex(w http.ResponseWriter, r *http.Request) {
var err error
env := os.Getenv("ABOT_ENV")
if env != "production" && env != "test" {
p := filepath.Join("assets", "html", "layout.html")
tmplLayout, err = template.ParseFiles(p)
if err != nil {
writeErrorInternal(w, err)
return
}
if err = compileAssets(); err != nil {
writeErrorInternal(w, err)
return
}
}
data := struct {
IsProd bool
ItsAbotURL string
}{
IsProd: os.Getenv("ABOT_ENV") == "production",
ItsAbotURL: os.Getenv("ITSABOT_URL"),
}
if err = tmplLayout.Execute(w, data); err != nil {
writeErrorInternal(w, err)
}
}
// hMain is the endpoint to hit when you want a direct response via JSON.
// The Abot console uses this endpoint.
func hMain(w http.ResponseWriter, r *http.Request) {
errMsg := "Something went wrong with my wiring... I'll get that fixed up soon."
ret, err := ProcessText(r)
if err != nil {
if len(ret) > 0 {
ret = errMsg
}
log.Info("failed to process text", err)
// TODO notify plugins listening for errors
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Access-Control-Allow-Origin")
_, err = fmt.Fprint(w, ret)
if err != nil {
writeErrorInternal(w, err)
}
}
// hOptions sets appropriate response headers in cases like browser-based
// communication with Abot.
func hOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Access-Control-Allow-Origin")
w.WriteHeader(http.StatusOK)
}
// hapiLogoutSubmit processes a logout request deleting the session from
// the server.
func hapiLogoutSubmit(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("id")
if err != nil {
writeError(w, err)
return
}
uid := cookie.Value
if uid == "null" {
http.Error(w, "id was null", http.StatusBadRequest)
return
}
q := `DELETE FROM sessions WHERE userid=$1`
if _, err = db.Exec(q, uid); err != nil {
writeError(w, err)
return
}
w.WriteHeader(http.StatusOK)
}
// hapiLoginSubmit processes a logout request deleting the session from
// the server.
func hapiLoginSubmit(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string
Password string
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorInternal(w, err)
return
}
var u struct {
ID uint64
Password []byte
Admin bool
}
q := `SELECT id, password, admin FROM users WHERE email=$1`
err := db.Get(&u, q, req.Email)
if err == sql.ErrNoRows {
writeErrorAuth(w, ErrInvalidUserPass)
return
} else if err != nil {
writeErrorInternal(w, err)
return
}
if u.ID == 0 {
writeErrorAuth(w, ErrInvalidUserPass)
return
}
err = bcrypt.CompareHashAndPassword(u.Password, []byte(req.Password))
if err == bcrypt.ErrMismatchedHashAndPassword || err == bcrypt.ErrHashTooShort {
writeErrorAuth(w, ErrInvalidUserPass)
return
} else if err != nil {
writeErrorInternal(w, err)
return
}
user := &dt.User{
ID: u.ID,
Email: req.Email,
Admin: u.Admin,
}
csrfToken, err := createCSRFToken(user)
if err != nil {
writeErrorInternal(w, err)
return
}
header, token, err := getAuthToken(user)
if err != nil {
writeErrorInternal(w, err)
return
}
resp := struct {
ID uint64
Email string
Scopes []string
AuthToken string
IssuedAt int64
CSRFToken string
}{
ID: user.ID,
Email: user.Email,
Scopes: header.Scopes,
AuthToken: token,
IssuedAt: header.IssuedAt,
CSRFToken: csrfToken,
}
writeBytes(w, resp)
}
// hapiSignupSubmit signs up a user after server-side validation of all
// passed in values.
func hapiSignupSubmit(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string
Email string
Password string
FID string
// Admin is only used to check whether existing users are in
// the DB. Only the first user in the DB can become an admin by
// signing up. Additional admins must be added in the admin
// panel under Manage Team.
Admin bool
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorInternal(w, err)
return
}
// Validate the request parameters
if len(req.Name) == 0 {
writeErrorBadRequest(w, errors.New("You must enter a name."))
return
}
if len(req.Email) == 0 ||
!strings.ContainsAny(req.Email, "@") ||
!strings.ContainsAny(req.Email, ".") {
writeErrorBadRequest(w, errors.New("You must enter a valid email."))
return
}
if len(req.Password) < 8 {
writeErrorBadRequest(w, errors.New("Your password must be at least 8 characters."))
return
}
// Remove everything except numbers
req.FID = regexNum.ReplaceAllString(req.FID, "")
if len(req.FID) < 10 {
writeErrorBadRequest(w, errors.New("Your phone number must be at least 10 digits."))
return
}
if req.FID[0] != '1' {
if len(req.FID) >= 11 {
writeErrorBadRequest(w, errors.New("Invalid country code. Currently only American numbers are supported."))
return
}
req.FID = "+1" + req.FID
} else {
req.FID = "+" + req.FID
}
var admin bool
if req.Admin {
var count int
q := `SELECT COUNT(*) FROM users WHERE admin=TRUE`
if err := db.Get(&count, q); err != nil {
writeErrorInternal(w, err)
return
}
if count > 0 {
writeErrorBadRequest(w, errors.New("invalid param Admin"))
return
}
admin = true
}
// TODO format phone number for SMS interface (international format)
user := &dt.User{
Name: req.Name,
Email: req.Email,
// Password is hashed in user.Create()
Password: req.Password,
Trainer: false,
Admin: admin,
}
err := user.Create(db, dt.FlexIDType(2), req.FID)
if err != nil {
writeErrorInternal(w, err)
return
}
csrfToken, err := createCSRFToken(user)
if err != nil {
writeErrorInternal(w, err)
return
}
header, token, err := getAuthToken(user)
if err != nil {
writeErrorInternal(w, err)
return
}
resp := struct {
ID uint64
Email string
Scopes []string
AuthToken string
IssuedAt int64
CSRFToken string
}{
ID: user.ID,
Email: user.Email,
Scopes: header.Scopes,
AuthToken: token,
IssuedAt: header.IssuedAt,
CSRFToken: csrfToken,
}
resp.ID = user.ID
log.Info("user signed up. id", user.ID)
writeBytes(w, resp)
}
// hapiProfile shows a user profile with the user's current addresses, credit
// cards, and contact information.
func hapiProfile(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isLoggedIn(w, r) {
return
}
}
cookie, err := r.Cookie("id")
if err != nil {
writeErrorInternal(w, err)
return
}
uid := cookie.Value
var user struct {
Name string
Email string
Phones []dt.Phone
}
q := `SELECT name, email FROM users WHERE id=$1`
err = db.Get(&user, q, uid)
if err != nil {
writeErrorInternal(w, err)
return
}
q = `SELECT flexid FROM userflexids
WHERE flexidtype=2 AND userid=$1
LIMIT 10`
err = db.Select(&user.Phones, q, uid)
if err != nil && err != sql.ErrNoRows {
writeErrorInternal(w, err)
return
}
writeBytes(w, user)
}
// hapiProfileView is used to validate a purchase or disclosure of
// sensitive information by a plugin. This method of validation has the user
// view their profile page, meaning that they have to be logged in on their
// device, ensuring that they either have a valid email/password or a valid
// session token in their cookies before the plugin will continue. This is a
// useful security measure because SMS is not a secure means of communication;
// SMS messages can easily be hijacked or spoofed. Taking the user to an HTTPS
// site offers the developer a better guarantee that information entered is
// coming from the correct person.
func hapiProfileView(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isLoggedIn(w, r) {
return
}
if !isValidCSRF(w, r) {
return
}
}
cookie, err := r.Cookie("id")
if err != nil {
writeErrorInternal(w, err)
return
}
uid := cookie.Value
q := `SELECT authorizationid FROM users WHERE id=$1`
var authID sql.NullInt64
if err = db.Get(&authID, q, uid); err != nil {
writeErrorInternal(w, err)
return
}
if !authID.Valid {
// We don't have an auth request in the database for this user,
// which is fine.
goto Response
}
q = `UPDATE authorizations SET authorizedat=$1 WHERE id=$2`
_, err = db.Exec(q, time.Now(), authID)
if err != nil {
writeErrorInternal(w, err)
return
}
Response:
w.WriteHeader(http.StatusOK)
}
// hapiForgotPasswordSubmit asks the server to send the user a "Forgot
// Password" email with instructions for resetting their password.
func hapiForgotPasswordSubmit(w http.ResponseWriter, r *http.Request) {
var req struct{ Email string }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorInternal(w, err)
return
}
var user dt.User
q := `SELECT id, name, email FROM users WHERE email=$1`
err := db.Get(&user, q, req.Email)
if err == sql.ErrNoRows {
writeError(w, errors.New("Sorry, there's no record of that email. Are you sure that's the email you used to sign up with and that you typed it correctly?"))
return
}
if err != nil {
writeError(w, err)
return
}
secret := RandSeq(40)
q = `INSERT INTO passwordresets (userid, secret) VALUES ($1, $2)`
if _, err = db.Exec(q, user.ID, secret); err != nil {
writeError(w, err)
return
}
if len(emailsender.Drivers()) == 0 {
writeError(w, errors.New("Sorry, this feature is not enabled. To be enabled, an email driver must be imported."))
return
}
w.WriteHeader(http.StatusOK)
}
// hapiResetPasswordSubmit is arrived at through the email generated by
// hapiForgotPasswordSubmit. This endpoint resets the user password with
// another bcrypt hash after validating on the server that their new password is
// sufficient.
func hapiResetPasswordSubmit(w http.ResponseWriter, r *http.Request) {
var req struct {
Password string
Secret string
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorInternal(w, err)
return
}
if len(req.Password) < 8 {
writeError(w, errors.New("Your password must be at least 8 characters"))
return
}
var uid uint64
q := `SELECT userid FROM passwordresets
WHERE secret=$1 AND
createdat >= CURRENT_TIMESTAMP - interval '30 minutes'`
err := db.Get(&uid, q, req.Secret)
if err == sql.ErrNoRows {
writeError(w, errors.New("Sorry, that information doesn't match our records."))
return
}
if err != nil {
writeError(w, err)
return
}
hpw, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
if err != nil {
writeError(w, err)
return
}
tx, err := db.Begin()
if err != nil {
writeError(w, err)
return
}
q = `UPDATE users SET password=$1 WHERE id=$2`
if _, err = tx.Exec(q, hpw, uid); err != nil {
writeError(w, err)
return
}
q = `DELETE FROM passwordresets WHERE secret=$1`
if _, err = tx.Exec(q, req.Secret); err != nil {
writeError(w, err)
return
}
if err = tx.Commit(); err != nil {
writeError(w, err)
return
}
w.WriteHeader(http.StatusOK)
}
// hapiAdminExists checks if an admin exists in the database.
func hapiAdminExists(w http.ResponseWriter, r *http.Request) {
var count int
q := `SELECT COUNT(*) FROM users WHERE admin=TRUE LIMIT 1`
if err := db.Get(&count, q); err != nil {
writeErrorInternal(w, err)
return
}
byt, err := json.Marshal(count > 0)
if err != nil {
writeErrorInternal(w, err)
return
}
_, err = w.Write(byt)
if err != nil {
log.Info("failed writing response header.", err)
}
}
// hapiPlugins responds with all of the server's installed plugin
// configurations from each their respective plugin.json files and
// database-stored configuration.
func hapiPlugins(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isAdmin(w, r) {
return
}
if !isLoggedIn(w, r) {
return
}
}
var settings []struct {
Name string
Value string
PluginName string
}
q := `SELECT name, value, pluginname FROM settings`
if err := db.Select(&settings, q); err != nil {
writeErrorInternal(w, err)
return
}
type respT struct {
ID uint64
Name string
Icon string
Maintainer string
Settings map[string]string
}
var resp []respT
for _, plugin := range pluginsGo {
data := respT{
ID: plugin.ID,
Name: plugin.Name,
Icon: plugin.Icon,
Maintainer: plugin.Maintainer,
Settings: map[string]string{},
}
for k, v := range plugin.Settings {
data.Settings[k] = v.Default
}
for _, setting := range settings {
if setting.PluginName != plugin.Name {
continue
}
data.Settings[setting.Name] = setting.Value
}
resp = append(resp, data)
}
writeBytes(w, resp)
}
// hapiConversationsNeedTraining returns a list of all sentences that require a
// human response.
func hapiConversationsNeedTraining(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isAdmin(w, r) {
return
}
if !isLoggedIn(w, r) {
return
}
}
msgs := []struct {
Sentence string
FlexID *string
CreatedAt time.Time
UserID uint64
FlexIDType *int
}{}
q := `SELECT * FROM (
SELECT DISTINCT ON (flexid)
userid, flexid, flexidtype, sentence, createdat
FROM messages
WHERE needstraining=TRUE AND trained=FALSE AND abotsent=FALSE AND sentence<>''
) t ORDER BY createdat DESC`
err := db.Select(&msgs, q)
if err == sql.ErrNoRows {
w.WriteHeader(http.StatusOK)
}
if err != nil {
writeErrorInternal(w, err)
return
}
byt, err := json.Marshal(msgs)
if err != nil {
writeErrorInternal(w, err)
return
}
_, err = w.Write(byt)
if err != nil {
log.Info("failed to write response.", err)
}
}
// hapiConversation returns a conversation for a specific user or flexID.
func hapiConversation(w http.ResponseWriter, r *http.Request,
ps httprouter.Params) {
if os.Getenv("ABOT_ENV") != "test" {
if !isAdmin(w, r) {
return
}
if !isLoggedIn(w, r) {
return
}
}
var msgs []struct {
Sentence string
AbotSent bool
CreatedAt time.Time
}
var name, location string
var signedUp time.Time
uid := ps.ByName("uid")
fid := ps.ByName("fid")
fidT := ps.ByName("fidt")
offset := ps.ByName("off")
if uid != "0" {
q := `WITH t AS (
SELECT sentence, abotsent, createdat FROM messages
WHERE userid=$1
ORDER BY createdat DESC LIMIT 30 OFFSET $2
) SELECT * FROM t ORDER BY createdat ASC`
if err := db.Select(&msgs, q, uid, offset); err != nil {
writeErrorInternal(w, err)
return
}
q = `SELECT createdat FROM users WHERE id=$1`
if err := db.Get(&signedUp, q, uid); err != nil {
writeErrorInternal(w, err)
return
}
var val []byte
q = `SELECT value FROM states WHERE userid=$1 AND key=$2`
err := db.Get(&val, q, uid, prefs.Name)
if err != sql.ErrNoRows {
writeErrorInternal(w, err)
if err = json.Unmarshal(val, &name); err != nil {
return
}
}
err = db.Get(&val, q, uid, prefs.Location)
if err != sql.ErrNoRows {
writeErrorInternal(w, err)
if err = json.Unmarshal(val, &location); err != nil {
return
}
}
} else {
q := `WITH t AS (
SELECT sentence, abotsent, createdat FROM messages
WHERE flexid=$1 AND flexidtype=$2
ORDER BY createdat DESC LIMIT 30 OFFSET $3
) SELECT * FROM t ORDER BY createdat ASC`
if err := db.Select(&msgs, q, fid, fidT, offset); err != nil {
writeErrorInternal(w, err)
return
}
q = `SELECT createdat FROM messages
WHERE flexid=$1 AND flexidtype=$2 ORDER BY createdat ASC`
if err := db.Get(&signedUp, q, fid, fidT); err != nil {
writeErrorInternal(w, err)
return
}
var val []byte
q = `SELECT value FROM states
WHERE flexid=$1 AND flexidtype=$2 AND key=$3`
err := db.Get(&val, q, fid, fidT, prefs.Name)
if err != sql.ErrNoRows {
writeErrorInternal(w, err)
if err = json.Unmarshal(val, &name); err != nil {
return
}
}
err = db.Get(&val, q, fid, fidT, prefs.Location)
if err != nil && err != sql.ErrNoRows {
writeErrorInternal(w, err)
if err = json.Unmarshal(val, &location); err != nil {
return
}
}
}
resp := struct {
Name string
CreatedAt time.Time
Location string
Messages []struct {
Sentence string
AbotSent bool
CreatedAt time.Time
}
}{
Name: name,
CreatedAt: signedUp,
Location: location,
Messages: msgs,
}
byt, err := json.Marshal(resp)
if err != nil {
writeErrorInternal(w, err)
return
}
_, err = w.Write(byt)
if err != nil {
log.Info("failed to write response.", err)
}
}
func hapiConversationsUpdate(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isAdmin(w, r) {
return
}
if !isLoggedIn(w, r) {
return
}
if !isValidCSRF(w, r) {
return
}
}
var req struct {
MessageID uint64
UserID uint64
FlexID string
FlexIDType dt.FlexIDType
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorInternal(w, err)
return
}
q := `UPDATE messages SET trained=TRUE WHERE userid=$1 AND id>=$2`
_, err := db.Exec(q, req.UserID, req.MessageID)
if err != nil {
writeErrorInternal(w, err)
return
}
w.WriteHeader(http.StatusOK)
}
// hapiSendMessage enables an admin to send a message to a user on behalf of
// Abot from the Response Panel.
func hapiSendMessage(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isAdmin(w, r) {
return
}
if !isLoggedIn(w, r) {
return
}
if !isValidCSRF(w, r) {
return
}
}
var req struct {
UserID uint64
FlexID string
FlexIDType dt.FlexIDType
Name string
Sentence string
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorBadRequest(w, err)
return
}
msg := &dt.Msg{
User: &dt.User{ID: req.UserID},
FlexID: req.FlexID,
FlexIDType: req.FlexIDType,
Sentence: req.Sentence,
AbotSent: true,
}
switch req.FlexIDType {
case dt.FIDTPhone:
if smsConn == nil {
writeErrorInternal(w, errors.New("No SMS driver installed."))
return
}
if err := smsConn.Send(msg.FlexID, msg.Sentence); err != nil {
writeErrorInternal(w, err)
return
}
case dt.FIDTEmail:
/*
// TODO
if emailConn == nil {
writeErrorInternal(w, errors.New("No email driver installed."))
return
}
adminEmail := os.Getenv("ABOT_EMAIL")
email := template.GenericEmail(req.Name)
err := emailConn.SendHTML(msg.FlexID, adminEmail, "SUBJ", email)
if err != nil {
writeErrorInternal(w, err)
return
}
*/
case dt.FIDTSession:
/*
// TODO
if err := ws.NotifySocketSession(); err != nil {
}
*/
default:
writeErrorInternal(w, errors.New("invalid flexidtype"))
return
}
if err := msg.Save(db); err != nil {
writeErrorInternal(w, err)
return
}
w.WriteHeader(http.StatusOK)
}
// hapiAdmins returns a list of all admins with the training and manage team
// permissions.
func hapiAdmins(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isAdmin(w, r) {
return
}
if !isLoggedIn(w, r) {
return
}
}
var admins []struct {
ID uint64
Name string
Email string
}
q := `SELECT id, name, email FROM users WHERE admin=TRUE`
err := db.Select(&admins, q)
if err != nil && err != sql.ErrNoRows {
writeErrorInternal(w, err)
return
}
b, err := json.Marshal(admins)
if err != nil {
writeErrorInternal(w, err)
return
}
_, err = w.Write(b)
if err != nil {
log.Info("failed to write response.", err)
}
}
// hapiAdminsUpdate adds or removes admin permission from a given user.
func hapiAdminsUpdate(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isAdmin(w, r) {
return
}
if !isLoggedIn(w, r) {
return
}
if !isValidCSRF(w, r) {
return
}
}
var req struct {
ID uint64
Email string
Admin bool
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorBadRequest(w, err)
return
}
// This is a clever way to update the user using EITHER email or ID
// (whatever the client had available). Then we return the ID of the
// updated entry to send back to the client for faster future requests.
if req.ID > 0 && len(req.Email) > 0 {
writeErrorBadRequest(w, errors.New("only one value allowed: ID or Email"))
return
}
q := `UPDATE users SET admin=$1 WHERE id=$2 OR email=$3 RETURNING id`
err := db.QueryRow(q, req.Admin, req.ID, req.Email).Scan(&req.ID)
if err == sql.ErrNoRows {
// This error is frequently user-facing.
writeErrorBadRequest(w, errors.New("User not found."))
return
}
if err != nil {
writeErrorInternal(w, err)
return
}
var user struct {
ID uint64
Email string
Name string
}
q = `SELECT id, email, name FROM users WHERE id=$1`
if err = db.Get(&user, q, req.ID); err != nil {
writeErrorInternal(w, err)
return
}
byt, err := json.Marshal(user)
if err != nil {
writeErrorInternal(w, err)
return
}
_, err = w.Write(byt)
if err != nil {
log.Info("failed to write response.", err)
}
}
// hapiRemoteTokens returns the final six bytes of each auth token used to
// authenticate to the remote service and when.
func hapiRemoteTokens(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isAdmin(w, r) {
return
}
if !isLoggedIn(w, r) {
return
}
}
// We initialize the variable here because we want empty slices to
// marshal to [], not null
auths := []struct {
Token string
Email string
CreatedAt time.Time
PluginIDs dt.Uint64Slice
}{}
q := `SELECT token, email, pluginids, createdat FROM remotetokens`
err := db.Select(&auths, q)
if err != nil && err != sql.ErrNoRows {
writeErrorInternal(w, err)
return
}
byt, err := json.Marshal(auths)
if err != nil {
writeErrorInternal(w, err)
return
}
_, err = w.Write(byt)
if err != nil {
log.Info("failed to write response.", err)
}
}
// hapiRemoteTokensSubmit adds a remote token for modifying ITSABOT_URL's
// plugin training data.
func hapiRemoteTokensSubmit(w http.ResponseWriter, r *http.Request) {
if os.Getenv("ABOT_ENV") != "test" {
if !isAdmin(w, r) {
return
}
if !isLoggedIn(w, r) {
return
}
if !isValidCSRF(w, r) {
return
}
}
var req struct {
Token string
PluginIDs dt.Uint64Slice
}