From 365a42e790f277297ff91d30fc522b76e50c03b4 Mon Sep 17 00:00:00 2001 From: Mengz <2567587994@qq.com> Date: Wed, 10 Jun 2026 17:48:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(message):=20=E5=A2=9E=E5=8A=A0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E4=B8=AD=E5=BF=83=E5=BF=AB=E6=8D=B7=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++ doc/changes/message-shortcuts.md | 25 ++ shortcuts/message/message.go | 418 ++++++++++++++++++++++++++++++ shortcuts/message/message_test.go | 291 +++++++++++++++++++++ shortcuts/register.go | 3 + shortcuts/register_test.go | 2 +- 6 files changed, 756 insertions(+), 1 deletion(-) create mode 100644 doc/changes/message-shortcuts.md create mode 100644 shortcuts/message/message.go create mode 100644 shortcuts/message/message_test.go diff --git a/README.md b/README.md index b43dcd1..0cb79bf 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans | 📋 PM | Sprint management, kanban boards, weekly reports | | 🤖 Workflow | AI-powered issue triage, PR review, release notes | +The `message` shortcut group adds inbox automation for listing messages, checking unread counters, marking messages as read, and deleting older notifications. + ## Installation & Quick Start ### Requirements @@ -285,6 +287,22 @@ gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role D gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true ``` +### Message Center Management + +```bash +# List the latest notifications with unread filtering +gitlink-cli message +list --login Mengz --type notification --status unread --limit 10 + +# Show unread counters for notifications and @mentions +gitlink-cli message +stats --login Mengz + +# Preview marking all notifications as read +gitlink-cli message +read --login Mengz --type notification --all --dry-run + +# Delete selected @mention messages +gitlink-cli message +delete --login Mengz --type atme --ids 101,102 +``` + ### Issue Management ```bash diff --git a/doc/changes/message-shortcuts.md b/doc/changes/message-shortcuts.md new file mode 100644 index 0000000..03c9138 --- /dev/null +++ b/doc/changes/message-shortcuts.md @@ -0,0 +1,25 @@ +# Message Center Shortcuts + +## Summary + +This change adds a new `message` shortcut group for personal inbox management in GitLink CLI. +It covers message listing, unread counters, batch mark-as-read, and batch delete workflows without forcing users to drop down to raw API calls. + +## Included Commands + +- `message +list` filters inbox items by message type, read status, page, and limit. +- `message +stats` returns unread counters for notifications and `@me` messages. +- `message +read` marks selected message IDs, or all unread messages of a given type, as read. +- `message +delete` deletes selected message IDs, or all unread messages of a given type. + +## Usability Details + +- `--login` defaults to the authenticated user when omitted. +- `--dry-run` is supported for write operations so users can inspect destructive requests first. +- `content_text` is added to list output to expose HTML-free plain text that is easier to grep, diff, and script. + +## Validation + +- Added shortcut tests for list, stats, read, delete, ID parsing, and content normalization. +- Verified registration by wiring the `message` group into the global shortcut registry. +- Updated `README.md` with feature coverage and usage examples. diff --git a/shortcuts/message/message.go b/shortcuts/message/message.go new file mode 100644 index 0000000..e7589c8 --- /dev/null +++ b/shortcuts/message/message.go @@ -0,0 +1,418 @@ +package message + +import ( + "encoding/json" + "fmt" + "html" + "net/url" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +const ( + messageTypeNotification = "notification" + messageTypeAtme = "atme" + messageTypeAll = "all" + + messageStatusAll = "all" + messageStatusUnread = "unread" + messageStatusRead = "read" +) + +var htmlTagPattern = regexp.MustCompile(`<[^>]+>`) + +type currentUserResponse struct { + Login string `json:"login"` +} + +type listResponse struct { + TotalCount int `json:"total_count"` + Type string `json:"type"` + UnreadNotification int `json:"unread_notification"` + UnreadAtme int `json:"unread_atme"` + Messages []messageRow `json:"messages"` +} + +type messageRow struct { + ID int64 `json:"id"` + Status int `json:"status"` + Content string `json:"content"` + NotificationURL string `json:"notification_url"` + Source string `json:"source"` + CreatedAt string `json:"created_at"` + TimeAgo string `json:"time_ago"` + Type string `json:"type"` + Sender map[string]interface{} `json:"sender"` +} + +type messageOutput struct { + ID int64 `json:"id"` + Type string `json:"type"` + Status int `json:"status"` + Read bool `json:"read"` + Source string `json:"source,omitempty"` + Content string `json:"content"` + ContentText string `json:"content_text"` + NotificationURL string `json:"notification_url,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + TimeAgo string `json:"time_ago,omitempty"` + Sender map[string]interface{} `json:"sender,omitempty"` +} + +// Shortcuts returns message management shortcuts. +func Shortcuts() []*common.Shortcut { + return []*common.Shortcut{ + { + Name: "list", + Description: "List user messages with filters and plain-text content", + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"}, + {Name: "type", Short: "t", Usage: "Message type: notification, atme, or all", Default: messageTypeAll}, + {Name: "status", Usage: "Message status: unread, read, or all", Default: messageStatusAll}, + {Name: "page", Usage: "Page number", Default: "1"}, + {Name: "limit", Usage: "Items per page", Default: "20"}, + }, + Run: runList, + }, + { + Name: "stats", + Description: "Show unread message counters for a user", + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"}, + {Name: "type", Short: "t", Usage: "Message type: notification, atme, or all", Default: messageTypeAll}, + }, + Run: runStats, + }, + { + Name: "read", + Description: "Mark selected messages as read", + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"}, + {Name: "type", Short: "t", Usage: "Message type: notification or atme", Required: true}, + {Name: "ids", Usage: "Comma-separated message IDs"}, + {Name: "all", Usage: "Mark all unread messages of the selected type as read", Bool: true, Default: "false"}, + {Name: "dry-run", Usage: "Preview the request without sending it", Bool: true, Default: "false"}, + }, + Run: runRead, + }, + { + Name: "delete", + Description: "Delete selected messages", + Flags: []common.Flag{ + {Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"}, + {Name: "type", Short: "t", Usage: "Message type: notification or atme", Required: true}, + {Name: "ids", Usage: "Comma-separated message IDs"}, + {Name: "all", Usage: "Delete all unread messages of the selected type", Bool: true, Default: "false"}, + {Name: "dry-run", Usage: "Preview the request without sending it", Bool: true, Default: "false"}, + }, + Run: runDelete, + }, + } +} + +func runList(ctx *common.RuntimeContext) error { + login, err := resolveTargetLogin(ctx) + if err != nil { + return err + } + query, messageType, status, page, limit, err := buildListQuery(ctx.Arg("type"), ctx.Arg("status"), ctx.Arg("page"), ctx.Arg("limit")) + if err != nil { + return err + } + response, err := fetchMessages(ctx, login, query) + if err != nil { + return err + } + + return ctx.OutputData(map[string]interface{}{ + "action": "list_messages", + "login": login, + "type": messageType, + "status": status, + "page": page, + "limit": limit, + "total_count": response.TotalCount, + "unread_notification": response.UnreadNotification, + "unread_atme": response.UnreadAtme, + "messages": normalizeMessages(response.Messages), + }) +} + +func runStats(ctx *common.RuntimeContext) error { + login, err := resolveTargetLogin(ctx) + if err != nil { + return err + } + messageType, err := parseListType(ctx.Arg("type")) + if err != nil { + return err + } + + query := url.Values{} + query.Set("page", "1") + query.Set("limit", "1") + if messageType != messageTypeAll { + query.Set("type", messageType) + } + + response, err := fetchMessages(ctx, login, query) + if err != nil { + return err + } + + return ctx.OutputData(map[string]interface{}{ + "action": "message_stats", + "login": login, + "type": messageType, + "total_count": response.TotalCount, + "unread_notification": response.UnreadNotification, + "unread_atme": response.UnreadAtme, + "unread_total": response.UnreadNotification + response.UnreadAtme, + }) +} + +func runRead(ctx *common.RuntimeContext) error { + return mutateMessages(ctx, "read_messages", httpMutation{ + Method: "POST", + PathSuffix: "/read", + }) +} + +func runDelete(ctx *common.RuntimeContext) error { + return mutateMessages(ctx, "delete_messages", httpMutation{ + Method: "DELETE", + PathSuffix: "", + }) +} + +type httpMutation struct { + Method string + PathSuffix string +} + +func mutateMessages(ctx *common.RuntimeContext, action string, mutation httpMutation) error { + login, err := resolveTargetLogin(ctx) + if err != nil { + return err + } + messageType, err := parseMutationType(ctx.Arg("type")) + if err != nil { + return err + } + ids, mode, err := parseMutationIDs(ctx.Arg("ids"), parseBoolArg(ctx.Arg("all"))) + if err != nil { + return err + } + + result := map[string]interface{}{ + "action": action, + "login": login, + "type": messageType, + "mode": mode, + "ids": ids, + "dry_run": parseBoolArg(ctx.Arg("dry-run")), + "item_count": len(ids), + } + if mode == "all" { + result["item_count"] = "all" + } + + if parseBoolArg(ctx.Arg("dry-run")) { + return ctx.OutputData(result) + } + + payload := map[string]interface{}{ + "type": messageType, + "ids": ids, + } + env, err := ctx.CallAPI(mutation.Method, fmt.Sprintf("/api/users/%s/messages%s", login, mutation.PathSuffix), payload) + if err != nil { + return err + } + result["updated"] = env.Data + return ctx.OutputData(result) +} + +func resolveTargetLogin(ctx *common.RuntimeContext) (string, error) { + if login := strings.TrimSpace(ctx.Arg("login")); login != "" { + return login, nil + } + + env, err := ctx.CallAPI("GET", "/users/me", nil) + if err != nil { + return "", fmt.Errorf("fetch current user: %w", err) + } + var current currentUserResponse + if err := decodeEnvelopeData(env.Data, ¤t); err != nil { + return "", fmt.Errorf("parse current user: %w", err) + } + if strings.TrimSpace(current.Login) == "" { + return "", fmt.Errorf("current user response did not include a login") + } + return current.Login, nil +} + +func buildListQuery(typeValue, statusValue, pageValue, limitValue string) (url.Values, string, string, int, int, error) { + messageType, err := parseListType(typeValue) + if err != nil { + return nil, "", "", 0, 0, err + } + status, statusCode, err := parseListStatus(statusValue) + if err != nil { + return nil, "", "", 0, 0, err + } + page, err := parsePositiveInt(pageValue, "page") + if err != nil { + return nil, "", "", 0, 0, err + } + limit, err := parsePositiveInt(limitValue, "limit") + if err != nil { + return nil, "", "", 0, 0, err + } + + query := url.Values{} + query.Set("page", strconv.Itoa(page)) + query.Set("limit", strconv.Itoa(limit)) + if messageType != messageTypeAll { + query.Set("type", messageType) + } + if statusCode != 0 { + query.Set("status", strconv.Itoa(statusCode)) + } + return query, messageType, status, page, limit, nil +} + +func fetchMessages(ctx *common.RuntimeContext, login string, query url.Values) (*listResponse, error) { + env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/api/users/%s/messages", login), query) + if err != nil { + return nil, fmt.Errorf("fetch messages: %w", err) + } + var response listResponse + if err := decodeEnvelopeData(env.Data, &response); err != nil { + return nil, fmt.Errorf("parse message list: %w", err) + } + return &response, nil +} + +func normalizeMessages(rows []messageRow) []messageOutput { + items := make([]messageOutput, 0, len(rows)) + for _, row := range rows { + items = append(items, messageOutput{ + ID: row.ID, + Type: row.Type, + Status: row.Status, + Read: row.Status == 2, + Source: row.Source, + Content: row.Content, + ContentText: normalizeMessageText(row.Content), + NotificationURL: row.NotificationURL, + CreatedAt: row.CreatedAt, + TimeAgo: row.TimeAgo, + Sender: row.Sender, + }) + } + return items +} + +func normalizeMessageText(value string) string { + value = htmlTagPattern.ReplaceAllString(value, " ") + value = html.UnescapeString(value) + return strings.Join(strings.Fields(value), " ") +} + +func parseListType(value string) (string, error) { + value = strings.ToLower(strings.TrimSpace(value)) + if value == "" { + value = messageTypeAll + } + switch value { + case messageTypeAll, messageTypeNotification, messageTypeAtme: + return value, nil + default: + return "", fmt.Errorf("invalid --type value %q", value) + } +} + +func parseMutationType(value string) (string, error) { + value = strings.ToLower(strings.TrimSpace(value)) + switch value { + case messageTypeNotification, messageTypeAtme: + return value, nil + default: + return "", fmt.Errorf("invalid --type value %q", value) + } +} + +func parseListStatus(value string) (string, int, error) { + value = strings.ToLower(strings.TrimSpace(value)) + if value == "" { + value = messageStatusAll + } + switch value { + case messageStatusAll: + return value, 0, nil + case messageStatusUnread: + return value, 1, nil + case messageStatusRead: + return value, 2, nil + default: + return "", 0, fmt.Errorf("invalid --status value %q", value) + } +} + +func parsePositiveInt(value, name string) (int, error) { + parsed, err := strconv.Atoi(strings.TrimSpace(value)) + if err != nil || parsed <= 0 { + return 0, fmt.Errorf("invalid --%s value %q", name, value) + } + return parsed, nil +} + +func parseMutationIDs(value string, all bool) ([]int64, string, error) { + if all { + if strings.TrimSpace(value) != "" { + return nil, "", fmt.Errorf("--ids cannot be used together with --all") + } + return []int64{-1}, "all", nil + } + + parts := strings.Split(strings.TrimSpace(value), ",") + ids := make([]int64, 0, len(parts)) + seen := map[int64]bool{} + for _, part := range parts { + token := strings.TrimSpace(part) + if token == "" { + continue + } + id, err := strconv.ParseInt(token, 10, 64) + if err != nil || id <= 0 { + return nil, "", fmt.Errorf("invalid --ids value %q", token) + } + if seen[id] { + continue + } + seen[id] = true + ids = append(ids, id) + } + if len(ids) == 0 { + return nil, "", fmt.Errorf("one of --ids or --all is required") + } + sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + return ids, "selected", nil +} + +func parseBoolArg(value string) bool { + return strings.EqualFold(strings.TrimSpace(value), "true") +} + +func decodeEnvelopeData(data interface{}, target interface{}) error { + raw, err := json.Marshal(data) + if err != nil { + return err + } + return json.Unmarshal(raw, target) +} diff --git a/shortcuts/message/message_test.go b/shortcuts/message/message_test.go new file mode 100644 index 0000000..cabba82 --- /dev/null +++ b/shortcuts/message/message_test.go @@ -0,0 +1,291 @@ +package message + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func runMessageShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + shortcut := findMessageShortcut(t, name) + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Format: "json", + Args: args, + } + if ctx.Args == nil { + ctx.Args = map[string]string{} + } + return shortcut.Run(ctx) +} + +func findMessageShortcut(t *testing.T, name string) *common.Shortcut { + t.Helper() + for _, shortcut := range Shortcuts() { + if shortcut.Name == name { + return shortcut + } + } + t.Fatalf("shortcut %q not found", name) + return nil +} + +func writeMessageJSON(t *testing.T, w http.ResponseWriter, value interface{}) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(value); err != nil { + t.Fatalf("encode json: %v", err) + } +} + +func decodeMessageJSON(t *testing.T, r *http.Request) map[string]interface{} { + t.Helper() + defer r.Body.Close() + var value map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&value); err != nil { + t.Fatalf("decode request body: %v", err) + } + return value +} + +func TestMessageListUsesCurrentUser(t *testing.T) { + var sawList bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/users/me.json": + writeMessageJSON(t, w, map[string]interface{}{"login": "alice"}) + case r.Method == http.MethodGet && r.URL.Path == "/api/users/alice/messages.json": + sawList = true + if got := r.URL.Query().Get("type"); got != messageTypeNotification { + t.Fatalf("type query = %q, want %q", got, messageTypeNotification) + } + if got := r.URL.Query().Get("status"); got != "1" { + t.Fatalf("status query = %q, want 1", got) + } + if got := r.URL.Query().Get("page"); got != "2" { + t.Fatalf("page query = %q, want 2", got) + } + if got := r.URL.Query().Get("limit"); got != "5" { + t.Fatalf("limit query = %q, want 5", got) + } + writeMessageJSON(t, w, messageListFixture()) + default: + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String()) + } + })) + defer server.Close() + + err := runMessageShortcut(t, server, "list", map[string]string{ + "type": messageTypeNotification, + "status": messageStatusUnread, + "page": "2", + "limit": "5", + }) + if err != nil { + t.Fatalf("list failed: %v", err) + } + if !sawList { + t.Fatal("expected message list request to be sent") + } +} + +func TestMessageStatsUsesSelectedLogin(t *testing.T) { + var sawStats bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/api/users/alice/messages.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String()) + } + sawStats = true + if got := r.URL.Query().Get("page"); got != "1" { + t.Fatalf("page query = %q, want 1", got) + } + if got := r.URL.Query().Get("limit"); got != "1" { + t.Fatalf("limit query = %q, want 1", got) + } + if got := r.URL.Query().Get("type"); got != messageTypeAtme { + t.Fatalf("type query = %q, want %q", got, messageTypeAtme) + } + writeMessageJSON(t, w, messageListFixture()) + })) + defer server.Close() + + err := runMessageShortcut(t, server, "stats", map[string]string{ + "login": "alice", + "type": messageTypeAtme, + }) + if err != nil { + t.Fatalf("stats failed: %v", err) + } + if !sawStats { + t.Fatal("expected message stats request to be sent") + } +} + +func TestMessageReadDryRunDoesNotWrite(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("dry-run should not write, got %s %s", r.Method, r.URL.String()) + })) + defer server.Close() + + err := runMessageShortcut(t, server, "read", map[string]string{ + "login": "alice", + "type": messageTypeNotification, + "ids": "101,202", + "dry-run": "true", + }) + if err != nil { + t.Fatalf("read dry-run failed: %v", err) + } +} + +func TestMessageReadMarksAllUnread(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/api/users/alice/messages/read.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String()) + } + payload = decodeMessageJSON(t, r) + writeMessageJSON(t, w, map[string]interface{}{"status": 0, "message": "updated"}) + })) + defer server.Close() + + err := runMessageShortcut(t, server, "read", map[string]string{ + "login": "alice", + "type": messageTypeNotification, + "all": "true", + }) + if err != nil { + t.Fatalf("read failed: %v", err) + } + + if payload["type"] != messageTypeNotification { + t.Fatalf("type = %#v, want %q", payload["type"], messageTypeNotification) + } + ids := payload["ids"].([]interface{}) + if len(ids) != 1 || ids[0].(float64) != -1 { + t.Fatalf("ids = %#v, want [-1]", ids) + } +} + +func TestMessageDeletePostsSelectedIDs(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete || r.URL.Path != "/api/users/alice/messages.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String()) + } + payload = decodeMessageJSON(t, r) + writeMessageJSON(t, w, map[string]interface{}{"status": 0, "message": "deleted"}) + })) + defer server.Close() + + err := runMessageShortcut(t, server, "delete", map[string]string{ + "login": "alice", + "type": messageTypeAtme, + "ids": "202,101,202", + }) + if err != nil { + t.Fatalf("delete failed: %v", err) + } + + if payload["type"] != messageTypeAtme { + t.Fatalf("type = %#v, want %q", payload["type"], messageTypeAtme) + } + ids := payload["ids"].([]interface{}) + if len(ids) != 2 || ids[0].(float64) != 101 || ids[1].(float64) != 202 { + t.Fatalf("ids = %#v, want [101 202]", ids) + } +} + +func TestParseMutationIDsRejectsInvalidInput(t *testing.T) { + if _, _, err := parseMutationIDs("", false); err == nil { + t.Fatal("expected error when neither --ids nor --all is provided") + } + if _, _, err := parseMutationIDs("1,abc", false); err == nil { + t.Fatal("expected error for invalid message id") + } + if _, _, err := parseMutationIDs("1", true); err == nil { + t.Fatal("expected error when --ids and --all are used together") + } +} + +func TestNormalizeMessageText(t *testing.T) { + got := normalizeMessageText("someone @you & team") + want := "someone @you & team" + if got != want { + t.Fatalf("normalizeMessageText() = %q, want %q", got, want) + } +} + +func TestNormalizeMessagesIncludesPlainTextContent(t *testing.T) { + got := normalizeMessages([]messageRow{ + { + ID: 101, + Type: messageTypeNotification, + Status: 1, + Content: "merged successfully", + NotificationURL: "https://example.com/pulls/1", + Source: "PullRequestMerged", + CreatedAt: "2026-06-10 12:00:00", + TimeAgo: "1 hour ago", + Sender: map[string]interface{}{ + "login": "alice", + }, + }, + }) + want := []messageOutput{ + { + ID: 101, + Type: messageTypeNotification, + Status: 1, + Read: false, + Source: "PullRequestMerged", + Content: "merged successfully", + ContentText: "merged successfully", + NotificationURL: "https://example.com/pulls/1", + CreatedAt: "2026-06-10 12:00:00", + TimeAgo: "1 hour ago", + Sender: map[string]interface{}{ + "login": "alice", + }, + }, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("normalizeMessages() = %#v, want %#v", got, want) + } +} + +func messageListFixture() map[string]interface{} { + return map[string]interface{}{ + "total_count": 2, + "type": messageTypeNotification, + "unread_notification": 1, + "unread_atme": 3, + "messages": []map[string]interface{}{ + { + "id": 101, + "status": 1, + "content": "your pull request was merged", + "notification_url": "https://example.com/pulls/1", + "source": "PullRequestMerged", + "created_at": "2026-06-10 12:00:00", + "time_ago": "1 hour ago", + "type": messageTypeNotification, + }, + { + "id": 202, + "status": 2, + "content": "someone @you", + "type": messageTypeAtme, + "sender": map[string]interface{}{ + "login": "bob", + }, + }, + }, + } +} diff --git a/shortcuts/register.go b/shortcuts/register.go index 917bf85..691e63b 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -13,6 +13,7 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/label" "github.com/gitlink-org/gitlink-cli/shortcuts/license" "github.com/gitlink-org/gitlink-cli/shortcuts/member" + "github.com/gitlink-org/gitlink-cli/shortcuts/message" "github.com/gitlink-org/gitlink-cli/shortcuts/milestone" "github.com/gitlink-org/gitlink-cli/shortcuts/org" "github.com/gitlink-org/gitlink-cli/shortcuts/pipeline" @@ -37,6 +38,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "label": label.Shortcuts(), "license": license.Shortcuts(), "member": member.Shortcuts(), + "message": message.Shortcuts(), "milestone": milestone.Shortcuts(), "pipeline": pipeline.Shortcuts(), "pr": pr.Shortcuts(tr), @@ -58,6 +60,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "label": "Issue label operations", "license": "License operations", "member": "Repository member operations", + "message": "Message center operations", "milestone": "Milestone operations", "pipeline": "Pipeline operations", "pr": tr.T("cmd.pr.short"), diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index a8c8ce4..43dfa7d 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) { "repo", "issue", "label", "license", "pr", "release", "branch", "org", "user", "search", "ci", "workflow", "compare", "member", "milestone", "pipeline", "webhook", - "health", + "health", "message", } groupSet := map[string]bool{}