diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 855666b..8b36c13 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,16 +4,15 @@
-
-
+
+
+
-
+
-
-
-
-
-
+
+
+
@@ -151,7 +150,6 @@
-
@@ -176,7 +174,8 @@
-
+
+
true
diff --git a/pkg/internal/models/statuses.go b/pkg/internal/models/statuses.go
index d9c88da..a662d4b 100644
--- a/pkg/internal/models/statuses.go
+++ b/pkg/internal/models/statuses.go
@@ -1,5 +1,7 @@
package models
+import "time"
+
type StatusAttitude = uint8
const (
@@ -16,5 +18,6 @@ type Status struct {
Attitude StatusAttitude `json:"attitude"`
IsNoDisturb bool `json:"is_no_disturb"`
IsInvisible bool `json:"is_invisible"`
+ ClearAt *time.Time `json:"clear_at"`
AccountID uint `json:"account_id"`
}
diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go
index 223fd80..d1be88b 100644
--- a/pkg/internal/server/api/index.go
+++ b/pkg/internal/server/api/index.go
@@ -35,6 +35,8 @@ func MapAPIs(app *fiber.App) {
me.Post("/confirm", doRegisterConfirm)
+ me.Post("/status", setStatus)
+
friends := me.Group("/friends").Name("Friends")
{
friends.Get("/", listFriendship)
@@ -49,6 +51,7 @@ func MapAPIs(app *fiber.App) {
directory := api.Group("/users/:alias").Name("User Directory")
{
directory.Get("/", getOtherUserinfo)
+ directory.Get("/status", getStatus)
}
api.Post("/users", doRegister)
diff --git a/pkg/internal/server/api/statuses_api.go b/pkg/internal/server/api/statuses_api.go
new file mode 100644
index 0000000..b4293e4
--- /dev/null
+++ b/pkg/internal/server/api/statuses_api.go
@@ -0,0 +1,66 @@
+package api
+
+import (
+ "fmt"
+ "git.solsynth.dev/hydrogen/passport/pkg/internal/models"
+ "git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
+ "git.solsynth.dev/hydrogen/passport/pkg/internal/services"
+ "github.com/gofiber/fiber/v2"
+ "github.com/samber/lo"
+ "time"
+)
+
+func getStatus(c *fiber.Ctx) error {
+ alias := c.Params("alias")
+
+ user, err := services.GetAccountWithName(alias)
+ if err != nil {
+ return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("account not found: %s", alias))
+ }
+
+ status, err := services.GetStatus(user.ID)
+ disturbable := services.GetStatusDisturbable(user.ID) == nil
+ online := services.GetStatusOnline(user.ID) == nil
+
+ return c.JSON(fiber.Map{
+ "status": lo.Ternary(err == nil, &status, nil),
+ "is_disturbable": disturbable,
+ "is_online": online,
+ })
+}
+
+func setStatus(c *fiber.Ctx) error {
+ user := c.Locals("user").(models.Account)
+ if err := exts.EnsureAuthenticated(c); err != nil {
+ return err
+ }
+
+ var req struct {
+ Type string `json:"type" validate:"required"`
+ Label string `json:"label" validate:"required"`
+ Attitude uint `json:"attitude" validate:"required"`
+ IsNoDisturb bool `json:"is_no_disturb"`
+ IsInvisible bool `json:"is_invisible"`
+ ClearAt *time.Time `json:"clear_at"`
+ }
+
+ if err := exts.BindAndValidate(c, &req); err != nil {
+ return err
+ }
+
+ status := models.Status{
+ Type: req.Type,
+ Label: req.Label,
+ Attitude: models.StatusAttitude(req.Attitude),
+ IsNoDisturb: req.IsNoDisturb,
+ IsInvisible: req.IsInvisible,
+ ClearAt: req.ClearAt,
+ AccountID: user.ID,
+ }
+
+ if status, err := services.NewStatus(user, status); err != nil {
+ return fiber.NewError(fiber.StatusBadRequest, err.Error())
+ } else {
+ return c.JSON(status)
+ }
+}
diff --git a/pkg/internal/services/accounts.go b/pkg/internal/services/accounts.go
index a4b23a0..e49f341 100644
--- a/pkg/internal/services/accounts.go
+++ b/pkg/internal/services/accounts.go
@@ -25,6 +25,17 @@ func GetAccount(id uint) (models.Account, error) {
return account, nil
}
+func GetAccountWithName(alias string) (models.Account, error) {
+ var account models.Account
+ if err := database.C.Where(models.Account{
+ Name: alias,
+ }).First(&account).Error; err != nil {
+ return account, err
+ }
+
+ return account, nil
+}
+
func LookupAccount(probe string) (models.Account, error) {
var account models.Account
if err := database.C.Where(models.Account{Name: probe}).First(&account).Error; err == nil {
diff --git a/pkg/internal/services/factors.go b/pkg/internal/services/factors.go
index 8228dab..d518e7f 100644
--- a/pkg/internal/services/factors.go
+++ b/pkg/internal/services/factors.go
@@ -2,6 +2,7 @@ package services
import (
"fmt"
+ "github.com/rs/zerolog/log"
"github.com/samber/lo"
"strings"
@@ -81,7 +82,8 @@ func GetFactorCode(factor models.AuthFactor) (bool, error) {
subject := fmt.Sprintf("[%s] Login verification code", viper.GetString("name"))
content := fmt.Sprintf(EmailPasswordTemplate, user.Name, factor.Secret, viper.GetString("maintainer"))
if err := SendMail(user.GetPrimaryEmail().Content, subject, content); err != nil {
- return true, err
+ log.Warn().Err(err).Uint("factor", factor.ID).Msg("Failed to delivery one-time-password via mail...")
+ return true, nil
}
return true, nil
diff --git a/pkg/internal/services/notifications.go b/pkg/internal/services/notifications.go
index f9db8ae..bea847d 100644
--- a/pkg/internal/services/notifications.go
+++ b/pkg/internal/services/notifications.go
@@ -65,7 +65,10 @@ func PushNotification(notification models.Notification) error {
}.Marshal())
}
- // TODO Detect the push notification is turned off (still push when IsForcePush is on)
+ // Skip push notify
+ if GetStatusDisturbable(notification.RecipientID) != nil {
+ return nil
+ }
var subscribers []models.NotificationSubscriber
if err := database.C.Where(&models.NotificationSubscriber{
diff --git a/pkg/internal/services/statuses.go b/pkg/internal/services/statuses.go
new file mode 100644
index 0000000..409895b
--- /dev/null
+++ b/pkg/internal/services/statuses.go
@@ -0,0 +1,59 @@
+package services
+
+import (
+ "fmt"
+ "git.solsynth.dev/hydrogen/passport/pkg/internal/database"
+ "git.solsynth.dev/hydrogen/passport/pkg/internal/models"
+ "time"
+)
+
+var statusCache = make(map[uint]models.Status)
+
+func NewStatus(user models.Account, status models.Status) (models.Status, error) {
+ if err := database.C.Save(&status).Error; err != nil {
+ return status, err
+ } else {
+ statusCache[user.ID] = status
+ }
+ return status, nil
+}
+
+func GetStatus(uid uint) (models.Status, error) {
+ if status, ok := statusCache[uid]; ok {
+ return status, nil
+ }
+ var status models.Status
+ if err := database.C.
+ Where("account_id = ?", uid).
+ Where("clear_at < ?", time.Now()).
+ First(&status).Error; err != nil {
+ return status, err
+ } else {
+ statusCache[uid] = status
+ }
+ return status, nil
+}
+
+func GetStatusDisturbable(uid uint) error {
+ status, err := GetStatus(uid)
+ isOnline := wsConn[uid] == nil || len(wsConn[uid]) < 0
+ if isOnline && err != nil {
+ return nil
+ } else if err == nil && status.IsNoDisturb {
+ return fmt.Errorf("do not disturb")
+ } else {
+ return fmt.Errorf("offline")
+ }
+}
+
+func GetStatusOnline(uid uint) error {
+ status, err := GetStatus(uid)
+ isOnline := wsConn[uid] == nil || len(wsConn[uid]) < 0
+ if isOnline && err != nil {
+ return nil
+ } else if err == nil && status.IsInvisible {
+ return fmt.Errorf("invisible")
+ } else {
+ return fmt.Errorf("offline")
+ }
+}