machinaos 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.template +71 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/cli.js +159 -0
- package/client/.dockerignore +45 -0
- package/client/Dockerfile +68 -0
- package/client/eslint.config.js +29 -0
- package/client/index.html +13 -0
- package/client/nginx.conf +66 -0
- package/client/package.json +48 -0
- package/client/src/App.tsx +27 -0
- package/client/src/Dashboard.tsx +1173 -0
- package/client/src/ParameterPanel.tsx +301 -0
- package/client/src/components/AIAgentNode.tsx +321 -0
- package/client/src/components/APIKeyValidator.tsx +118 -0
- package/client/src/components/ClaudeChatModelNode.tsx +18 -0
- package/client/src/components/ConditionalEdge.tsx +189 -0
- package/client/src/components/CredentialsModal.tsx +306 -0
- package/client/src/components/EdgeConditionEditor.tsx +443 -0
- package/client/src/components/GeminiChatModelNode.tsx +18 -0
- package/client/src/components/GenericNode.tsx +357 -0
- package/client/src/components/LocationParameterPanel.tsx +154 -0
- package/client/src/components/ModelNode.tsx +286 -0
- package/client/src/components/OpenAIChatModelNode.tsx +18 -0
- package/client/src/components/OutputPanel.tsx +471 -0
- package/client/src/components/ParameterRenderer.tsx +1874 -0
- package/client/src/components/SkillEditorModal.tsx +417 -0
- package/client/src/components/SquareNode.tsx +797 -0
- package/client/src/components/StartNode.tsx +250 -0
- package/client/src/components/ToolkitNode.tsx +365 -0
- package/client/src/components/TriggerNode.tsx +463 -0
- package/client/src/components/auth/LoginPage.tsx +247 -0
- package/client/src/components/auth/ProtectedRoute.tsx +59 -0
- package/client/src/components/base/BaseChatModelNode.tsx +271 -0
- package/client/src/components/icons/AIProviderIcons.tsx +50 -0
- package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
- package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
- package/client/src/components/maps/index.ts +26 -0
- package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
- package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
- package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
- package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
- package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
- package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
- package/client/src/components/parameterPanel/index.ts +42 -0
- package/client/src/components/shared/DataPanel.tsx +142 -0
- package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
- package/client/src/components/ui/AIResultModal.tsx +204 -0
- package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
- package/client/src/components/ui/CodeEditor.tsx +81 -0
- package/client/src/components/ui/CollapsibleSection.tsx +88 -0
- package/client/src/components/ui/ComponentItem.tsx +154 -0
- package/client/src/components/ui/ComponentPalette.tsx +321 -0
- package/client/src/components/ui/ConsolePanel.tsx +1074 -0
- package/client/src/components/ui/ErrorBoundary.tsx +196 -0
- package/client/src/components/ui/InputNodesPanel.tsx +204 -0
- package/client/src/components/ui/MapSelector.tsx +314 -0
- package/client/src/components/ui/Modal.tsx +149 -0
- package/client/src/components/ui/NodeContextMenu.tsx +192 -0
- package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
- package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
- package/client/src/components/ui/SettingsPanel.tsx +243 -0
- package/client/src/components/ui/TopToolbar.tsx +736 -0
- package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
- package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
- package/client/src/config/antdTheme.ts +186 -0
- package/client/src/config/api.ts +54 -0
- package/client/src/contexts/AuthContext.tsx +221 -0
- package/client/src/contexts/ThemeContext.tsx +42 -0
- package/client/src/contexts/WebSocketContext.tsx +1971 -0
- package/client/src/factories/baseChatModelFactory.ts +256 -0
- package/client/src/hooks/useAndroidOperations.ts +164 -0
- package/client/src/hooks/useApiKeyValidation.ts +107 -0
- package/client/src/hooks/useApiKeys.ts +238 -0
- package/client/src/hooks/useAppTheme.ts +17 -0
- package/client/src/hooks/useComponentPalette.ts +51 -0
- package/client/src/hooks/useCopyPaste.ts +155 -0
- package/client/src/hooks/useDragAndDrop.ts +124 -0
- package/client/src/hooks/useDragVariable.ts +88 -0
- package/client/src/hooks/useExecution.ts +313 -0
- package/client/src/hooks/useParameterPanel.ts +176 -0
- package/client/src/hooks/useReactFlowNodes.ts +189 -0
- package/client/src/hooks/useToolSchema.ts +209 -0
- package/client/src/hooks/useWhatsApp.ts +196 -0
- package/client/src/hooks/useWorkflowManagement.ts +46 -0
- package/client/src/index.css +315 -0
- package/client/src/main.tsx +19 -0
- package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
- package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
- package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
- package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
- package/client/src/nodeDefinitions/chatNodes.ts +135 -0
- package/client/src/nodeDefinitions/codeNodes.ts +54 -0
- package/client/src/nodeDefinitions/documentNodes.ts +379 -0
- package/client/src/nodeDefinitions/index.ts +15 -0
- package/client/src/nodeDefinitions/locationNodes.ts +463 -0
- package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
- package/client/src/nodeDefinitions/skillNodes.ts +211 -0
- package/client/src/nodeDefinitions/toolNodes.ts +198 -0
- package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
- package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
- package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
- package/client/src/nodeDefinitions.ts +104 -0
- package/client/src/schemas/workflowSchema.ts +264 -0
- package/client/src/services/dynamicParameterService.ts +96 -0
- package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
- package/client/src/services/executionService.ts +232 -0
- package/client/src/services/workflowApi.ts +91 -0
- package/client/src/store/useAppStore.ts +582 -0
- package/client/src/styles/theme.ts +508 -0
- package/client/src/styles/zIndex.ts +17 -0
- package/client/src/types/ComponentTypes.ts +39 -0
- package/client/src/types/EdgeCondition.ts +231 -0
- package/client/src/types/INodeProperties.ts +288 -0
- package/client/src/types/NodeTypes.ts +28 -0
- package/client/src/utils/formatters.ts +33 -0
- package/client/src/utils/googleMapsLoader.ts +140 -0
- package/client/src/utils/locationUtils.ts +85 -0
- package/client/src/utils/nodeUtils.ts +31 -0
- package/client/src/utils/workflow.ts +30 -0
- package/client/src/utils/workflowExport.ts +120 -0
- package/client/src/vite-env.d.ts +12 -0
- package/client/tailwind.config.js +60 -0
- package/client/tsconfig.json +25 -0
- package/client/tsconfig.node.json +11 -0
- package/client/vite.config.js +35 -0
- package/docker-compose.prod.yml +107 -0
- package/docker-compose.yml +104 -0
- package/docs-MachinaOs/README.md +85 -0
- package/docs-MachinaOs/deployment/docker.mdx +228 -0
- package/docs-MachinaOs/deployment/production.mdx +345 -0
- package/docs-MachinaOs/docs.json +75 -0
- package/docs-MachinaOs/faq.mdx +309 -0
- package/docs-MachinaOs/favicon.svg +5 -0
- package/docs-MachinaOs/installation.mdx +160 -0
- package/docs-MachinaOs/introduction.mdx +114 -0
- package/docs-MachinaOs/logo/dark.svg +6 -0
- package/docs-MachinaOs/logo/light.svg +6 -0
- package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
- package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
- package/docs-MachinaOs/nodes/android.mdx +411 -0
- package/docs-MachinaOs/nodes/overview.mdx +181 -0
- package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
- package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
- package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
- package/docs-MachinaOs/quickstart.mdx +119 -0
- package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
- package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
- package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
- package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
- package/nul +0 -0
- package/package.json +70 -0
- package/scripts/build.js +158 -0
- package/scripts/check-ports.ps1 +33 -0
- package/scripts/clean.js +40 -0
- package/scripts/docker.js +93 -0
- package/scripts/kill-port.ps1 +154 -0
- package/scripts/start.js +210 -0
- package/scripts/stop.js +325 -0
- package/server/.dockerignore +44 -0
- package/server/Dockerfile +45 -0
- package/server/constants.py +249 -0
- package/server/core/__init__.py +1 -0
- package/server/core/cache.py +461 -0
- package/server/core/config.py +128 -0
- package/server/core/container.py +99 -0
- package/server/core/database.py +1211 -0
- package/server/core/logging.py +314 -0
- package/server/main.py +289 -0
- package/server/middleware/__init__.py +5 -0
- package/server/middleware/auth.py +89 -0
- package/server/models/__init__.py +1 -0
- package/server/models/auth.py +52 -0
- package/server/models/cache.py +24 -0
- package/server/models/database.py +211 -0
- package/server/models/nodes.py +455 -0
- package/server/package.json +9 -0
- package/server/pyproject.toml +72 -0
- package/server/requirements.txt +83 -0
- package/server/routers/__init__.py +1 -0
- package/server/routers/android.py +294 -0
- package/server/routers/auth.py +203 -0
- package/server/routers/database.py +151 -0
- package/server/routers/maps.py +142 -0
- package/server/routers/nodejs_compat.py +289 -0
- package/server/routers/webhook.py +90 -0
- package/server/routers/websocket.py +2127 -0
- package/server/routers/whatsapp.py +761 -0
- package/server/routers/workflow.py +200 -0
- package/server/services/__init__.py +1 -0
- package/server/services/ai.py +2415 -0
- package/server/services/android/__init__.py +27 -0
- package/server/services/android/broadcaster.py +114 -0
- package/server/services/android/client.py +608 -0
- package/server/services/android/manager.py +78 -0
- package/server/services/android/protocol.py +165 -0
- package/server/services/android_service.py +588 -0
- package/server/services/auth.py +131 -0
- package/server/services/chat_client.py +160 -0
- package/server/services/deployment/__init__.py +12 -0
- package/server/services/deployment/manager.py +706 -0
- package/server/services/deployment/state.py +47 -0
- package/server/services/deployment/triggers.py +275 -0
- package/server/services/event_waiter.py +785 -0
- package/server/services/execution/__init__.py +77 -0
- package/server/services/execution/cache.py +769 -0
- package/server/services/execution/conditions.py +373 -0
- package/server/services/execution/dlq.py +132 -0
- package/server/services/execution/executor.py +1351 -0
- package/server/services/execution/models.py +531 -0
- package/server/services/execution/recovery.py +235 -0
- package/server/services/handlers/__init__.py +126 -0
- package/server/services/handlers/ai.py +355 -0
- package/server/services/handlers/android.py +260 -0
- package/server/services/handlers/code.py +278 -0
- package/server/services/handlers/document.py +598 -0
- package/server/services/handlers/http.py +193 -0
- package/server/services/handlers/polyglot.py +105 -0
- package/server/services/handlers/tools.py +845 -0
- package/server/services/handlers/triggers.py +107 -0
- package/server/services/handlers/utility.py +822 -0
- package/server/services/handlers/whatsapp.py +476 -0
- package/server/services/maps.py +289 -0
- package/server/services/memory_store.py +103 -0
- package/server/services/node_executor.py +375 -0
- package/server/services/parameter_resolver.py +218 -0
- package/server/services/polyglot_client.py +169 -0
- package/server/services/scheduler.py +155 -0
- package/server/services/skill_loader.py +417 -0
- package/server/services/status_broadcaster.py +826 -0
- package/server/services/temporal/__init__.py +23 -0
- package/server/services/temporal/activities.py +344 -0
- package/server/services/temporal/client.py +76 -0
- package/server/services/temporal/executor.py +147 -0
- package/server/services/temporal/worker.py +251 -0
- package/server/services/temporal/workflow.py +355 -0
- package/server/services/temporal/ws_client.py +236 -0
- package/server/services/text.py +111 -0
- package/server/services/user_auth.py +172 -0
- package/server/services/websocket_client.py +29 -0
- package/server/services/workflow.py +597 -0
- package/server/skills/android-skill/SKILL.md +82 -0
- package/server/skills/assistant-personality/SKILL.md +45 -0
- package/server/skills/code-skill/SKILL.md +140 -0
- package/server/skills/http-skill/SKILL.md +161 -0
- package/server/skills/maps-skill/SKILL.md +170 -0
- package/server/skills/memory-skill/SKILL.md +154 -0
- package/server/skills/scheduler-skill/SKILL.md +84 -0
- package/server/skills/whatsapp-skill/SKILL.md +283 -0
- package/server/uv.lock +2916 -0
- package/server/whatsapp-rpc/.dockerignore +30 -0
- package/server/whatsapp-rpc/Dockerfile +44 -0
- package/server/whatsapp-rpc/Dockerfile.web +17 -0
- package/server/whatsapp-rpc/README.md +139 -0
- package/server/whatsapp-rpc/cli.js +95 -0
- package/server/whatsapp-rpc/configs/config.yaml +7 -0
- package/server/whatsapp-rpc/docker-compose.yml +35 -0
- package/server/whatsapp-rpc/docs/API.md +410 -0
- package/server/whatsapp-rpc/go.mod +67 -0
- package/server/whatsapp-rpc/go.sum +203 -0
- package/server/whatsapp-rpc/package.json +30 -0
- package/server/whatsapp-rpc/schema.json +1294 -0
- package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
- package/server/whatsapp-rpc/scripts/cli.js +162 -0
- package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
- package/server/whatsapp-rpc/src/go/config/config.go +49 -0
- package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
- package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
- package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
- package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
- package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
- package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
- package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
- package/server/whatsapp-rpc/web/app.py +609 -0
- package/server/whatsapp-rpc/web/requirements.txt +6 -0
- package/server/whatsapp-rpc/web/rpc_client.py +427 -0
- package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
- package/server/whatsapp-rpc/web/templates/base.html +150 -0
- package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
- package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
- package/server/whatsapp-rpc/web/templates/groups.html +328 -0
- package/server/whatsapp-rpc/web/templates/messages.html +465 -0
- package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
- package/server/whatsapp-rpc/web/templates/send.html +259 -0
- package/server/whatsapp-rpc/web/templates/settings.html +459 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package whatsapp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"database/sql"
|
|
5
|
+
"time"
|
|
6
|
+
|
|
7
|
+
"github.com/sirupsen/logrus"
|
|
8
|
+
_ "modernc.org/sqlite"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// HistoryStore handles message persistence for chat history
|
|
12
|
+
type HistoryStore struct {
|
|
13
|
+
db *sql.DB
|
|
14
|
+
logger *logrus.Logger
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// MessageRecord represents a stored message
|
|
18
|
+
type MessageRecord struct {
|
|
19
|
+
MessageID string `json:"message_id"`
|
|
20
|
+
ChatID string `json:"chat_id"`
|
|
21
|
+
Sender string `json:"sender"`
|
|
22
|
+
SenderPhone string `json:"sender_phone"`
|
|
23
|
+
MessageType string `json:"message_type"`
|
|
24
|
+
Text string `json:"text"`
|
|
25
|
+
Timestamp time.Time `json:"timestamp"`
|
|
26
|
+
IsGroup bool `json:"is_group"`
|
|
27
|
+
IsFromMe bool `json:"is_from_me"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ChatHistoryResult is the response for chat_history queries
|
|
31
|
+
type ChatHistoryResult struct {
|
|
32
|
+
Messages []MessageRecord `json:"messages"`
|
|
33
|
+
Total int `json:"total"`
|
|
34
|
+
HasMore bool `json:"has_more"`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// NewHistoryStore creates a new history store with SQLite persistence
|
|
38
|
+
func NewHistoryStore(dbPath string, logger *logrus.Logger) (*HistoryStore, error) {
|
|
39
|
+
// Use separate file for history to avoid conflicts with whatsmeow's session store
|
|
40
|
+
historyPath := dbPath + "_history"
|
|
41
|
+
db, err := sql.Open("sqlite", "file:"+historyPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
|
42
|
+
if err != nil {
|
|
43
|
+
return nil, err
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
store := &HistoryStore{db: db, logger: logger}
|
|
47
|
+
if err := store.initTable(); err != nil {
|
|
48
|
+
return nil, err
|
|
49
|
+
}
|
|
50
|
+
return store, nil
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func (h *HistoryStore) initTable() error {
|
|
54
|
+
_, err := h.db.Exec(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS message_history (
|
|
56
|
+
message_id TEXT PRIMARY KEY,
|
|
57
|
+
chat_id TEXT NOT NULL,
|
|
58
|
+
sender TEXT NOT NULL,
|
|
59
|
+
sender_phone TEXT,
|
|
60
|
+
message_type TEXT NOT NULL,
|
|
61
|
+
text TEXT,
|
|
62
|
+
timestamp INTEGER NOT NULL,
|
|
63
|
+
is_group INTEGER NOT NULL DEFAULT 0,
|
|
64
|
+
is_from_me INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
|
66
|
+
);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_chat_id ON message_history(chat_id);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_timestamp ON message_history(timestamp DESC);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_sender_phone ON message_history(sender_phone);
|
|
70
|
+
`)
|
|
71
|
+
return err
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// StoreMessage persists a message to the history store
|
|
75
|
+
func (h *HistoryStore) StoreMessage(msg MessageRecord) error {
|
|
76
|
+
_, err := h.db.Exec(`
|
|
77
|
+
INSERT OR IGNORE INTO message_history
|
|
78
|
+
(message_id, chat_id, sender, sender_phone, message_type, text, timestamp, is_group, is_from_me)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
80
|
+
`, msg.MessageID, msg.ChatID, msg.Sender, msg.SenderPhone, msg.MessageType,
|
|
81
|
+
msg.Text, msg.Timestamp.Unix(), boolToInt(msg.IsGroup), boolToInt(msg.IsFromMe))
|
|
82
|
+
return err
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// GetChatHistory retrieves messages for a chat with optional filters
|
|
86
|
+
func (h *HistoryStore) GetChatHistory(chatID string, limit, offset int, senderPhone string, textOnly bool) (*ChatHistoryResult, error) {
|
|
87
|
+
// Build query with filters
|
|
88
|
+
query := `SELECT message_id, chat_id, sender, sender_phone, message_type, text, timestamp, is_group, is_from_me
|
|
89
|
+
FROM message_history WHERE chat_id = ?`
|
|
90
|
+
args := []interface{}{chatID}
|
|
91
|
+
|
|
92
|
+
if senderPhone != "" {
|
|
93
|
+
query += " AND sender_phone = ?"
|
|
94
|
+
args = append(args, senderPhone)
|
|
95
|
+
}
|
|
96
|
+
if textOnly {
|
|
97
|
+
query += " AND message_type = 'text'"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Count total matching
|
|
101
|
+
countQuery := "SELECT COUNT(*) FROM message_history WHERE chat_id = ?"
|
|
102
|
+
countArgs := []interface{}{chatID}
|
|
103
|
+
if senderPhone != "" {
|
|
104
|
+
countQuery += " AND sender_phone = ?"
|
|
105
|
+
countArgs = append(countArgs, senderPhone)
|
|
106
|
+
}
|
|
107
|
+
if textOnly {
|
|
108
|
+
countQuery += " AND message_type = 'text'"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
var total int
|
|
112
|
+
h.db.QueryRow(countQuery, countArgs...).Scan(&total)
|
|
113
|
+
|
|
114
|
+
// Get messages ordered by timestamp descending (newest first)
|
|
115
|
+
query += " ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
|
116
|
+
args = append(args, limit, offset)
|
|
117
|
+
|
|
118
|
+
rows, err := h.db.Query(query, args...)
|
|
119
|
+
if err != nil {
|
|
120
|
+
return nil, err
|
|
121
|
+
}
|
|
122
|
+
defer rows.Close()
|
|
123
|
+
|
|
124
|
+
messages := []MessageRecord{}
|
|
125
|
+
for rows.Next() {
|
|
126
|
+
var msg MessageRecord
|
|
127
|
+
var ts int64
|
|
128
|
+
var isGroup, isFromMe int
|
|
129
|
+
err := rows.Scan(&msg.MessageID, &msg.ChatID, &msg.Sender, &msg.SenderPhone,
|
|
130
|
+
&msg.MessageType, &msg.Text, &ts, &isGroup, &isFromMe)
|
|
131
|
+
if err != nil {
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
msg.Timestamp = time.Unix(ts, 0)
|
|
135
|
+
msg.IsGroup = isGroup == 1
|
|
136
|
+
msg.IsFromMe = isFromMe == 1
|
|
137
|
+
messages = append(messages, msg)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return &ChatHistoryResult{
|
|
141
|
+
Messages: messages,
|
|
142
|
+
Total: total,
|
|
143
|
+
HasMore: offset+len(messages) < total,
|
|
144
|
+
}, nil
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Close closes the database connection
|
|
148
|
+
func (h *HistoryStore) Close() error {
|
|
149
|
+
if h.db != nil {
|
|
150
|
+
return h.db.Close()
|
|
151
|
+
}
|
|
152
|
+
return nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ClearHistory deletes all messages from the history store
|
|
156
|
+
func (h *HistoryStore) ClearHistory() error {
|
|
157
|
+
_, err := h.db.Exec("DELETE FROM message_history")
|
|
158
|
+
return err
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func boolToInt(b bool) int {
|
|
162
|
+
if b {
|
|
163
|
+
return 1
|
|
164
|
+
}
|
|
165
|
+
return 0
|
|
166
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
package whatsapp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/base64"
|
|
6
|
+
"fmt"
|
|
7
|
+
"strings"
|
|
8
|
+
"time"
|
|
9
|
+
|
|
10
|
+
"go.mau.fi/whatsmeow"
|
|
11
|
+
"go.mau.fi/whatsmeow/proto/waE2E"
|
|
12
|
+
"go.mau.fi/whatsmeow/types"
|
|
13
|
+
"google.golang.org/protobuf/proto"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// SendMessage - Legacy simple message sending (kept for backward compatibility)
|
|
17
|
+
func (s *Service) SendMessage(phone, message string) error {
|
|
18
|
+
req := &MessageRequest{
|
|
19
|
+
Phone: phone,
|
|
20
|
+
Message: message,
|
|
21
|
+
Type: "text",
|
|
22
|
+
}
|
|
23
|
+
return s.SendEnhancedMessage(req)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// SendEnhancedMessage - Comprehensive message sending with all WhatsApp features
|
|
27
|
+
func (s *Service) SendEnhancedMessage(req *MessageRequest) error {
|
|
28
|
+
if !s.client.IsConnected() {
|
|
29
|
+
return fmt.Errorf("WhatsApp not connected")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var targetJID types.JID
|
|
33
|
+
var err error
|
|
34
|
+
|
|
35
|
+
// Log received data for debugging
|
|
36
|
+
s.logger.Infof("SendEnhancedMessage: Type=%s, Phone='%s', GroupID='%s'", req.Type, req.Phone, req.GroupID)
|
|
37
|
+
|
|
38
|
+
// Determine target JID (individual or group)
|
|
39
|
+
if req.GroupID != "" {
|
|
40
|
+
targetJID, err = types.ParseJID(req.GroupID)
|
|
41
|
+
if err != nil {
|
|
42
|
+
return fmt.Errorf("invalid group ID '%s': %w", req.GroupID, err)
|
|
43
|
+
}
|
|
44
|
+
} else if req.Phone != "" {
|
|
45
|
+
// Validate phone number is not empty after trimming
|
|
46
|
+
if len(req.Phone) == 0 {
|
|
47
|
+
return fmt.Errorf("phone number cannot be empty")
|
|
48
|
+
}
|
|
49
|
+
targetJID = types.NewJID(req.Phone, types.DefaultUserServer)
|
|
50
|
+
} else {
|
|
51
|
+
return fmt.Errorf("either phone or group_id is required (received: phone='%s', group_id='%s')", req.Phone, req.GroupID)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Determine recipient for rate limiting (use phone or group ID)
|
|
55
|
+
recipient := req.Phone
|
|
56
|
+
if recipient == "" {
|
|
57
|
+
recipient = req.GroupID
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check rate limits before proceeding
|
|
61
|
+
if err := s.CheckRateLimit(recipient); err != nil {
|
|
62
|
+
s.logger.Warnf("Rate limit check failed: %v", err)
|
|
63
|
+
return err
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Send typing indicator if enabled (simulates human behavior)
|
|
67
|
+
s.SendTypingIfEnabled(targetJID.String())
|
|
68
|
+
|
|
69
|
+
// Check if message contains links for extra delay
|
|
70
|
+
hasLinks := strings.Contains(req.Message, "http://") || strings.Contains(req.Message, "https://")
|
|
71
|
+
|
|
72
|
+
// Apply message delay (with randomization for human-like behavior)
|
|
73
|
+
s.ApplyMessageDelay(hasLinks)
|
|
74
|
+
|
|
75
|
+
// Create message based on type
|
|
76
|
+
var message *waE2E.Message
|
|
77
|
+
switch req.Type {
|
|
78
|
+
case "text":
|
|
79
|
+
message = s.createTextMessage(req)
|
|
80
|
+
case "image":
|
|
81
|
+
message, err = s.createImageMessage(req)
|
|
82
|
+
case "document":
|
|
83
|
+
message, err = s.createDocumentMessage(req)
|
|
84
|
+
case "audio":
|
|
85
|
+
message, err = s.createAudioMessage(req)
|
|
86
|
+
case "video":
|
|
87
|
+
message, err = s.createVideoMessage(req)
|
|
88
|
+
case "location":
|
|
89
|
+
message = s.createLocationMessage(req)
|
|
90
|
+
case "sticker":
|
|
91
|
+
message, err = s.createStickerMessage(req)
|
|
92
|
+
case "contact":
|
|
93
|
+
message = s.createContactMessage(req)
|
|
94
|
+
default:
|
|
95
|
+
return fmt.Errorf("unsupported message type: %s", req.Type)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if err != nil {
|
|
99
|
+
return fmt.Errorf("failed to create %s message: %w", req.Type, err)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add reply context if specified
|
|
103
|
+
if req.Reply != nil {
|
|
104
|
+
msgText := message.GetConversation()
|
|
105
|
+
message.ExtendedTextMessage = &waE2E.ExtendedTextMessage{
|
|
106
|
+
Text: &msgText,
|
|
107
|
+
ContextInfo: &waE2E.ContextInfo{
|
|
108
|
+
StanzaID: proto.String(req.Reply.MessageID),
|
|
109
|
+
Participant: proto.String(req.Reply.Sender),
|
|
110
|
+
QuotedMessage: &waE2E.Message{Conversation: proto.String(req.Reply.Content)},
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
message.Conversation = nil
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Send the message
|
|
117
|
+
resp, err := s.client.SendMessage(context.Background(), targetJID, message)
|
|
118
|
+
if err != nil {
|
|
119
|
+
return fmt.Errorf("failed to send message: %w", err)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Record message sent for rate limiting stats
|
|
123
|
+
s.RecordMessageSent(recipient)
|
|
124
|
+
|
|
125
|
+
// Log successful send
|
|
126
|
+
s.logger.Infof("Message sent successfully: %s to %s", resp.ID, targetJID.String())
|
|
127
|
+
|
|
128
|
+
// Send event notification
|
|
129
|
+
s.safeEventSend(Event{
|
|
130
|
+
Type: "message_sent",
|
|
131
|
+
Data: map[string]interface{}{
|
|
132
|
+
"message_id": resp.ID,
|
|
133
|
+
"to": targetJID.String(),
|
|
134
|
+
"type": req.Type,
|
|
135
|
+
"timestamp": resp.Timestamp,
|
|
136
|
+
},
|
|
137
|
+
Time: time.Now(),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return nil
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// SendTyping sends typing indicator
|
|
144
|
+
// TODO: Fix context API breaking changes
|
|
145
|
+
/*
|
|
146
|
+
func (s *Service) SendTyping(jidStr string, typing bool) error {
|
|
147
|
+
if !s.client.IsConnected() {
|
|
148
|
+
return fmt.Errorf("WhatsApp not connected")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var presence types.Presence
|
|
152
|
+
if typing {
|
|
153
|
+
presence = types.PresenceAvailable
|
|
154
|
+
} else {
|
|
155
|
+
presence = types.PresenceUnavailable
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
sendErr := s.client.SendPresence(context.Background(), presence)
|
|
159
|
+
if sendErr != nil {
|
|
160
|
+
return fmt.Errorf("failed to send typing indicator: %w", sendErr)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return nil
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// MarkMessageAsRead marks a message as read
|
|
167
|
+
func (s *Service) MarkMessageAsRead(messageID, senderJID string) error {
|
|
168
|
+
if !s.client.IsConnected() {
|
|
169
|
+
return fmt.Errorf("WhatsApp not connected")
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
jid, err := types.ParseJID(senderJID)
|
|
173
|
+
if err != nil {
|
|
174
|
+
return fmt.Errorf("invalid sender JID: %w", err)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
err = s.client.MarkRead(context.Background(), []string{messageID}, time.Now(), jid, jid)
|
|
178
|
+
if err != nil {
|
|
179
|
+
return fmt.Errorf("failed to mark message as read: %w", err)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return nil
|
|
183
|
+
}
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
// Helper functions to create different message types
|
|
187
|
+
|
|
188
|
+
func (s *Service) createTextMessage(req *MessageRequest) *waE2E.Message {
|
|
189
|
+
return &waE2E.Message{
|
|
190
|
+
Conversation: proto.String(req.Message),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func (s *Service) createImageMessage(req *MessageRequest) (*waE2E.Message, error) {
|
|
195
|
+
if req.MediaData == nil {
|
|
196
|
+
return nil, fmt.Errorf("media_data is required for image messages")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Decode base64 media data
|
|
200
|
+
mediaBytes, err := base64.StdEncoding.DecodeString(req.MediaData.Data)
|
|
201
|
+
if err != nil {
|
|
202
|
+
return nil, fmt.Errorf("invalid base64 media data: %w", err)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Upload media to WhatsApp servers
|
|
206
|
+
uploaded, err := s.client.Upload(context.Background(), mediaBytes, whatsmeow.MediaImage)
|
|
207
|
+
if err != nil {
|
|
208
|
+
return nil, fmt.Errorf("failed to upload image: %w", err)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create image message
|
|
212
|
+
message := &waE2E.Message{
|
|
213
|
+
ImageMessage: &waE2E.ImageMessage{
|
|
214
|
+
Caption: proto.String(req.MediaData.Caption),
|
|
215
|
+
Mimetype: proto.String(req.MediaData.MimeType),
|
|
216
|
+
URL: proto.String(uploaded.URL),
|
|
217
|
+
DirectPath: proto.String(uploaded.DirectPath),
|
|
218
|
+
MediaKey: uploaded.MediaKey,
|
|
219
|
+
FileEncSHA256: uploaded.FileEncSHA256,
|
|
220
|
+
FileSHA256: uploaded.FileSHA256,
|
|
221
|
+
FileLength: proto.Uint64(uint64(len(mediaBytes))),
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return message, nil
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func (s *Service) createDocumentMessage(req *MessageRequest) (*waE2E.Message, error) {
|
|
229
|
+
if req.MediaData == nil {
|
|
230
|
+
return nil, fmt.Errorf("media_data is required for document messages")
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Decode base64 media data
|
|
234
|
+
mediaBytes, err := base64.StdEncoding.DecodeString(req.MediaData.Data)
|
|
235
|
+
if err != nil {
|
|
236
|
+
return nil, fmt.Errorf("invalid base64 media data: %w", err)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Upload media to WhatsApp servers
|
|
240
|
+
uploaded, err := s.client.Upload(context.Background(), mediaBytes, whatsmeow.MediaDocument)
|
|
241
|
+
if err != nil {
|
|
242
|
+
return nil, fmt.Errorf("failed to upload document: %w", err)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Create document message
|
|
246
|
+
message := &waE2E.Message{
|
|
247
|
+
DocumentMessage: &waE2E.DocumentMessage{
|
|
248
|
+
Title: proto.String(req.MediaData.Filename),
|
|
249
|
+
FileName: proto.String(req.MediaData.Filename),
|
|
250
|
+
Mimetype: proto.String(req.MediaData.MimeType),
|
|
251
|
+
URL: proto.String(uploaded.URL),
|
|
252
|
+
DirectPath: proto.String(uploaded.DirectPath),
|
|
253
|
+
MediaKey: uploaded.MediaKey,
|
|
254
|
+
FileEncSHA256: uploaded.FileEncSHA256,
|
|
255
|
+
FileSHA256: uploaded.FileSHA256,
|
|
256
|
+
FileLength: proto.Uint64(uint64(len(mediaBytes))),
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return message, nil
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func (s *Service) createAudioMessage(req *MessageRequest) (*waE2E.Message, error) {
|
|
264
|
+
if req.MediaData == nil {
|
|
265
|
+
return nil, fmt.Errorf("media_data is required for audio messages")
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Decode base64 media data
|
|
269
|
+
mediaBytes, err := base64.StdEncoding.DecodeString(req.MediaData.Data)
|
|
270
|
+
if err != nil {
|
|
271
|
+
return nil, fmt.Errorf("invalid base64 media data: %w", err)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Upload media to WhatsApp servers
|
|
275
|
+
uploaded, err := s.client.Upload(context.Background(), mediaBytes, whatsmeow.MediaAudio)
|
|
276
|
+
if err != nil {
|
|
277
|
+
return nil, fmt.Errorf("failed to upload audio: %w", err)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Create audio message
|
|
281
|
+
message := &waE2E.Message{
|
|
282
|
+
AudioMessage: &waE2E.AudioMessage{
|
|
283
|
+
Mimetype: proto.String(req.MediaData.MimeType),
|
|
284
|
+
URL: proto.String(uploaded.URL),
|
|
285
|
+
DirectPath: proto.String(uploaded.DirectPath),
|
|
286
|
+
MediaKey: uploaded.MediaKey,
|
|
287
|
+
FileEncSHA256: uploaded.FileEncSHA256,
|
|
288
|
+
FileSHA256: uploaded.FileSHA256,
|
|
289
|
+
FileLength: proto.Uint64(uint64(len(mediaBytes))),
|
|
290
|
+
PTT: proto.Bool(strings.Contains(req.MediaData.MimeType, "ogg")), // Voice note for OGG
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return message, nil
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
func (s *Service) createVideoMessage(req *MessageRequest) (*waE2E.Message, error) {
|
|
298
|
+
if req.MediaData == nil {
|
|
299
|
+
return nil, fmt.Errorf("media_data is required for video messages")
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Decode base64 media data
|
|
303
|
+
mediaBytes, err := base64.StdEncoding.DecodeString(req.MediaData.Data)
|
|
304
|
+
if err != nil {
|
|
305
|
+
return nil, fmt.Errorf("invalid base64 media data: %w", err)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Upload media to WhatsApp servers
|
|
309
|
+
uploaded, err := s.client.Upload(context.Background(), mediaBytes, whatsmeow.MediaVideo)
|
|
310
|
+
if err != nil {
|
|
311
|
+
return nil, fmt.Errorf("failed to upload video: %w", err)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Create video message
|
|
315
|
+
message := &waE2E.Message{
|
|
316
|
+
VideoMessage: &waE2E.VideoMessage{
|
|
317
|
+
Caption: proto.String(req.MediaData.Caption),
|
|
318
|
+
Mimetype: proto.String(req.MediaData.MimeType),
|
|
319
|
+
URL: proto.String(uploaded.URL),
|
|
320
|
+
DirectPath: proto.String(uploaded.DirectPath),
|
|
321
|
+
MediaKey: uploaded.MediaKey,
|
|
322
|
+
FileEncSHA256: uploaded.FileEncSHA256,
|
|
323
|
+
FileSHA256: uploaded.FileSHA256,
|
|
324
|
+
FileLength: proto.Uint64(uint64(len(mediaBytes))),
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return message, nil
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func (s *Service) createLocationMessage(req *MessageRequest) *waE2E.Message {
|
|
332
|
+
if req.Location == nil {
|
|
333
|
+
return &waE2E.Message{Conversation: proto.String("Invalid location data")}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return &waE2E.Message{
|
|
337
|
+
LocationMessage: &waE2E.LocationMessage{
|
|
338
|
+
DegreesLatitude: proto.Float64(req.Location.Latitude),
|
|
339
|
+
DegreesLongitude: proto.Float64(req.Location.Longitude),
|
|
340
|
+
Name: proto.String(req.Location.Name),
|
|
341
|
+
Address: proto.String(req.Location.Address),
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
func (s *Service) createStickerMessage(req *MessageRequest) (*waE2E.Message, error) {
|
|
347
|
+
if req.MediaData == nil {
|
|
348
|
+
return nil, fmt.Errorf("media_data is required for sticker messages")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Decode base64 media data
|
|
352
|
+
mediaBytes, err := base64.StdEncoding.DecodeString(req.MediaData.Data)
|
|
353
|
+
if err != nil {
|
|
354
|
+
return nil, fmt.Errorf("invalid base64 media data: %w", err)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Upload media to WhatsApp servers
|
|
358
|
+
uploaded, err := s.client.Upload(context.Background(), mediaBytes, whatsmeow.MediaImage)
|
|
359
|
+
if err != nil {
|
|
360
|
+
return nil, fmt.Errorf("failed to upload sticker: %w", err)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Create sticker message
|
|
364
|
+
message := &waE2E.Message{
|
|
365
|
+
StickerMessage: &waE2E.StickerMessage{
|
|
366
|
+
Mimetype: proto.String(req.MediaData.MimeType),
|
|
367
|
+
URL: proto.String(uploaded.URL),
|
|
368
|
+
DirectPath: proto.String(uploaded.DirectPath),
|
|
369
|
+
MediaKey: uploaded.MediaKey,
|
|
370
|
+
FileEncSHA256: uploaded.FileEncSHA256,
|
|
371
|
+
FileSHA256: uploaded.FileSHA256,
|
|
372
|
+
FileLength: proto.Uint64(uint64(len(mediaBytes))),
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return message, nil
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
func (s *Service) createContactMessage(req *MessageRequest) *waE2E.Message {
|
|
380
|
+
if req.Contact == nil {
|
|
381
|
+
return &waE2E.Message{Conversation: proto.String("Invalid contact data")}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return &waE2E.Message{
|
|
385
|
+
ContactMessage: &waE2E.ContactMessage{
|
|
386
|
+
DisplayName: proto.String(req.Contact.DisplayName),
|
|
387
|
+
Vcard: proto.String(req.Contact.VCard),
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
}
|