web3-agent-kit 0.9.0__tar.gz → 1.0.0__tar.gz
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.
- {web3_agent_kit-0.9.0/web3_agent_kit.egg-info → web3_agent_kit-1.0.0}/PKG-INFO +1 -1
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/pyproject.toml +1 -1
- web3_agent_kit-1.0.0/src/airdrop/executor/__init__.py +92 -0
- web3_agent_kit-1.0.0/src/airdrop/executor/base_executor.py +413 -0
- web3_agent_kit-1.0.0/src/airdrop/executor/captcha_solver.py +634 -0
- web3_agent_kit-1.0.0/src/airdrop/executor/galxe_exec.py +554 -0
- web3_agent_kit-1.0.0/src/airdrop/executor/intract_exec.py +320 -0
- web3_agent_kit-1.0.0/src/airdrop/executor/layer3_exec.py +404 -0
- web3_agent_kit-1.0.0/src/airdrop/executor/plugin_registry.py +318 -0
- web3_agent_kit-1.0.0/src/airdrop/executor/port3_exec.py +269 -0
- web3_agent_kit-1.0.0/src/airdrop/executor/questn.py +344 -0
- web3_agent_kit-1.0.0/src/airdrop/executor/taskon.py +241 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/__init__.py +3 -3
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_api.py +1 -1
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_executor.py +9 -0
- web3_agent_kit-1.0.0/tests/test_platforms.py +1034 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0/web3_agent_kit.egg-info}/PKG-INFO +1 -1
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/web3_agent_kit.egg-info/SOURCES.txt +10 -0
- web3_agent_kit-0.9.0/src/airdrop/executor/__init__.py +0 -33
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/LICENSE +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/README.md +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/setup.cfg +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/setup.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/__init__.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/agent.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/__init__.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/base.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/browser.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/cli.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/gleam_exec.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/social_exec.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/executor/zealy_exec.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/galxe.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/gleam.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/multi_wallet.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/social.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/tracker.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/airdrop/zealy.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/models.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/__init__.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/approval.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/bridge.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/dca.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/gas.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/portfolio.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/swap.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/wallet.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/watcher.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/api/routes/yield_opt.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/approval_manager.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/bridge.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/chain.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/dca_bot.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/defi/__init__.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/gas_optimizer.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/llm.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/multi_wallet.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/plugins/__init__.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/plugins/examples/gas_tracker.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/portfolio.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/sniper.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/utils/__init__.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/wallet.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/wallet_watcher.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/src/yield_optimizer.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_airdrop.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_bridge.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_core.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_dca_bot.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_defi.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_llm.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_multi_wallet.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_new_features.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_plugins.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_portfolio.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_sniper.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/tests/test_yield_optimizer.py +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/web3_agent_kit.egg-info/dependency_links.txt +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/web3_agent_kit.egg-info/requires.txt +0 -0
- {web3_agent_kit-0.9.0 → web3_agent_kit-1.0.0}/web3_agent_kit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "web3-agent-kit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "Open-source framework for building autonomous AI agents that interact with blockchain networks"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Executor layer — browser automation for airdrop task completion.
|
|
2
|
+
|
|
3
|
+
Provides real browser-based automation for Gleam.io, Zealy, and social
|
|
4
|
+
platform tasks using Playwright with anti-detect capabilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .browser import BrowserManager, BrowserConfig
|
|
8
|
+
from .gleam_exec import GleamExecutor, GleamResult, GleamTaskEntry
|
|
9
|
+
from .social_exec import (
|
|
10
|
+
TwitterExecutor,
|
|
11
|
+
DiscordExecutor,
|
|
12
|
+
TelegramExecutor,
|
|
13
|
+
SocialExecutorConfig,
|
|
14
|
+
)
|
|
15
|
+
from .zealy_exec import ZealyExecutor, ZealyResult
|
|
16
|
+
|
|
17
|
+
# New platform executors
|
|
18
|
+
from .base_executor import (
|
|
19
|
+
BasePlatformExecutor,
|
|
20
|
+
ExecutorConfig,
|
|
21
|
+
ExecutorResult,
|
|
22
|
+
PlatformTask,
|
|
23
|
+
TaskDifficulty,
|
|
24
|
+
)
|
|
25
|
+
from .captcha_solver import (
|
|
26
|
+
CaptchaSolver,
|
|
27
|
+
CaptchaConfig,
|
|
28
|
+
CaptchaProvider,
|
|
29
|
+
CaptchaSolvingError,
|
|
30
|
+
)
|
|
31
|
+
from .questn import QuestNExecutor, QuestNTask, QuestNResult
|
|
32
|
+
from .taskon import TaskOnExecutor, TaskOnTask, TaskOnResult
|
|
33
|
+
from .intract_exec import IntractExecutor, IntractTask, IntractResult
|
|
34
|
+
from .port3_exec import Port3Executor, Port3Task, Port3Result
|
|
35
|
+
from .galxe_exec import GalxeExecutor, GalxeTask, GalxeResult
|
|
36
|
+
from .layer3_exec import Layer3Executor, Layer3Task, Layer3Result
|
|
37
|
+
from .plugin_registry import PlatformPluginRegistry
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Browser
|
|
41
|
+
"BrowserManager",
|
|
42
|
+
"BrowserConfig",
|
|
43
|
+
# Gleam
|
|
44
|
+
"GleamExecutor",
|
|
45
|
+
"GleamResult",
|
|
46
|
+
"GleamTaskEntry",
|
|
47
|
+
# Social
|
|
48
|
+
"TwitterExecutor",
|
|
49
|
+
"DiscordExecutor",
|
|
50
|
+
"TelegramExecutor",
|
|
51
|
+
"SocialExecutorConfig",
|
|
52
|
+
# Zealy
|
|
53
|
+
"ZealyExecutor",
|
|
54
|
+
"ZealyResult",
|
|
55
|
+
# Base executor
|
|
56
|
+
"BasePlatformExecutor",
|
|
57
|
+
"ExecutorConfig",
|
|
58
|
+
"ExecutorResult",
|
|
59
|
+
"PlatformTask",
|
|
60
|
+
"TaskDifficulty",
|
|
61
|
+
# CAPTCHA
|
|
62
|
+
"CaptchaSolver",
|
|
63
|
+
"CaptchaConfig",
|
|
64
|
+
"CaptchaProvider",
|
|
65
|
+
"CaptchaSolvingError",
|
|
66
|
+
# QuestN
|
|
67
|
+
"QuestNExecutor",
|
|
68
|
+
"QuestNTask",
|
|
69
|
+
"QuestNResult",
|
|
70
|
+
# TaskOn
|
|
71
|
+
"TaskOnExecutor",
|
|
72
|
+
"TaskOnTask",
|
|
73
|
+
"TaskOnResult",
|
|
74
|
+
# Intract
|
|
75
|
+
"IntractExecutor",
|
|
76
|
+
"IntractTask",
|
|
77
|
+
"IntractResult",
|
|
78
|
+
# Port3
|
|
79
|
+
"Port3Executor",
|
|
80
|
+
"Port3Task",
|
|
81
|
+
"Port3Result",
|
|
82
|
+
# Galxe
|
|
83
|
+
"GalxeExecutor",
|
|
84
|
+
"GalxeTask",
|
|
85
|
+
"GalxeResult",
|
|
86
|
+
# Layer3
|
|
87
|
+
"Layer3Executor",
|
|
88
|
+
"Layer3Task",
|
|
89
|
+
"Layer3Result",
|
|
90
|
+
# Plugin Registry
|
|
91
|
+
"PlatformPluginRegistry",
|
|
92
|
+
]
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Base platform executor — abstract class for all airdrop platform executors.
|
|
2
|
+
|
|
3
|
+
Provides a common interface with built-in rate limiting, retry logic,
|
|
4
|
+
logging, and progress tracking. All platform executors extend this class.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
from ..base import AirdropTask, TaskType, TaskStatus
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TaskDifficulty(Enum):
|
|
24
|
+
"""Difficulty level of a platform task."""
|
|
25
|
+
EASY = "easy"
|
|
26
|
+
MEDIUM = "medium"
|
|
27
|
+
HARD = "hard"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PlatformTask:
|
|
32
|
+
"""A task discovered on a platform."""
|
|
33
|
+
task_id: str
|
|
34
|
+
title: str
|
|
35
|
+
description: str = ""
|
|
36
|
+
task_type: str = "custom"
|
|
37
|
+
url: str = ""
|
|
38
|
+
points: int = 0
|
|
39
|
+
is_completed: bool = False
|
|
40
|
+
is_claimable: bool = False
|
|
41
|
+
difficulty: TaskDifficulty = TaskDifficulty.EASY
|
|
42
|
+
metadata: dict = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
def to_airdrop_task(self, platform: str) -> AirdropTask:
|
|
45
|
+
"""Convert to an AirdropTask."""
|
|
46
|
+
type_map = {
|
|
47
|
+
"twitter_follow": TaskType.SOCIAL_TWITTER_FOLLOW,
|
|
48
|
+
"twitter_retweet": TaskType.SOCIAL_TWITTER_RETWEET,
|
|
49
|
+
"twitter_like": TaskType.SOCIAL_TWITTER_LIKE,
|
|
50
|
+
"twitter_comment": TaskType.SOCIAL_TWITTER_COMMENT,
|
|
51
|
+
"discord_join": TaskType.SOCIAL_DISCORD_JOIN,
|
|
52
|
+
"discord_verify": TaskType.SOCIAL_DISCORD_VERIFY,
|
|
53
|
+
"telegram_join": TaskType.SOCIAL_TELEGRAM_JOIN,
|
|
54
|
+
"youtube_subscribe": TaskType.SOCIAL_YOUTUBE_SUBSCRIBE,
|
|
55
|
+
"github_star": TaskType.SOCIAL_GITHUB_STAR,
|
|
56
|
+
"github_fork": TaskType.SOCIAL_GITHUB_FORK,
|
|
57
|
+
"on_chain_tx": TaskType.ON_CHAIN_TX,
|
|
58
|
+
"on_chain_swap": TaskType.ON_CHAIN_SWAP,
|
|
59
|
+
"on_chain_bridge": TaskType.ON_CHAIN_BRIDGE,
|
|
60
|
+
"on_chain_stake": TaskType.ON_CHAIN_STAKE,
|
|
61
|
+
"quiz": TaskType.QUIZ,
|
|
62
|
+
"visit_url": TaskType.VISIT_URL,
|
|
63
|
+
"wallet_connect": TaskType.WALLET_CONNECT,
|
|
64
|
+
"referral": TaskType.REFERRAL,
|
|
65
|
+
}
|
|
66
|
+
return AirdropTask(
|
|
67
|
+
task_id=f"{platform}_{self.task_id}",
|
|
68
|
+
platform=platform,
|
|
69
|
+
task_type=type_map.get(self.task_type, TaskType.CUSTOM),
|
|
70
|
+
title=self.title,
|
|
71
|
+
description=self.description,
|
|
72
|
+
url=self.url,
|
|
73
|
+
points=self.points,
|
|
74
|
+
status=TaskStatus.COMPLETED if self.is_completed else TaskStatus.PENDING,
|
|
75
|
+
metadata=self.metadata,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class ExecutorResult:
|
|
81
|
+
"""Result of running an executor."""
|
|
82
|
+
platform: str
|
|
83
|
+
url: str
|
|
84
|
+
total_tasks: int = 0
|
|
85
|
+
completed_tasks: int = 0
|
|
86
|
+
failed_tasks: int = 0
|
|
87
|
+
skipped_tasks: int = 0
|
|
88
|
+
points_earned: int = 0
|
|
89
|
+
tasks: list[PlatformTask] = field(default_factory=list)
|
|
90
|
+
errors: list[str] = field(default_factory=list)
|
|
91
|
+
elapsed_seconds: float = 0.0
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def success_rate(self) -> float:
|
|
95
|
+
"""Calculate success rate."""
|
|
96
|
+
if self.total_tasks == 0:
|
|
97
|
+
return 0.0
|
|
98
|
+
return self.completed_tasks / self.total_tasks
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_fully_completed(self) -> bool:
|
|
102
|
+
"""Check if all tasks are completed."""
|
|
103
|
+
return self.completed_tasks == self.total_tasks and self.total_tasks > 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class ExecutorConfig:
|
|
108
|
+
"""Configuration for a platform executor."""
|
|
109
|
+
rate_limit_delay: float = 2.0
|
|
110
|
+
max_retries: int = 3
|
|
111
|
+
retry_delay: float = 5.0
|
|
112
|
+
timeout: int = 30
|
|
113
|
+
proxy: Optional[str] = None
|
|
114
|
+
user_agent: str = (
|
|
115
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
116
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
117
|
+
"Chrome/120.0.0.0 Safari/537.36"
|
|
118
|
+
)
|
|
119
|
+
captcha_api_key: Optional[str] = None
|
|
120
|
+
captcha_provider: str = "anticaptcha"
|
|
121
|
+
verbose: bool = False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class BasePlatformExecutor(ABC):
|
|
125
|
+
"""Abstract base class for all platform executors.
|
|
126
|
+
|
|
127
|
+
Provides common functionality: session management, rate limiting,
|
|
128
|
+
retry logic, logging, and progress tracking.
|
|
129
|
+
|
|
130
|
+
Subclasses must implement:
|
|
131
|
+
- visit(url): Navigate to the platform
|
|
132
|
+
- get_tasks(): Discover available tasks
|
|
133
|
+
- complete_task(task): Execute a single task
|
|
134
|
+
|
|
135
|
+
Optional overrides:
|
|
136
|
+
- complete_all(): Complete all tasks (has default implementation)
|
|
137
|
+
- verify(): Verify task completion
|
|
138
|
+
- get_results(): Return results
|
|
139
|
+
- login(credentials): Authenticate
|
|
140
|
+
|
|
141
|
+
Example::
|
|
142
|
+
|
|
143
|
+
class MyPlatform(BasePlatformExecutor):
|
|
144
|
+
platform_name = "my_platform"
|
|
145
|
+
|
|
146
|
+
def visit(self, url):
|
|
147
|
+
self._url = url
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
def get_tasks(self):
|
|
151
|
+
return [PlatformTask(task_id="1", title="Do something")]
|
|
152
|
+
|
|
153
|
+
def complete_task(self, task):
|
|
154
|
+
return True
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
platform_name: str = "unknown"
|
|
158
|
+
platform_url: str = ""
|
|
159
|
+
supported_task_types: list[str] = []
|
|
160
|
+
|
|
161
|
+
def __init__(self, config: Optional[ExecutorConfig] = None):
|
|
162
|
+
"""Initialize the executor.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
config: Optional executor configuration.
|
|
166
|
+
"""
|
|
167
|
+
self.config = config or ExecutorConfig()
|
|
168
|
+
self.session = requests.Session()
|
|
169
|
+
self.session.headers.update({"User-Agent": self.config.user_agent})
|
|
170
|
+
if self.config.proxy:
|
|
171
|
+
self.session.proxies = {
|
|
172
|
+
"http": self.config.proxy,
|
|
173
|
+
"https": self.config.proxy,
|
|
174
|
+
}
|
|
175
|
+
self._last_request_time: float = 0
|
|
176
|
+
self._current_url: str = ""
|
|
177
|
+
self._tasks: list[PlatformTask] = []
|
|
178
|
+
self._results: ExecutorResult = ExecutorResult(platform=self.platform_name, url="")
|
|
179
|
+
self._progress_callback: Optional[Any] = None
|
|
180
|
+
logger.info(f"Initialized {self.platform_name} executor")
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def visit(self, url: str) -> bool:
|
|
184
|
+
"""Navigate to the platform URL.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
url: The platform or campaign URL.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if navigation succeeded.
|
|
191
|
+
"""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
@abstractmethod
|
|
195
|
+
def get_tasks(self) -> list[PlatformTask]:
|
|
196
|
+
"""Discover available tasks on the platform.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of PlatformTask objects.
|
|
200
|
+
"""
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
@abstractmethod
|
|
204
|
+
def complete_task(self, task: PlatformTask) -> bool:
|
|
205
|
+
"""Execute a single task.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
task: The task to complete.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
True if task was completed successfully.
|
|
212
|
+
"""
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
def complete_all(
|
|
216
|
+
self,
|
|
217
|
+
url: str,
|
|
218
|
+
tracker: Optional[Any] = None,
|
|
219
|
+
) -> ExecutorResult:
|
|
220
|
+
"""Complete all tasks at the given URL.
|
|
221
|
+
|
|
222
|
+
Default implementation: visit → get_tasks → complete each task.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
url: Platform or campaign URL.
|
|
226
|
+
tracker: Optional AirdropTracker for progress tracking.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
ExecutorResult with completion statistics.
|
|
230
|
+
"""
|
|
231
|
+
start_time = time.time()
|
|
232
|
+
self._results = ExecutorResult(platform=self.platform_name, url=url)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# Visit the platform
|
|
236
|
+
if not self.visit(url):
|
|
237
|
+
self._results.errors.append(f"Failed to load {url}")
|
|
238
|
+
return self._results
|
|
239
|
+
|
|
240
|
+
# Get available tasks
|
|
241
|
+
tasks = self.get_tasks()
|
|
242
|
+
self._results.total_tasks = len(tasks)
|
|
243
|
+
self._results.tasks = tasks
|
|
244
|
+
|
|
245
|
+
if not tasks:
|
|
246
|
+
self._results.errors.append("No tasks found")
|
|
247
|
+
return self._results
|
|
248
|
+
|
|
249
|
+
logger.info(f"{self.platform_name}: starting {len(tasks)} tasks")
|
|
250
|
+
|
|
251
|
+
# Complete each task
|
|
252
|
+
for i, task in enumerate(tasks):
|
|
253
|
+
if task.is_completed:
|
|
254
|
+
self._results.skipped_tasks += 1
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
logger.info(
|
|
258
|
+
f"{self.platform_name}: [{i + 1}/{len(tasks)}] {task.title}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Notify progress callback
|
|
262
|
+
if self._progress_callback:
|
|
263
|
+
self._progress_callback(i, len(tasks), task.title)
|
|
264
|
+
|
|
265
|
+
success = self.complete_task(task)
|
|
266
|
+
if success:
|
|
267
|
+
self._results.completed_tasks += 1
|
|
268
|
+
self._results.points_earned += task.points
|
|
269
|
+
task.is_completed = True
|
|
270
|
+
|
|
271
|
+
if tracker:
|
|
272
|
+
airdrop_task = task.to_airdrop_task(self.platform_name)
|
|
273
|
+
tracker.mark_task_completed(airdrop_task)
|
|
274
|
+
else:
|
|
275
|
+
self._results.failed_tasks += 1
|
|
276
|
+
self._results.errors.append(f"Failed: {task.title}")
|
|
277
|
+
|
|
278
|
+
# Rate limiting between tasks
|
|
279
|
+
self._rate_limit()
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
self._results.errors.append(f"Unexpected error: {str(e)}")
|
|
283
|
+
logger.error(f"{self.platform_name}: unexpected error: {e}")
|
|
284
|
+
finally:
|
|
285
|
+
self._results.elapsed_seconds = time.time() - start_time
|
|
286
|
+
logger.info(
|
|
287
|
+
f"{self.platform_name}: finished in {self._results.elapsed_seconds:.1f}s — "
|
|
288
|
+
f"{self._results.completed_tasks}/{self._results.total_tasks} completed"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return self._results
|
|
292
|
+
|
|
293
|
+
def verify(self) -> bool:
|
|
294
|
+
"""Verify completion status. Override in subclasses.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
True if verification succeeded.
|
|
298
|
+
"""
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
def get_results(self) -> ExecutorResult:
|
|
302
|
+
"""Get the current results.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
ExecutorResult object.
|
|
306
|
+
"""
|
|
307
|
+
return self._results
|
|
308
|
+
|
|
309
|
+
def login(self, credentials: dict) -> bool:
|
|
310
|
+
"""Authenticate with the platform. Override if needed.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
credentials: Platform-specific credentials.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
True if login succeeded.
|
|
317
|
+
"""
|
|
318
|
+
logger.info(f"{self.platform_name}: login not required")
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
def set_progress_callback(self, callback: Any) -> None:
|
|
322
|
+
"""Set a progress callback function.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
callback: Function(current, total, task_name) called on progress.
|
|
326
|
+
"""
|
|
327
|
+
self._progress_callback = callback
|
|
328
|
+
|
|
329
|
+
def close(self) -> None:
|
|
330
|
+
"""Clean up resources."""
|
|
331
|
+
self.session.close()
|
|
332
|
+
logger.debug(f"{self.platform_name}: closed")
|
|
333
|
+
|
|
334
|
+
# ─── Helper Methods ──────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
def _rate_limit(self) -> None:
|
|
337
|
+
"""Enforce rate limiting between requests."""
|
|
338
|
+
elapsed = time.time() - self._last_request_time
|
|
339
|
+
if elapsed < self.config.rate_limit_delay:
|
|
340
|
+
sleep_time = self.config.rate_limit_delay - elapsed
|
|
341
|
+
logger.debug(f"Rate limiting: sleeping {sleep_time:.1f}s")
|
|
342
|
+
time.sleep(sleep_time)
|
|
343
|
+
self._last_request_time = time.time()
|
|
344
|
+
|
|
345
|
+
def _request(
|
|
346
|
+
self,
|
|
347
|
+
method: str,
|
|
348
|
+
url: str,
|
|
349
|
+
**kwargs,
|
|
350
|
+
) -> requests.Response:
|
|
351
|
+
"""Make an HTTP request with rate limiting and retry logic.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
method: HTTP method (GET, POST, etc.).
|
|
355
|
+
url: Request URL.
|
|
356
|
+
**kwargs: Passed to requests.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Response object.
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
requests.RequestException: After all retries exhausted.
|
|
363
|
+
"""
|
|
364
|
+
kwargs.setdefault("timeout", self.config.timeout)
|
|
365
|
+
last_error = None
|
|
366
|
+
|
|
367
|
+
for attempt in range(self.config.max_retries):
|
|
368
|
+
self._rate_limit()
|
|
369
|
+
try:
|
|
370
|
+
response = self.session.request(method, url, **kwargs)
|
|
371
|
+
if response.status_code == 429:
|
|
372
|
+
retry_after = int(
|
|
373
|
+
response.headers.get("Retry-After", self.config.retry_delay)
|
|
374
|
+
)
|
|
375
|
+
logger.warning(f"Rate limited, waiting {retry_after}s")
|
|
376
|
+
time.sleep(retry_after)
|
|
377
|
+
continue
|
|
378
|
+
response.raise_for_status()
|
|
379
|
+
return response
|
|
380
|
+
except requests.RequestException as e:
|
|
381
|
+
last_error = e
|
|
382
|
+
logger.warning(f"Request failed (attempt {attempt + 1}): {e}")
|
|
383
|
+
if attempt < self.config.max_retries - 1:
|
|
384
|
+
time.sleep(self.config.retry_delay * (attempt + 1))
|
|
385
|
+
|
|
386
|
+
raise last_error # type: ignore[misc]
|
|
387
|
+
|
|
388
|
+
def _get(self, url: str, **kwargs) -> requests.Response:
|
|
389
|
+
"""Make a GET request."""
|
|
390
|
+
return self._request("GET", url, **kwargs)
|
|
391
|
+
|
|
392
|
+
def _post(self, url: str, **kwargs) -> requests.Response:
|
|
393
|
+
"""Make a POST request."""
|
|
394
|
+
return self._request("POST", url, **kwargs)
|
|
395
|
+
|
|
396
|
+
def _extract_id_from_url(self, url: str) -> str:
|
|
397
|
+
"""Extract an ID from a URL. Override for platform-specific parsing."""
|
|
398
|
+
import re
|
|
399
|
+
# Try to find a UUID or alphanumeric ID in the URL
|
|
400
|
+
match = re.search(r"[0-9a-f]{24}", url)
|
|
401
|
+
if match:
|
|
402
|
+
return match.group(0)
|
|
403
|
+
# Fallback: use last path segment
|
|
404
|
+
from urllib.parse import urlparse
|
|
405
|
+
parsed = urlparse(url)
|
|
406
|
+
path_parts = [p for p in parsed.path.split("/") if p]
|
|
407
|
+
return path_parts[-1] if path_parts else "unknown"
|
|
408
|
+
|
|
409
|
+
def __enter__(self):
|
|
410
|
+
return self
|
|
411
|
+
|
|
412
|
+
def __exit__(self, *args):
|
|
413
|
+
self.close()
|