gitlink-cli/shortcuts/notification/notification.go

240 lines
6.2 KiB
Go

package notification
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
var messageTypes = map[string]string{
"notification": "notification",
"atme": "atme",
}
var listStatuses = map[string]string{
"unread": "1",
"read": "2",
}
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
tr := i18n.Default()
if len(translators) > 0 && translators[0] != nil {
tr = translators[0]
}
return []*common.Shortcut{
{
Name: "list",
Description: tr.T("cmd.notification.list.short"),
Flags: []common.Flag{
{Name: "user", Short: "u", Usage: tr.T("flag.notification.user")},
{Name: "type", Short: "t", Usage: tr.T("flag.notification.type_all"), Default: "all"},
{Name: "status", Short: "s", Usage: tr.T("flag.notification.status"), Default: "all"},
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
},
Run: runList,
},
{
Name: "read",
Description: tr.T("cmd.notification.read.short"),
Flags: []common.Flag{
{Name: "user", Short: "u", Usage: tr.T("flag.notification.user")},
{Name: "type", Short: "t", Usage: tr.T("flag.notification.type"), Required: true},
{Name: "ids", Short: "i", Usage: tr.T("flag.notification.ids_read"), Required: true},
},
Run: runRead,
},
{
Name: "delete",
Description: tr.T("cmd.notification.delete.short"),
Flags: []common.Flag{
{Name: "user", Short: "u", Usage: tr.T("flag.notification.user")},
{Name: "type", Short: "t", Usage: tr.T("flag.notification.type"), Required: true},
{Name: "ids", Short: "i", Usage: tr.T("flag.notification.ids"), Required: true},
},
Run: runDelete,
},
}
}
func runList(ctx *common.RuntimeContext) error {
user, err := resolveUserLogin(ctx)
if err != nil {
return err
}
query, err := listQuery(ctx)
if err != nil {
return err
}
env, err := ctx.CallAPIWithQuery("GET", messagesPath(user), query)
if err != nil {
return err
}
return ctx.Output(env)
}
func runRead(ctx *common.RuntimeContext) error {
user, payload, err := messagePayload(ctx, true)
if err != nil {
return err
}
env, err := ctx.CallAPI("POST", messagesPath(user)+"/read", payload)
if err != nil {
return err
}
return ctx.Output(env)
}
func runDelete(ctx *common.RuntimeContext) error {
user, payload, err := messagePayload(ctx, false)
if err != nil {
return err
}
env, err := ctx.CallAPI("DELETE", messagesPath(user), payload)
if err != nil {
return err
}
return ctx.Output(env)
}
func messagesPath(user string) string {
return fmt.Sprintf("/users/%s/messages", url.PathEscape(user))
}
func listQuery(ctx *common.RuntimeContext) (url.Values, error) {
page, err := positiveInt(defaultString(ctx.Arg("page"), "1"), "page")
if err != nil {
return nil, err
}
limit, err := positiveInt(defaultString(ctx.Arg("limit"), "20"), "limit")
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("page", strconv.Itoa(page))
query.Set("limit", strconv.Itoa(limit))
if typ, err := normalizeOptionalType(ctx.Arg("type")); err != nil {
return nil, err
} else if typ != "" {
query.Set("type", typ)
}
if status, err := normalizeStatus(ctx.Arg("status")); err != nil {
return nil, err
} else if status != "" {
query.Set("status", status)
}
return query, nil
}
func messagePayload(ctx *common.RuntimeContext, allowAllUnread bool) (string, map[string]interface{}, error) {
user, err := resolveUserLogin(ctx)
if err != nil {
return "", nil, err
}
typ, err := normalizeRequiredType(ctx.Arg("type"))
if err != nil {
return "", nil, err
}
ids, err := parseIDs(ctx.Arg("ids"), allowAllUnread)
if err != nil {
return "", nil, err
}
return user, map[string]interface{}{
"type": typ,
"ids": ids,
}, nil
}
func resolveUserLogin(ctx *common.RuntimeContext) (string, error) {
if user := strings.TrimSpace(ctx.Arg("user")); user != "" {
return user, nil
}
env, err := ctx.CallAPI("GET", "/users/me", nil)
if err != nil {
return "", fmt.Errorf("resolve current user: %w", err)
}
data, ok := env.Data.(map[string]interface{})
if !ok {
return "", fmt.Errorf("resolve current user: unexpected response")
}
login, _ := data["login"].(string)
if strings.TrimSpace(login) == "" {
return "", fmt.Errorf("resolve current user: login is missing")
}
return strings.TrimSpace(login), nil
}
func normalizeOptionalType(value string) (string, error) {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" || value == "all" {
return "", nil
}
return normalizeRequiredType(value)
}
func normalizeRequiredType(value string) (string, error) {
value = strings.ToLower(strings.TrimSpace(value))
if typ, ok := messageTypes[value]; ok {
return typ, nil
}
return "", fmt.Errorf("invalid --type %q: use notification or atme", value)
}
func normalizeStatus(value string) (string, error) {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" || value == "all" {
return "", nil
}
if status, ok := listStatuses[value]; ok {
return status, nil
}
return "", fmt.Errorf("invalid --status %q: use unread, read, or all", value)
}
func parseIDs(value string, allowAllUnread bool) ([]int, error) {
parts := strings.Split(value, ",")
ids := make([]int, 0, len(parts))
seen := map[int]bool{}
for _, part := range parts {
raw := strings.TrimSpace(part)
if raw == "" {
continue
}
id, err := strconv.Atoi(raw)
if err != nil || id == 0 || id < -1 {
return nil, fmt.Errorf("invalid --ids value %q: use positive integer IDs", raw)
}
if id == -1 && !allowAllUnread {
return nil, fmt.Errorf("invalid --ids value -1: delete requires explicit message IDs")
}
if seen[id] {
continue
}
seen[id] = true
ids = append(ids, id)
}
if len(ids) == 0 {
return nil, fmt.Errorf("required flag --ids is empty")
}
return ids, nil
}
func positiveInt(value, name string) (int, error) {
parsed, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil || parsed <= 0 {
return 0, fmt.Errorf("invalid --%s %q: use a positive integer", name, value)
}
return parsed, nil
}
func defaultString(value, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return value
}