Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,84 @@ anytype space join <invite-link>
anytype space leave <space-id>
```

### Chat Operations

Chat support enables programmatic interaction with Anytype's messaging feature. This unlocks powerful automation possibilities: build chat bots, create notification integrations, archive conversations, or bridge Anytype chats with other platforms—all through the command line.

#### Getting Started

First, discover chat objects in your space:

```bash
# Get your space ID
anytype space list

# Find all chat objects in the space
anytype chat find <space-id>
```

This displays a table of chat objects with their Chat IDs, names, and Object IDs. Use the Chat ID for subsequent commands.

#### Sending Messages

```bash
# Send a simple message
anytype chat send <chat-id> "Hello from the CLI!"

# Reply to a specific message
anytype chat send <chat-id> "Thanks for the info" --reply-to <message-id>
```

#### Reading Messages

```bash
# List recent messages (default: 20)
anytype chat list <chat-id>

# Get more messages
anytype chat list <chat-id> -n 50

# Show newest first
anytype chat list <chat-id> --reverse

# Pagination: get messages before/after a specific point
anytype chat list <chat-id> --before <order-id>
anytype chat list <chat-id> --after <order-id>
```

#### Managing Messages

```bash
# Edit a message you sent
anytype chat edit <chat-id> <message-id> "Updated text"

# Delete a message
anytype chat delete <chat-id> <message-id>

# Add or remove a reaction
anytype chat react <chat-id> <message-id> "👍"

# Mark messages as read
anytype chat read <chat-id>
```

#### Example: Simple Chat Bot

```bash
#!/bin/bash
CHAT_ID="your-chat-id"

# Monitor and respond (basic polling example)
while true; do
# Get latest messages
anytype chat list $CHAT_ID -n 5 --reverse

# Your bot logic here...

sleep 10
done
```

## Development

### Project Structure
Expand Down
31 changes: 31 additions & 0 deletions cmd/chat/chat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package chat

import (
"github.com/spf13/cobra"

"github.com/anyproto/anytype-cli/cmd/chat/delete"
"github.com/anyproto/anytype-cli/cmd/chat/edit"
"github.com/anyproto/anytype-cli/cmd/chat/find"
"github.com/anyproto/anytype-cli/cmd/chat/list"
"github.com/anyproto/anytype-cli/cmd/chat/react"
"github.com/anyproto/anytype-cli/cmd/chat/read"
"github.com/anyproto/anytype-cli/cmd/chat/send"
)

func NewChatCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "chat",
Short: "Chat operations",
Long: "Send, receive, and manage chat messages in Anytype spaces",
}

cmd.AddCommand(find.NewFindCmd())
cmd.AddCommand(send.NewSendCmd())
cmd.AddCommand(list.NewListCmd())
cmd.AddCommand(edit.NewEditCmd())
cmd.AddCommand(delete.NewDeleteCmd())
cmd.AddCommand(react.NewReactCmd())
cmd.AddCommand(read.NewReadCmd())

return cmd
}
31 changes: 31 additions & 0 deletions cmd/chat/delete/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package delete

import (
"github.com/spf13/cobra"

"github.com/anyproto/anytype-cli/core"
"github.com/anyproto/anytype-cli/core/output"
)

func NewDeleteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <chat-id> <message-id>",
Short: "Delete a message",
Long: "Delete a message from a chat",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
chatId := args[0]
messageId := args[1]

err := core.DeleteChatMessage(chatId, messageId)
if err != nil {
return output.Error("Failed to delete message: %w", err)
}

output.Info("Message deleted successfully")
return nil
},
}

return cmd
}
32 changes: 32 additions & 0 deletions cmd/chat/edit/edit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package edit

import (
"github.com/spf13/cobra"

"github.com/anyproto/anytype-cli/core"
"github.com/anyproto/anytype-cli/core/output"
)

func NewEditCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "edit <chat-id> <message-id> <new-text>",
Short: "Edit a message",
Long: "Edit the content of an existing chat message",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
chatId := args[0]
messageId := args[1]
newText := args[2]

err := core.EditChatMessage(chatId, messageId, newText)
if err != nil {
return output.Error("Failed to edit message: %w", err)
}

output.Info("Message edited successfully")
return nil
},
}

return cmd
}
49 changes: 49 additions & 0 deletions cmd/chat/find/find.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package find

import (
"github.com/spf13/cobra"

"github.com/anyproto/anytype-cli/core"
"github.com/anyproto/anytype-cli/core/output"
)

func NewFindCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "find <space-id>",
Short: "Find chat objects in a space",
Long: "Search for objects with chat functionality in a space and display their chat IDs",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
spaceId := args[0]

chats, err := core.FindChats(spaceId)
if err != nil {
return output.Error("Failed to find chats: %w", err)
}

if len(chats) == 0 {
output.Info("No chat objects found in this space")
return nil
}

output.Info("%-40s %-20s %s", "CHAT ID", "NAME", "OBJECT ID")
output.Info("%-40s %-20s %s", "───────", "────", "─────────")

for _, chat := range chats {
name := chat.Name
if len(name) > 18 {
name = name[:15] + "..."
}
chatId := chat.ChatID
if chatId == "" {
chatId = "(no chatId set)"
}
output.Info("%-40s %-20s %s", chatId, name, chat.ObjectID)
}

return nil
},
}

return cmd
}
81 changes: 81 additions & 0 deletions cmd/chat/list/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package list

import (
"fmt"

"github.com/spf13/cobra"

"github.com/anyproto/anytype-cli/core"
"github.com/anyproto/anytype-cli/core/output"
)

func NewListCmd() *cobra.Command {
var (
limit int32
before string
after string
reverse bool
)

cmd := &cobra.Command{
Use: "list <chat-id>",
Short: "List messages in a chat",
Long: "Retrieve and display messages from an Anytype chat object",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
chatId := args[0]

messages, err := core.GetChatMessages(chatId, limit, before, after)
if err != nil {
return output.Error("Failed to get messages: %w", err)
}

if len(messages) == 0 {
output.Info("No messages found")
return nil
}

// Print in chronological order (oldest first) unless reversed
start, end, step := 0, len(messages), 1
if reverse {
start, end, step = len(messages)-1, -1, -1
}

for i := start; i != end; i += step {
msg := messages[i]
timestamp := msg.CreatedAt.Format("2006-01-02 15:04:05")
readMark := " "
if !msg.Read {
readMark = "●"
}

output.Info("%s [%s] %s:", readMark, timestamp, msg.Creator)
output.Info(" %s", msg.Text)

if len(msg.Reactions) > 0 {
reactionStr := " Reactions:"
for emoji, users := range msg.Reactions {
reactionStr += fmt.Sprintf(" %s(%d)", emoji, len(users))
}
output.Info(reactionStr)
}

if msg.ReplyTo != "" {
output.Info(" ↳ Reply to: %s", msg.ReplyTo)
}

output.Info(" ID: %s", msg.ID)
output.Info("")
}

return nil
},
}

cmd.Flags().Int32VarP(&limit, "limit", "n", 20, "Maximum number of messages to retrieve")
cmd.Flags().StringVar(&before, "before", "", "Get messages before this order ID")
cmd.Flags().StringVar(&after, "after", "", "Get messages after this order ID")
cmd.Flags().BoolVarP(&reverse, "reverse", "r", false, "Show newest messages first")

return cmd
}
36 changes: 36 additions & 0 deletions cmd/chat/react/react.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package react

import (
"github.com/spf13/cobra"

"github.com/anyproto/anytype-cli/core"
"github.com/anyproto/anytype-cli/core/output"
)

func NewReactCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "react <chat-id> <message-id> <emoji>",
Short: "React to a message",
Long: "Add or remove a reaction (emoji) from a message. Running twice toggles the reaction off.",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
chatId := args[0]
messageId := args[1]
emoji := args[2]

added, err := core.ToggleChatReaction(chatId, messageId, emoji)
if err != nil {
return output.Error("Failed to toggle reaction: %w", err)
}

if added {
output.Info("Reaction %s added", emoji)
} else {
output.Info("Reaction %s removed", emoji)
}
return nil
},
}

return cmd
}
38 changes: 38 additions & 0 deletions cmd/chat/read/read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package read

import (
"github.com/spf13/cobra"

"github.com/anyproto/anytype-cli/core"
"github.com/anyproto/anytype-cli/core/output"
)

func NewReadCmd() *cobra.Command {
var (
afterOrderId string
beforeOrderId string
)

cmd := &cobra.Command{
Use: "read <chat-id>",
Short: "Mark chat messages as read",
Long: "Mark messages in a chat as read within an optional range",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
chatId := args[0]

err := core.MarkChatMessagesRead(chatId, afterOrderId, beforeOrderId)
if err != nil {
return output.Error("Failed to mark messages as read: %w", err)
}

output.Info("Messages marked as read")
return nil
},
}

cmd.Flags().StringVar(&afterOrderId, "after", "", "Mark messages after this order ID")
cmd.Flags().StringVar(&beforeOrderId, "before", "", "Mark messages before this order ID")

return cmd
}
Loading