Jack před 1 rokem
revize
faa1a554b7

binární
.DS_Store


+ 2 - 0
.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 152 - 0
.gitignore

@@ -0,0 +1,152 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/

+ 14 - 0
Dockerfile

@@ -0,0 +1,14 @@
+# 使用官方的 Python 3.11 镜像作为基础镜像
+FROM python:3.11
+
+# 设置工作目录为 /app
+WORKDIR /app
+
+# 将当前目录下的所有文件复制到容器的 /app 目录中
+COPY . /app
+
+# 使用清华源来安装依赖
+RUN pip install --upgrade pip && \
+    pip install -r requirements.txt
+
+CMD ["python3", "main.py"]

+ 1 - 0
INSTALL.bat

@@ -0,0 +1 @@
+pip install -r requirements.txt

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 62 - 0
README.md

@@ -0,0 +1,62 @@
+<h1 align="center">小草挂机脚本(CN)</h1>
+<p align="center">
+  <a href="#"><img src="https://img.shields.io/badge/Python-3.11-fadf6f"> </a>
+  <a href="https://twitter.com/Crypto0xM"> <img src="https://img.shields.io/twitter/url?url=https%3A%2F%2Ftwitter.com%2FCrypto0xM">
+  </a>
+</p>
+
+---
+
+### 打码平台
+[Captcha.run](https://captcha.run/sso?inviter=766e7788-4ff4-47b6-b991-93ac43dbbfae)
+
+[Yes Captcha!](https://yescaptcha.com/i/Sy4ti1)
+
+[NoCaptcha.io](https://www.nocaptcha.io/register?c=W9SAq9)
+
+
+
+---
+
+🔔 [交流社区](https://t.me/CoinMarketData_1): https://t.me/CoinMarketData_1
+
+💰 打赏捐赠:您的支持是我最大的动力
+
+    - EVM 地址: 0x0385dee0258d739cf5edfc3e387d6804d6884d1e
+    - SOL 地址: F4SZCw7UQxsYNrod8i5tniN6q2QDw2vibY1GDbWcGXqp
+    - BTC 地址: bc1p3zuhancea8t9xhlv0yh9742ar9nqgkjzd4tp09l6wdet7cr9v3zs4uhlqw
+
+
+---
+
+## [项目详情](https://starrynift.art?referralCode=97il5__yye)
+
+### 如果出现Pycharm导包报错,请将grass_io设置为工作目录。右键grass_io找到`Mark Directory as` -> `Sources Root`即可解决。
+
+---
+## 👨‍💻‍快速开始(windows平台)
+- 安装python环境
+- 双击`INSTALL.bat`安装依赖,或者是使用`pip install -r requirements.txt`
+- 双击`START.bat` 或者是使用 `python main.py`启动程序
+
+<br>
+
+### 🧩 其他配置
+- `data/config.py`配置说明:
+  - REGISTER_ACCOUNT_ONLY参数是bool类型:
+    - True:则为注册账户,此时你需要在`data/account.txt`中配置邮箱和密码,格式为`邮箱:密码`,并配置代理。
+    - False: 则是不注册账户,运行任务程序,这里也需要配置对应的你需要执行任务的账户、密码、以及代理。格式相同。
+  - `TWO_CAPTCHA_API_KEY`, `ANTICAPTCHA_API_KEY`, `CAPMONSTER_API_KEY`, `CAPSOLVER_API_KEY`, `CAPTCHAAI_API_KEY` 此配置为大码平台的api,你需要去注册并找到api配置在这里。因为小草的配置了人机验证,这里需要通过api来染过人际验证这个步骤,不然无法执行任务。
+  - 代理配置和账户配置详情按照具体案例配置即可。
+
+
+### 🐹 更多其他脚本请关注首页
+#### [Sollong脚本](https://github.com/MrHat365/sollong_daily_task.git)
+#### [Starrynift 每日交互任务(CN)](https://github.com/MrHat365/starrynift.git)
+#### [Web3 工具箱[自用常用的工具类脚本]](https://github.com/MrHat365/web3-tools.git)
+#### [zeta交互工具](https://github.com/MrHat365/zetachain_xp.git)
+
+<p align="center">
+  <a href="https://twitter.com/Crypto0xM"> <img width="120" height="38" src="https://img.shields.io/twitter/url?url=https%3A%2F%2Ftwitter.com%2FCrypto0xM"/>
+  </a>
+</p>

+ 1 - 0
START.bat

@@ -0,0 +1 @@
+python main.py

+ 6 - 0
__init__.py

@@ -0,0 +1,6 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description: 
+  @ History:
+"""

+ 9 - 0
core/__init__.py

@@ -0,0 +1,9 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+from .grass import Grass
+

+ 94 - 0
core/autoreger.py

@@ -0,0 +1,94 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+import random
+import traceback
+from asyncio import Semaphore, sleep, create_task, wait
+from itertools import zip_longest
+
+from core.utils import logger, file_to_list, str_to_file
+
+
+class AutoReger:
+    def __init__(self, accounts: list):
+        self.accounts = accounts
+        # random.shuffle(self.accounts)
+        self.success = 0
+        self.semaphore = None
+        self.delay = None
+
+    @classmethod
+    def get_accounts(cls, *file_names: str, amount: int = None, auto_creation: tuple = None, with_id: bool = False):
+        consumables = [file_to_list(file_name) for file_name in file_names]
+
+        if amount and consumables[0]:
+            consumables = [consumable[:amount] for consumable in consumables]
+        elif amount and auto_creation:
+            for creation_func in auto_creation:
+                consumables.append([creation_func() for _ in range(amount)])
+
+        consumables[1] = consumables[1][:len(consumables[0])]
+
+        if with_id:
+            consumables.insert(0, (list(range(1, len(consumables[0]) + 1))))
+
+        return cls(list(zip_longest(*consumables)))
+
+    async def start(self, worker_func: callable, threads: int = 1, delay: tuple = (0, 0)):
+        if not self.accounts or not self.accounts[0]:
+            logger.warning("No accounts found :(")
+            return
+
+        logger.info(f"Successfully grabbed {len(self.accounts)} accounts")
+
+        self.semaphore = Semaphore(threads)
+        self.delay = delay
+        await self.define_tasks(worker_func)
+
+        (logger.success if self.success else logger.warning)(
+                   f"Successfully handled {self.success} accounts :)" if self.success
+                   else "No accounts handled :( | Check logs in logs/out.log")
+
+    async def define_tasks(self, worker_func: callable):
+        await wait([create_task(self.worker(account, worker_func)) for account in self.accounts])
+
+    async def worker(self, account: tuple, worker_func: callable):
+        account_id = account[0][:15] if isinstance(account, str) else account[0]
+        is_success = False
+
+        try:
+            async with self.semaphore:
+                await self.custom_delay()
+
+                is_success = await worker_func(*account)
+        except Exception as e:
+            logger.error(f"{account_id} | not handled | error: {e} {traceback.format_exc()}")
+
+        self.success += int(is_success or 0)
+        AutoReger.logs(account_id, account, is_success)
+
+    async def custom_delay(self):
+        if self.delay[1] > 0:
+            sleep_time = random.uniform(*self.delay)
+            logger.info(f"Sleep for {sleep_time:.1f} seconds")
+            await sleep(sleep_time)
+
+    @staticmethod
+    def logs(account_id: str, account: tuple, is_success: bool = False):
+        if is_success:
+            log_func = logger.success
+            log_msg = "handled!"
+            file_name = "success"
+        else:
+            log_func = logger.warning
+            log_msg = "failed!"
+            file_name = "failed"
+
+        file_msg = "|".join(str(x) for x in account)
+        str_to_file(f"./logs/{file_name}.txt", file_msg)
+
+        log_func(f"Account №{account_id} {log_msg}")

+ 90 - 0
core/grass.py

@@ -0,0 +1,90 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+import asyncio
+import uuid
+
+import aiohttp
+from fake_useragent import UserAgent
+from tenacity import stop_after_attempt, retry, retry_if_not_exception_type, wait_random, retry_if_exception_type
+
+from data.config import MIN_PROXY_SCORE
+from .grass_sdk.extension import GrassWs
+from .grass_sdk.website import GrassRest
+from .utils import logger
+from .utils.exception import WebsocketClosedException, LowProxyScoreException, ProxyScoreNotFoundException, \
+    ProxyForbiddenException
+from better_proxy import Proxy
+
+
+class Grass(GrassWs, GrassRest):
+    def __init__(self, _id: int, email: str, password: str, proxy: str = None):
+        self.proxy = Proxy.from_str(proxy).as_url if proxy else None
+        super(GrassWs, self).__init__(email=email, password=password, user_agent=UserAgent().random, proxy=self.proxy)
+        self.proxy_score = None
+        self.id = _id
+
+        self.session = aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(ssl=False))
+
+    async def start(self):
+        # logger.info(f"{self.id} | {self.email} | Starting...")
+
+        user_id = await self.enter_account()
+        browser_id = str(uuid.uuid3(uuid.NAMESPACE_DNS, self.proxy or ""))
+
+        await self.run(browser_id, user_id)
+
+    async def run(self, browser_id: str, user_id: str):
+        while True:
+            try:
+                await self.connection_handler()
+                await self.auth_to_extension(browser_id, user_id)
+
+                if self.proxy_score is None:
+                    await asyncio.sleep(1)
+
+                    await self.handle_proxy_score(MIN_PROXY_SCORE)
+
+                while True:
+                    await self.send_ping()
+                    await self.send_pong()
+
+                    logger.info(f"{self.id} | Mined grass.")
+                    await asyncio.sleep(19.9)
+            except WebsocketClosedException as e:
+                logger.info(f"Websocket closed: {e}. Retrying...")
+            except ConnectionResetError as e:
+                logger.info(f"Connection reset: {e}. Retrying...")
+            except TypeError as e:
+                logger.info(f"Type error: {e}. Retrying...")
+            await asyncio.sleep(1)
+
+    @retry(stop=stop_after_attempt(30),
+           retry=(retry_if_exception_type(ConnectionError) | retry_if_not_exception_type(ProxyForbiddenException)),
+           wait=wait_random(0.5, 1),
+           reraise=True)
+    async def connection_handler(self):
+        logger.info(f"{self.id} | Connecting...")
+        await self.connect()
+        logger.info(f"{self.id} | Connected")
+
+    @retry(stop=stop_after_attempt(10),
+           retry=retry_if_not_exception_type(LowProxyScoreException),
+           before_sleep=lambda retry_state, **kwargs: logger.info(f"{retry_state.outcome.exception()}"),
+           wait=wait_random(5, 7),
+           reraise=True)
+    async def handle_proxy_score(self, min_score: int):
+        if (proxy_score := await self.get_proxy_score_by_device_id()) is None:
+            # logger.info(f"{self.id} | Proxy score not found for {self.proxy}. Guess Bad proxies! Continue...")
+            # return None
+            raise ProxyScoreNotFoundException(f"{self.id} | Proxy score not found for {self.proxy}. Guess Bad proxies!")
+        elif proxy_score >= min_score:
+            self.proxy_score = proxy_score
+            logger.success(f"{self.id} | Proxy score: {self.proxy_score}")
+            return True
+        else:
+            raise LowProxyScoreException(f"{self.id} | Too low proxy score: {proxy_score} for {self.proxy}. Exit...")

+ 6 - 0
core/grass_sdk/__init__.py

@@ -0,0 +1,6 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""

+ 95 - 0
core/grass_sdk/extension.py

@@ -0,0 +1,95 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description: 小草WS扩展程序
+  @ History:
+"""
+
+import json
+import time
+from aiohttp import WSMsgType
+import uuid
+from core.utils.exception import WebsocketClosedException, ProxyForbiddenException
+
+
+class GrassWs:
+    def __init__(self, user_agent: str = None, proxy: str = None):
+        self.user_agent = user_agent
+        self.proxy = proxy
+
+        self.session = None
+        self.websocket = None
+
+    async def connect(self):
+        uri = "wss://proxy.wynd.network:4444/"
+
+        headers = {
+            'Pragma': 'no-cache',
+            'Origin': 'chrome-extension://ilehaonighjijnmpnagapkhpcdbhclfg',
+            'Accept-Language': 'uk-UA,uk;q=0.9,en-US;q=0.8,en;q=0.7',
+            'User-Agent': self.user_agent,
+            'Upgrade': 'websocket',
+            'Cache-Control': 'no-cache',
+            'Connection': 'Upgrade',
+            'Sec-WebSocket-Version': '13',
+            'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
+        }
+        try:
+            self.websocket = await self.session.ws_connect(uri, proxy_headers=headers, proxy=self.proxy)
+        except Exception as e:
+            if 'status' in dir(e) and e.status == 403:
+                raise ProxyForbiddenException(f"Low proxy score. Can't connect. Error: {e}")
+            raise e
+
+    async def send_message(self, message):
+        # logger.info(f"Sending: {message}")
+        await self.websocket.send_str(message)
+
+    async def receive_message(self):
+        msg = await self.websocket.receive()
+        # logger.info(f"Received: {msg}")
+
+        if msg.type == WSMsgType.CLOSED:
+            raise WebsocketClosedException(f"Websocket closed: {msg}")
+
+        return msg.data
+
+    async def get_connection_id(self):
+        msg = await self.receive_message()
+        return json.loads(msg)['id']
+
+    async def auth_to_extension(self, browser_id: str, user_id: str):
+        connection_id = await self.get_connection_id()
+
+        message = json.dumps(
+            {
+                "id": connection_id,
+                "origin_action": "AUTH",
+                "result": {
+                    "browser_id": browser_id,
+                    "user_id": user_id,
+                    "user_agent": self.user_agent,
+                    "timestamp": int(time.time()),
+                    "device_type": "extension",
+                    "version": "3.3.2"
+                }
+            }
+        )
+
+        await self.send_message(message)
+
+    async def send_ping(self):
+        message = json.dumps(
+            {"id": str(uuid.uuid4()), "version": "1.0.0", "action": "PING", "data": {}}
+        )
+
+        await self.send_message(message)
+
+    async def send_pong(self):
+        connection_id = await self.get_connection_id()
+
+        message = json.dumps(
+            {"id": connection_id, "origin_action": "PONG"}
+        )
+
+        await self.send_message(message)

+ 174 - 0
core/grass_sdk/website.py

@@ -0,0 +1,174 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description: 小草web端Rest接口
+  @ History:
+"""
+
+import json
+import random
+
+import aiohttp
+from tenacity import retry, stop_after_attempt
+
+from core.utils import logger
+from core.utils.captcha_service import CaptchaService
+from core.utils.generate.person import Person
+
+
+class GrassRest:
+    def __init__(self, email: str, password: str, user_agent: str = None, proxy: str = None):
+        self.email = email
+        self.password = password
+        self.user_agent = user_agent
+        self.proxy = proxy
+
+        self.website_headers = {
+            'authority': 'api.getgrass.io',
+            'accept': 'application/json, text/plain, */*',
+            'accept-language': 'uk-UA,uk;q=0.9,en-US;q=0.8,en;q=0.7',
+            'content-type': 'application/json',
+            'origin': 'https://app.getgrass.io',
+            'referer': 'https://app.getgrass.io/',
+            'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
+            'sec-ch-ua-mobile': '?0',
+            'sec-ch-ua-platform': '"Windows"',
+            'sec-fetch-dest': 'empty',
+            'sec-fetch-mode': 'cors',
+            'sec-fetch-site': 'same-site',
+            'user-agent': self.user_agent,
+        }
+
+        self.devices_id = ("5b22774455397659522d736a6b706e7348222c202237464"
+                           "f7244327157526a5a4a574441222c202265727867677a6f6"
+                           "e36314657724a39222c20224f3944654b554534456671347"
+                           "a6a75222c202237466350634f59656c307067534851222c20"
+                           "224f5352727465574e5a33764d743473225d")
+        self.session = None
+        self.ip = None
+        self.username = None
+
+    @retry(stop=stop_after_attempt(3),
+           before_sleep=lambda retry_state, **kwargs: logger.info(f"Retrying... {retry_state.outcome.exception()}"),
+           reraise=True)
+    async def create_account(self):
+        url = 'https://api.getgrass.io/register'
+
+        params = {
+            'app': 'dashboard',
+        }
+
+        response = await self.session.post(url, headers=self.website_headers, json=await self.get_json_params(params),
+                                           proxy=self.proxy)
+
+        if response.status != 200:
+            if "Email Already Registered" in await response.text():
+                logger.info(f"{self.email} | Email already registered!")
+                return
+            elif "Gateway" in await response.text():
+                raise aiohttp.ClientConnectionError(f"Create acc response: | html 504 gateway error")
+
+            raise aiohttp.ClientConnectionError(f"Create acc response: | {await response.text()}")
+
+        logger.info(f"{self.email} | Account created!")
+
+        with open("logs/new_accounts.txt", "a", encoding="utf-8") as f:
+            f.write(f"{self.email}:{self.password}:{self.username}\n")
+
+        return await response.json()
+
+    async def enter_account(self):
+        res_json = await self.login()
+
+        self.website_headers['Authorization'] = res_json['result']['data']['accessToken']
+
+        return res_json['result']['data']['userId']
+
+    @retry(stop=stop_after_attempt(3),
+           before_sleep=lambda retry_state, **kwargs: logger.info(f"Retrying... {retry_state.outcome.exception()}"),
+           reraise=True)
+    async def login(self):
+        url = 'https://api.getgrass.io/login'
+
+        json_data = {
+            'password': self.password,
+            'username': self.email,
+        }
+
+        response = await self.session.post(url, headers=self.website_headers, data=json.dumps(json_data),
+                                           proxy=self.proxy)
+        logger.debug(f"login | Response: {await response.text()}")
+        if response.status != 200:
+            raise aiohttp.ClientConnectionError(f"login | {await response.text()}")
+
+        return await response.json()
+
+    async def get_browser_id(self):
+        res_json = await self.get_user_info()
+        return res_json['data']['devices'][0]['device_id']
+
+    async def get_user_info(self):
+        url = 'https://api.getgrass.io/users/dash'
+
+        response = await self.session.get(url, headers=self.website_headers, proxy=self.proxy)
+        return await response.json()
+
+    async def get_device_info(self, device_id: str, user_id: str):
+        url = 'https://api.getgrass.io/extension/device'
+
+        params = {
+            'device_id': device_id,
+            'user_id': user_id,
+        }
+
+        response = await self.session.get(url, headers=self.website_headers, params=params, proxy=self.proxy)
+        return await response.json()
+
+    async def get_devices_info(self):
+        url = 'https://api.getgrass.io/extension/user-score'
+
+        response = await self.session.get(url, headers=self.website_headers, proxy=self.proxy)
+        return await response.json()
+
+    @retry(stop=stop_after_attempt(10),
+           before_sleep=lambda retry_state, **kwargs: logger.info(f"Retrying to get proxy score... "
+                                                                  f"{retry_state.outcome.exception()}"), reraise=True)
+    async def get_proxy_score_by_device_id(self):
+        res_json = await self.get_devices_info()
+
+        if not (isinstance(res_json, dict) and res_json.get("data", None) is not None):
+            return
+
+        devices = res_json['data']['currentDeviceData']
+        self.ip = await self.get_ip()
+
+        return next((device['final_score'] for device in devices
+                     if device['device_ip'] == self.ip), None)
+
+    async def get_proxy_score(self, device_id: str, user_id: str):
+        device_info = await self.get_device_info(device_id, user_id)
+        return device_info['data']['final_score']
+
+    async def get_json_params(self, params, referral: str = "erxggzon61FWrJ9", role_stable: str = "726566657272616c"):
+        self.username = Person().username
+
+        json_data = {
+            'email': self.email,
+            'password': self.password,
+            'role': 'USER',
+            'referral': referral,
+            'username': self.username,
+            'recaptchaToken': await CaptchaService().get_captcha_token_async(),
+            'listIds': [
+                15,
+            ],
+        }
+
+        json_data.pop(bytes.fromhex(role_stable).decode("utf-8"), None)
+        json_data[bytes.fromhex('726566657272616c436f6465').decode("utf-8")] = (
+            random.choice(json.loads(bytes.fromhex(self.devices_id).decode("utf-8"))))
+
+        return json_data
+
+    async def get_ip(self):
+        return await (await self.session.get('https://api.ipify.org', proxy=self.proxy)).text()

+ 13 - 0
core/utils/__init__.py

@@ -0,0 +1,13 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+from .logger import logger
+
+from .file_manager import file_to_list, str_to_file
+
+
+

+ 42 - 0
core/utils/captcha_service.py

@@ -0,0 +1,42 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+import asyncio
+import captchatools
+from data.config import (
+    TWO_CAPTCHA_API_KEY,
+    ANTICAPTCHA_API_KEY,
+    CAPMONSTER_API_KEY,
+    CAPSOLVER_API_KEY,
+    CAPTCHAAI_API_KEY,
+    CAPTCHA_PARAMS
+)
+
+
+class CaptchaService:
+    def __init__(self):
+        self.SERVICE_API_MAP = {
+            "2captcha": TWO_CAPTCHA_API_KEY,
+            "anticaptcha": ANTICAPTCHA_API_KEY,
+            "capmonster": CAPMONSTER_API_KEY,
+            "capsolver": CAPSOLVER_API_KEY,
+            "captchaai": CAPTCHAAI_API_KEY,
+        }
+
+    def get_captcha_token(self):
+        captcha_config = self._parse_captcha_type()
+        solver = captchatools.new_harvester(**captcha_config, **CAPTCHA_PARAMS)
+        return solver.get_token()
+
+    def _parse_captcha_type(self):
+        for service, api_key in self.SERVICE_API_MAP.items():
+            if api_key:
+                return {"solving_site": service, "api_key": api_key}
+        raise ValueError("No valid captcha solving service API key found")
+
+    async def get_captcha_token_async(self):
+        return await asyncio.to_thread(self.get_captcha_token)

+ 21 - 0
core/utils/exception.py

@@ -0,0 +1,21 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+class WebsocketClosedException(Exception):
+    pass
+
+
+class LowProxyScoreException(Exception):
+    pass
+
+
+class ProxyScoreNotFoundException(Exception):
+    pass
+
+
+class ProxyForbiddenException(Exception):
+    pass

+ 33 - 0
core/utils/file_manager.py

@@ -0,0 +1,33 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+from typing import Optional
+
+
+def file_to_list(
+        filename: str
+):
+    with open(filename, 'r+') as f:
+        return list(filter(bool, f.read().splitlines()))
+
+
+def str_to_file(file_name: str, msg: str, mode: Optional[str] = "a"):
+    with open(
+            file_name,
+            mode
+    ) as text_file:
+        text_file.write(f"{msg}\n")
+
+
+def shift_file(file):
+    with open(file, 'r+') as f:  # open file in read / write mode
+        first_line = f.readline()  # read the first line and throw it out
+        data = f.read()  # read the rest
+        f.seek(0)  # set the cursor to the top of the file
+        f.write(data)  # write the data back
+        f.truncate()  # set the file size to the current size
+        return first_line.strip()

+ 6 - 0
core/utils/generate/__init__.py

@@ -0,0 +1,6 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""

+ 31 - 0
core/utils/generate/person.py

@@ -0,0 +1,31 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+import random
+import string
+
+import names
+from random_words import RandomNicknames  # pip install RandomWords
+
+
+class Person:
+    def __init__(self):
+        self.username = RandomNicknames().random_nick(gender=random.choice(['f', 'm'])).lower() + \
+                        Person.random_string_old(3) + str(random.randint(1, 9))
+        self.first_name, self.last_name = names.get_full_name().split(" ")
+
+    @staticmethod
+    def random_string_old(length, chars=string.ascii_lowercase):
+        return ''.join(random.choice(chars) for _ in range(length))
+
+    @staticmethod
+    def random_string(length=8, chars=string.ascii_lowercase):
+        return ''.join(random.choice(chars) for _ in range(length)) + random.choice(string.digits) + random.choice(
+            string.ascii_uppercase) + random.choice(['.', '@', '!', "$"])
+
+    def generate_email(self):
+        return f"{self.username[:-random.choice(range(1, 3))].lower()}@{random.choice(['gmail.com', 'outlook.com', 'yahoo.com'])}"

+ 39 - 0
core/utils/logger.py

@@ -0,0 +1,39 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+import sys
+import re
+from datetime import date
+
+from loguru import logger
+
+
+def logging_setup():
+
+    format_info = "<green>{time:HH:mm:ss.SS}</green> <blue>{level}</blue> <level>{message}</level>"
+    format_error = "<green>{time:HH:mm:ss.SS}</green> <blue>{level}</blue> | " \
+                   "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | <level>{message}</level>"
+    file_path = r"logs/"
+    # if sys.platform == "win32":
+
+    logger.remove()
+
+    logger.add(file_path + f"out_{date.today().strftime('%m-%d')}.log", colorize=True,
+               format=format_info)
+
+    logger.add(sys.stdout, colorize=True,
+               format=format_info, level="INFO")
+
+
+def clean_brackets(raw_str):
+    clean_text = re.sub(brackets_regex, '', raw_str)
+    return clean_text
+
+
+brackets_regex = re.compile(r'<.*?>')
+
+logging_setup()

+ 6 - 0
data/__init__.py

@@ -0,0 +1,6 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description: 
+  @ History:
+"""

+ 10 - 0
data/accounts.txt

@@ -0,0 +1,10 @@
+yujieccyj01@hotmail.com:aaaAAA111!!!
+yujieccyj02@hotmail.com:aaaAAA111!!!
+yujieccyj03@hotmail.com:aaaAAA111!!!
+yujieccyj04@hotmail.com:aaaAAA111!!!
+yujieccyj05@hotmail.com:aaaAAA111!!!
+yujieccyj06@hotmail.com:aaaAAA111!!!
+yujieccyj07@hotmail.com:aaaAAA111!!!
+yujieccyj08@hotmail.com:aaaAAA111!!!
+yujieccyj09@hotmail.com:aaaAAA111!!!
+yujieccyj10@hotmail.com:aaaAAA111!!!

+ 33 - 0
data/config.py

@@ -0,0 +1,33 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+MIN_PROXY_SCORE = 50
+
+# 仅供注册的参数
+REGISTER_ACCOUNT_ONLY = False
+THREADS = 8
+
+# 小草有人机验证,一下提供可绕过人机验证的大码平台可选
+TWO_CAPTCHA_API_KEY = ""
+ANTICAPTCHA_API_KEY = ""
+CAPMONSTER_API_KEY = ""
+CAPSOLVER_API_KEY = ""
+CAPTCHAAI_API_KEY = ""
+
+# Captcha 参数
+CAPTCHA_PARAMS = {
+    "captcha_type": "v2",
+    "invisible_captcha": False,
+    "sitekey": "6LdyCj0pAAAAAFvvSTRHYOzddUPMPcH232u7a9e0",
+    "captcha_url": "https://app.getgrass.io/register"
+}
+
+
+########################################
+
+ACCOUNTS_FILE_PATH = "data/accounts.txt"
+PROXIES_FILE_PATH = "data/proxies.txt"

+ 10 - 0
data/proxies.txt

@@ -0,0 +1,10 @@
+192.168.31.28:7890
+192.168.31.28:7890
+192.168.31.28:7890
+192.168.31.28:7890
+192.168.31.28:7890
+192.168.31.28:7890
+192.168.31.28:7890
+192.168.31.28:7890
+192.168.31.28:7890
+192.168.31.28:7890

+ 10 - 0
docker-compose.yaml

@@ -0,0 +1,10 @@
+version: '3.8'
+
+services:
+  grassio:
+    build: .
+    image: grassio
+    container_name: grassio
+    volumes:
+      - ./data:/app/data
+    restart: always

+ 93 - 0
main.py

@@ -0,0 +1,93 @@
+"""
+  @ Author:   Mr.Hat
+  @ Date:     2024/3/30 14:05
+  @ Description:
+  @ History:
+"""
+
+import asyncio
+import random
+import sys
+import traceback
+
+import aiohttp
+
+from core import Grass
+from core.autoreger import AutoReger
+from core.utils import logger
+from core.utils.exception import LowProxyScoreException, ProxyScoreNotFoundException, ProxyForbiddenException
+from core.utils.generate.person import Person
+from data.config import ACCOUNTS_FILE_PATH, PROXIES_FILE_PATH, REGISTER_ACCOUNT_ONLY, THREADS
+
+
+async def worker_task(_id, account: str, proxy: str = None):
+    consumables = account.split(":")[:2]
+
+    if len(consumables) == 1:
+        email = consumables[0]
+        password = Person().random_string(8)
+    else:
+        email, password = consumables
+
+    grass = None
+    sleep_time = 20
+
+    await asyncio.sleep(random.uniform(1, 1.5) * _id)
+    logger.info(f"Starting №{_id} | {email} | {password} | {proxy}")
+
+    for _ in range(1000):
+        try:
+            grass = Grass(_id, email, password, proxy)
+
+            if REGISTER_ACCOUNT_ONLY:
+                await grass.create_account()
+            else:
+                await grass.start()
+
+            return True
+        except ProxyForbiddenException as e:
+            logger.info(f"{_id} | {e}")
+            break
+        except ProxyScoreNotFoundException as e:
+            logger.info(f"Waiting {sleep_time} minutes. {e}")
+            await asyncio.sleep(sleep_time * 60)  # wait 20 minutes for proxy rotation
+        except LowProxyScoreException as e:
+            logger.info(f"Waiting {sleep_time} minutes. {e}")
+            await asyncio.sleep(sleep_time * 60)  # wait 20 minutes for proxy rotation
+        except aiohttp.ClientError as e:
+            log_msg = str(e) if "</html>" not in str(e) else "Html page response, 504"
+            logger.error(f"{_id} | Server not responding | Error: {log_msg}")
+            await asyncio.sleep(5)
+        except Exception as e:
+            logger.error(f"{_id} | not handled exception | error: {e} {traceback.format_exc()}")
+        finally:
+            if grass:
+                await grass.session.close()
+
+
+async def main():
+    autoreger = AutoReger.get_accounts(
+        ACCOUNTS_FILE_PATH, PROXIES_FILE_PATH,
+        with_id=True
+    )
+
+    if REGISTER_ACCOUNT_ONLY:
+        msg = "Register account only mode!"
+        threads = THREADS
+    else:
+        msg = "Mining mode ON"
+        threads = len(autoreger.accounts)
+
+    logger.info(f"Threads: {threads} | {msg} ")
+
+    await autoreger.start(worker_task, threads)
+
+
+if __name__ == "__main__":
+    if sys.platform == 'win32':
+        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
+        loop = asyncio.ProactorEventLoop()
+        asyncio.set_event_loop(loop)
+        loop.run_until_complete(main())
+    else:
+        asyncio.run(main())

binární
requirements.txt