├── .python-version ├── .prettierrc ├── nonebot_plugin_setu_now ├── aioutils │ ├── __init__.py │ └── _main.py ├── cooldown.py ├── perf_timer.py ├── models.py ├── config.py ├── data_source.py ├── utils.py ├── img_utils.py ├── database.py └── __init__.py ├── .vscode ├── settings.json └── docstring.mustache ├── LICENSE ├── migrations └── versions │ ├── 4d3ca10a3be8_add_cooldown_record.py │ ├── 4f9463d0ecb0_change_groupwhitelistrecord.py │ └── dc8d05807856_first_revision.py ├── .github └── workflows │ └── publish.yml ├── pyproject.toml ├── .gitignore └── README.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "endOfLine": "lf", 5 | "arrowParens": "always", 6 | "singleQuote": false, 7 | "trailingComma": "es5", 8 | "semi": true 9 | } 10 | -------------------------------------------------------------------------------- /nonebot_plugin_setu_now/aioutils/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.1" 2 | 3 | from ._main import PendingValueException as PendingValueException 4 | from ._main import SoonValue as SoonValue 5 | from ._main import asyncify as asyncify 6 | from ._main import create_task_group as create_task_group 7 | from ._main import runnify as runnify 8 | from ._main import syncify as syncify 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.watcherExclude": { 4 | "**/.git/objects/**": true, 5 | "**/.git/subtree-cache/**": true, 6 | "**/node_modules/**": true, 7 | "**/.hg/store/**": true, 8 | "**/.venv/**": true, 9 | "**/.mypy_cache/**": true 10 | }, 11 | "autoDocstring.customTemplatePath": ".vscode/docstring.mustache", 12 | "files.encoding": "utf8", 13 | "git.autofetch": true, 14 | "git.enableSmartCommit": true, 15 | "files.exclude": { 16 | "**/__pycache__": true, 17 | ".venv": true, 18 | ".mypy_cache": true, 19 | "LICENSE": true 20 | }, 21 | "ruff.enable": true 22 | } -------------------------------------------------------------------------------- /nonebot_plugin_setu_now/cooldown.py: -------------------------------------------------------------------------------- 1 | """ 2 | CD 管理 3 | """ 4 | 5 | from nonebot import Bot, logger 6 | from nonebot_plugin_uninfo import Uninfo 7 | 8 | from .config import CDTIME 9 | from .database import CooldownRecord 10 | 11 | 12 | async def Cooldown(bot: Bot, session: Uninfo) -> bool: 13 | """检查用户是否在冷却中 14 | 15 | 超级管理员可以忽略冷却时间 16 | 17 | Args: 18 | session: 会话信息 19 | 20 | Returns: 21 | bool: 是否在冷却中(True表示冷却完成,False表示还在冷却中) 22 | """ 23 | user_id = session.user.id 24 | 25 | if session.user.id in bot.config.superusers: 26 | logger.debug("Superuser ignore cooldown") 27 | return True 28 | 29 | return await CooldownRecord.check_is_cooldown(user_id, cd=CDTIME) 30 | -------------------------------------------------------------------------------- /nonebot_plugin_setu_now/perf_timer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import time 4 | from typing import ClassVar 5 | 6 | from nonebot.log import logger 7 | 8 | 9 | class PerfTimer: 10 | timer_list: ClassVar[dict[str, float]] = {} 11 | 12 | def __init__(self, name: str) -> None: 13 | self.name: str = name 14 | self.start_time: float = time.time() 15 | 16 | @classmethod 17 | def start(cls, name: str, output: bool = False): 18 | if output: 19 | logger.debug(f"{name} started") 20 | return cls(name) 21 | 22 | def stop(self): 23 | timer_count = time.time() - self.start_time 24 | timer_count = round(timer_count, 2) 25 | logger.debug(f"{self.name} took {timer_count}s") 26 | -------------------------------------------------------------------------------- /.vscode/docstring.mustache: -------------------------------------------------------------------------------- 1 | {{! Takker Automatic docstring}} 2 | 3 | :说明: `{{name}}` 4 | > {{summaryPlaceholder}} {{extendedSummaryPlaceholder}} 5 | {{#argsExist}} 6 | 7 | :参数: 8 | {{#args}} 9 | * `{{var}}: {{typePlaceholder}}`: {{descriptionPlaceholder}} 10 | {{/args}} 11 | {{/argsExist}} 12 | {{#kwargsExist}} 13 | 14 | :可选参数: 15 | {{#kwargs}} 16 | * `{{var}}: {{typePlaceholder}} = {{default}}`: {{descriptionPlaceholder}} 17 | {{/kwargs}} 18 | {{/kwargsExist}} 19 | {{#exceptionsExist}} 20 | 21 | :Exceptions: 22 | {{#exceptions}} 23 | * `{{var}}`: {{descriptionPlaceholder}} 24 | {{/exceptions}} 25 | {{/exceptionsExist}} 26 | {{#yieldsExist}} 27 | 28 | :生成: 29 | {{#yields}} 30 | * `{{typePlaceholder}}`: {{descriptionPlaceholder}} 31 | {{/yields}} 32 | {{/yieldsExist}} 33 | {{#returnsExist}} 34 | 35 | :返回: 36 | {{#returns}} 37 | - `{{typePlaceholder}}`: {{descriptionPlaceholder}} 38 | {{/returns}} 39 | {{/returnsExist}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 kexue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nonebot_plugin_setu_now/models.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class SetuData(BaseModel): 7 | pid: int 8 | p: int 9 | uid: int 10 | title: str 11 | author: str 12 | r18: bool 13 | width: int 14 | height: int 15 | tags: list[str] 16 | ext: str 17 | aiType: int 18 | uploadDate: int 19 | urls: dict[str, str] 20 | 21 | 22 | class SetuApiData(BaseModel): 23 | error: str | None 24 | data: list[SetuData] 25 | 26 | 27 | class Setu: 28 | def __init__(self, data: SetuData): 29 | self.title: str = data.title 30 | self.urls: dict[str, str] = data.urls 31 | self.author: str = data.author 32 | self.tags: list[str] = data.tags 33 | self.pid: int = data.pid 34 | self.p: int = data.p 35 | self.r18: bool = data.r18 36 | self.ext: str = data.ext 37 | self.img: Path | None = None 38 | self.msg: str | None = None 39 | 40 | 41 | class SetuMessage(BaseModel): 42 | send: list[str] 43 | cd: list[str] 44 | 45 | 46 | class SetuNotFindError(Exception): 47 | pass 48 | -------------------------------------------------------------------------------- /nonebot_plugin_setu_now/config.py: -------------------------------------------------------------------------------- 1 | from nonebot import get_plugin_config 2 | from pydantic import BaseModel 3 | 4 | 5 | class Config(BaseModel): 6 | superusers: set[str] 7 | debug: bool = False 8 | setu_cd: int = 60 9 | setu_path: str | None = None 10 | setu_proxy: str | None = None 11 | setu_withdraw: int | None = None 12 | setu_reverse_proxy: str = "i.pixiv.re" 13 | setu_size: str = "regular" 14 | setu_api_url: str = "https://api.lolicon.app/setu/v2" 15 | setu_max: int = 30 16 | setu_add_random_effect: bool = True 17 | setu_minimum_send_interval: int = 3 18 | setu_excludeAI: bool = False 19 | setu_r18: bool = False 20 | 21 | 22 | plugin_config = get_plugin_config(Config) 23 | CDTIME = plugin_config.setu_cd 24 | SETU_PATH = plugin_config.setu_path 25 | PROXY = plugin_config.setu_proxy 26 | WITHDRAW_TIME = plugin_config.setu_withdraw 27 | REVERSE_PROXY = plugin_config.setu_reverse_proxy 28 | SETU_SIZE = plugin_config.setu_size 29 | API_URL = plugin_config.setu_api_url 30 | MAX = plugin_config.setu_max 31 | EFFECT = plugin_config.setu_add_random_effect 32 | SEND_INTERVAL = plugin_config.setu_minimum_send_interval 33 | EXCLUDEAI = plugin_config.setu_excludeAI 34 | SETU_R18 = plugin_config.setu_r18 35 | -------------------------------------------------------------------------------- /migrations/versions/4d3ca10a3be8_add_cooldown_record.py: -------------------------------------------------------------------------------- 1 | """add cooldown record 2 | 3 | 迁移 ID: 4d3ca10a3be8 4 | 父迁移: dc8d05807856 5 | 创建时间: 2025-10-23 22:14:01.393201 6 | 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | from collections.abc import Sequence 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | revision: str = "4d3ca10a3be8" 17 | down_revision: str | Sequence[str] | None = "dc8d05807856" 18 | branch_labels: str | Sequence[str] | None = None 19 | depends_on: str | Sequence[str] | None = None 20 | 21 | 22 | def upgrade(name: str = "") -> None: 23 | if name: 24 | return 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_table( 27 | "nonebot_plugin_setu_now_cooldownrecord", 28 | sa.Column("user_id", sa.String(), nullable=False), 29 | sa.Column("last_use_time", sa.DateTime(), nullable=False), 30 | sa.PrimaryKeyConstraint( 31 | "user_id", name=op.f("pk_nonebot_plugin_setu_now_cooldownrecord") 32 | ), 33 | info={"bind_key": "nonebot_plugin_setu_now"}, 34 | ) 35 | # ### end Alembic commands ### 36 | 37 | 38 | def downgrade(name: str = "") -> None: 39 | if name: 40 | return 41 | # ### commands auto generated by Alembic - please adjust! ### 42 | op.drop_table("nonebot_plugin_setu_now_cooldownrecord") 43 | # ### end Alembic commands ### 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "*" 5 | workflow_dispatch: 6 | 7 | 8 | jobs: 9 | pypi-publish: 10 | name: Upload release to PyPI 11 | runs-on: ubuntu-latest 12 | environment: release 13 | permissions: 14 | # IMPORTANT: this permission is mandatory for trusted publishing 15 | id-token: write 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install the latest version of uv 21 | uses: astral-sh/setup-uv@v3 22 | with: 23 | enable-cache: true 24 | 25 | - name: "Set up Python" 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version-file: ".python-version" 29 | 30 | - run: uv sync 31 | shell: bash 32 | 33 | # - name: Get Version 34 | # id: version 35 | # run: | 36 | # echo "VERSION=$(uvx pdm show --version)" >> $GITHUB_OUTPUT 37 | # echo "TAG_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 38 | # echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 39 | 40 | # - name: Check Version 41 | # if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION 42 | # run: exit 1 43 | 44 | - name: Build Package 45 | run: uv build 46 | 47 | - uses: actions/upload-artifact@v4 48 | with: 49 | name: artifact 50 | path: dist/ # or path/to/artifact 51 | 52 | - name: pypi-publish 53 | uses: pypa/gh-action-pypi-publish@v1.12.3 54 | # - name: Publish Package to PyPI 55 | # run: uv publish 56 | 57 | # - name: Publish Package to GitHub Release 58 | # run: gh release create ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl -t "🔖 ${{ steps.version.outputs.TAG_NAME }}" --generate-notes 59 | # env: 60 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | -------------------------------------------------------------------------------- /migrations/versions/4f9463d0ecb0_change_groupwhitelistrecord.py: -------------------------------------------------------------------------------- 1 | """change GroupWhiteListRecord 2 | 3 | 迁移 ID: 4f9463d0ecb0 4 | 父迁移: 4d3ca10a3be8 5 | 创建时间: 2025-10-23 23:09:35.517294 6 | 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | from collections.abc import Sequence 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | revision: str = "4f9463d0ecb0" 17 | down_revision: str | Sequence[str] | None = "4d3ca10a3be8" 18 | branch_labels: str | Sequence[str] | None = None 19 | depends_on: str | Sequence[str] | None = None 20 | 21 | 22 | def upgrade(name: str = "") -> None: 23 | if name: 24 | return 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | with op.batch_alter_table( 27 | "nonebot_plugin_setu_now_groupwhitelistrecord", schema=None 28 | ) as batch_op: 29 | batch_op.alter_column( 30 | "group_id", 31 | existing_type=sa.INTEGER(), 32 | type_=sa.String(), 33 | existing_nullable=False, 34 | ) 35 | batch_op.alter_column( 36 | "operator_user_id", 37 | existing_type=sa.INTEGER(), 38 | type_=sa.String(), 39 | existing_nullable=False, 40 | ) 41 | 42 | # ### end Alembic commands ### 43 | 44 | 45 | def downgrade(name: str = "") -> None: 46 | if name: 47 | return 48 | # ### commands auto generated by Alembic - please adjust! ### 49 | with op.batch_alter_table( 50 | "nonebot_plugin_setu_now_groupwhitelistrecord", schema=None 51 | ) as batch_op: 52 | batch_op.alter_column( 53 | "operator_user_id", 54 | existing_type=sa.String(), 55 | type_=sa.INTEGER(), 56 | existing_nullable=False, 57 | ) 58 | batch_op.alter_column( 59 | "group_id", 60 | existing_type=sa.String(), 61 | type_=sa.INTEGER(), 62 | existing_nullable=False, 63 | ) 64 | 65 | # ### end Alembic commands ### 66 | -------------------------------------------------------------------------------- /migrations/versions/dc8d05807856_first_revision.py: -------------------------------------------------------------------------------- 1 | """first revision 2 | 3 | 迁移 ID: dc8d05807856 4 | 父迁移: 5 | 创建时间: 2025-10-23 19:04:52.135908 6 | 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | from collections.abc import Sequence 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | revision: str = "dc8d05807856" 17 | down_revision: str | Sequence[str] | None = None 18 | branch_labels: str | Sequence[str] | None = None 19 | depends_on: str | Sequence[str] | None = None 20 | 21 | 22 | def upgrade(name: str = "") -> None: 23 | if name: 24 | return 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_table( 27 | "nonebot_plugin_setu_now_groupwhitelistrecord", 28 | sa.Column("group_id", sa.Integer(), nullable=False), 29 | sa.Column("operator_user_id", sa.Integer(), nullable=False), 30 | sa.PrimaryKeyConstraint( 31 | "group_id", name=op.f("pk_nonebot_plugin_setu_now_groupwhitelistrecord") 32 | ), 33 | info={"bind_key": "nonebot_plugin_setu_now"}, 34 | ) 35 | op.create_table( 36 | "nonebot_plugin_setu_now_messageinfo", 37 | sa.Column("message_id", sa.Integer(), nullable=False), 38 | sa.Column("pid", sa.Integer(), nullable=False), 39 | sa.PrimaryKeyConstraint( 40 | "message_id", name=op.f("pk_nonebot_plugin_setu_now_messageinfo") 41 | ), 42 | info={"bind_key": "nonebot_plugin_setu_now"}, 43 | ) 44 | op.create_table( 45 | "nonebot_plugin_setu_now_setuinfo", 46 | sa.Column("pid", sa.Integer(), nullable=False), 47 | sa.Column("author", sa.String(), nullable=False), 48 | sa.Column("title", sa.String(), nullable=False), 49 | sa.Column("url", sa.String(), nullable=False), 50 | sa.PrimaryKeyConstraint( 51 | "pid", name=op.f("pk_nonebot_plugin_setu_now_setuinfo") 52 | ), 53 | info={"bind_key": "nonebot_plugin_setu_now"}, 54 | ) 55 | # ### end Alembic commands ### 56 | 57 | 58 | def downgrade(name: str = "") -> None: 59 | if name: 60 | return 61 | # ### commands auto generated by Alembic - please adjust! ### 62 | op.drop_table("nonebot_plugin_setu_now_setuinfo") 63 | op.drop_table("nonebot_plugin_setu_now_messageinfo") 64 | op.drop_table("nonebot_plugin_setu_now_groupwhitelistrecord") 65 | # ### end Alembic commands ### 66 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "nonebot-plugin-setu-now" 3 | version = "1.0.0.dev2" 4 | description = "另一个色图插件" 5 | authors = [{ name = "kexue", email = "xana278@foxmail.com" }] 6 | dependencies = [ 7 | "httpx>=0.28.0", 8 | "nonebot2>=2.4.0", 9 | "pydantic>=2.10.3", 10 | "pillow>=11.0.0", 11 | "nonebot-plugin-localstore>=0.7.2", 12 | "nonebot-plugin-alconna>=0.60.1", 13 | "nonebot-plugin-uninfo>=0.10.0", 14 | "nonebot-plugin-orm>=0.8.2", 15 | ] 16 | requires-python = ">=3.11,<4.0" 17 | readme = "README.md" 18 | license = { file = "LICENSE" } 19 | keywords = ["nonebot2"] 20 | 21 | 22 | [project.urls] 23 | homepage = "https://github.com/kexue-z/nonebot-plugin-setu-now" 24 | 25 | [dependency-groups] 26 | dev = [ 27 | "greenlet>=3.2.4", 28 | "nb-cli>=1.4.2", 29 | "nonebot-adapter-onebot>=2.4.6", 30 | "nonebot-plugin-orm[default]>=0.8.2", 31 | "nonebot2[fastapi]>=2.4.3", 32 | ] 33 | 34 | 35 | [build-system] 36 | requires = ["hatchling"] 37 | build-backend = "hatchling.build" 38 | 39 | 40 | [tool.ruff] 41 | line-length = 88 42 | target-version = "py39" 43 | 44 | 45 | [tool.ruff.format] 46 | line-ending = "lf" 47 | 48 | 49 | [tool.ruff.lint] 50 | select = [ 51 | "F", # Pyflakes 52 | "W", # pycodestyle warnings 53 | "E", # pycodestyle errors 54 | "I", # isort 55 | "UP", # pyupgrade 56 | "ASYNC", # flake8-async 57 | "C4", # flake8-comprehensions 58 | "T10", # flake8-debugger 59 | "T20", # flake8-print 60 | "PYI", # flake8-pyi 61 | "PT", # flake8-pytest-style 62 | "Q", # flake8-quotes 63 | "TID", # flake8-tidy-imports 64 | "RUF", # Ruff-specific rules 65 | ] 66 | ignore = [ 67 | "E402", # module-import-not-at-top-of-file 68 | "UP037", # quoted-annotation 69 | "RUF001", # ambiguous-unicode-character-string 70 | "RUF002", # ambiguous-unicode-character-docstring 71 | "RUF003", # ambiguous-unicode-character-comment 72 | ] 73 | 74 | [tool.ruff.lint.isort] 75 | force-sort-within-sections = true 76 | known-first-party = ["nonebot_plugin_setu_now"] 77 | extra-standard-library = ["typing_extensions"] 78 | 79 | 80 | [tool.ruff.lint.flake8-pytest-style] 81 | fixture-parentheses = false 82 | mark-parentheses = false 83 | 84 | 85 | [tool.ruff.lint.pyupgrade] 86 | keep-runtime-typing = true 87 | 88 | 89 | [tool.nonebot] 90 | plugins = ["nonebot_plugin_setu_now"] 91 | adapters = [ 92 | { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" }, 93 | ] 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *DS_Store* 2 | test 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | .pdm-python 133 | -------------------------------------------------------------------------------- /nonebot_plugin_setu_now/data_source.py: -------------------------------------------------------------------------------- 1 | from asyncio import gather 2 | from pathlib import Path 3 | from typing import Callable 4 | 5 | from httpx import AsyncClient 6 | from nonebot.log import logger 7 | import nonebot_plugin_localstore as store 8 | 9 | from .config import API_URL, PROXY, REVERSE_PROXY, SETU_SIZE 10 | from .models import Setu, SetuApiData, SetuNotFindError 11 | from .utils import download_pic 12 | 13 | CACHE_PATH = Path(store.get_cache_dir("nonebot_plugin_setu_now")) 14 | if not CACHE_PATH.exists(): 15 | logger.info("Creating setu cache floder") 16 | CACHE_PATH.mkdir(parents=True, exist_ok=True) 17 | 18 | 19 | class SetuHandler: 20 | def __init__( 21 | self, 22 | key: str, 23 | tags: list[str], 24 | r18: bool, 25 | num: int, 26 | handler: Callable, 27 | excludeAI: bool = False, 28 | ) -> None: 29 | self.key = key 30 | self.tags = tags 31 | self.r18 = r18 32 | self.num = num 33 | self.api_url = API_URL 34 | self.size = SETU_SIZE 35 | self.proxy = PROXY 36 | self.reverse_proxy_url = REVERSE_PROXY 37 | self.handler = handler 38 | self.setu_instance_list: list[Setu] = [] 39 | self.excludeAI = excludeAI 40 | 41 | async def refresh_api_info(self): 42 | data = { 43 | "keyword": self.key, 44 | "tag": self.tags, 45 | "r18": self.r18, 46 | "proxy": self.reverse_proxy_url, 47 | "num": self.num, 48 | "size": self.size, 49 | "excludeAI": self.excludeAI, 50 | } 51 | headers = {"Content-Type": "application/json"} 52 | 53 | async with AsyncClient(proxy=self.proxy) as client: 54 | res = await client.post( 55 | self.api_url, json=data, headers=headers, timeout=60 56 | ) 57 | data = res.json() 58 | setu_api_data_instance = SetuApiData(**data) 59 | if len(setu_api_data_instance.data) == 0: 60 | raise SetuNotFindError() 61 | logger.debug(f"API Responsed {len(setu_api_data_instance.data)} image") 62 | for i in setu_api_data_instance.data: 63 | self.setu_instance_list.append(Setu(data=i)) 64 | 65 | async def prep_handler(self, setu: Setu): 66 | setu.img = await download_pic( 67 | url=setu.urls[SETU_SIZE], 68 | proxy=self.proxy, 69 | file_mode=True, 70 | file_name=f"{setu.pid}.{setu.ext}", 71 | ) 72 | await self.handler(setu) 73 | 74 | async def process_request(self): 75 | await self.refresh_api_info() 76 | task_list = [] 77 | for i in self.setu_instance_list: 78 | task_list.append(self.prep_handler(i)) 79 | await gather(*task_list) 80 | -------------------------------------------------------------------------------- /nonebot_plugin_setu_now/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | import time 4 | from typing import Optional 5 | 6 | import anyio 7 | from httpx import AsyncClient 8 | from nonebot.log import logger 9 | import nonebot_plugin_localstore as store 10 | 11 | from .config import SEND_INTERVAL, SETU_PATH 12 | from .perf_timer import PerfTimer 13 | 14 | 15 | async def download_pic( 16 | url: str, proxy: Optional[str] = None, file_mode=False, file_name="" 17 | ) -> Optional[Path]: 18 | headers = { 19 | "Referer": "https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index", 20 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) " 21 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", 22 | } 23 | download_timer = PerfTimer.start("Image download") 24 | image_path = ( 25 | store.get_cache_file("nonebot_plugin_setu_now", file_name) 26 | if SETU_PATH is None 27 | else Path(SETU_PATH, file_name) 28 | ) 29 | client = AsyncClient(proxy=proxy, timeout=5) 30 | try: 31 | async with client.stream( 32 | method="GET", url=url, headers=headers, timeout=15 33 | ) as response: 34 | if response.status_code != 200: 35 | logger.warning( 36 | f"Image respond status code error: {response.status_code}" 37 | ) 38 | raise ValueError( 39 | f"Image respond status code error: {response.status_code}" 40 | ) 41 | async with await anyio.open_file(image_path, "wb") as f: 42 | async for chunk in response.aiter_bytes(): 43 | await f.write(chunk) 44 | except Exception: 45 | logger.warning(f"Image download failed: {url}") 46 | return None 47 | finally: 48 | await client.aclose() 49 | download_timer.stop() 50 | logger.debug(f"Image download success: {image_path}") 51 | return image_path 52 | 53 | 54 | # async def send_forward_msg( 55 | # bot: Bot, 56 | # event: GroupMessageEvent, 57 | # name: str, 58 | # uin: str, 59 | # msgs: list[Message], 60 | # ): 61 | # """ 62 | # :说明: `send_forward_msg` 63 | # > 发送合并转发消息 64 | 65 | # :参数: 66 | # * `bot: Bot`: bot 实例 67 | # * `event: GroupMessageEvent`: 群聊事件 68 | # * `name: str`: 名字 69 | # * `uin: str`: qq号 70 | # * `msgs: List[Message]`: 消息列表 71 | # """ 72 | 73 | # def to_json(msg: Message): 74 | # return {"type": "node", "data": {"name": name, "uin": uin, "content": msg}} 75 | 76 | # messages = [to_json(msg) for msg in msgs] 77 | # await bot.call_api( 78 | # "send_group_forward_msg", group_id=event.group_id, messages=messages 79 | # ) 80 | 81 | 82 | class SpeedLimiter: 83 | def __init__(self) -> None: 84 | self.send_success_time = 0 85 | 86 | def send_success(self) -> None: 87 | self.send_success_time = time.time() 88 | 89 | async def async_speedlimit(self): 90 | if (delay_time := time.time() - self.send_success_time) < SEND_INTERVAL: 91 | delay_time = round(delay_time, 2) 92 | logger.debug(f"Speed limit: Asyncio sleep {delay_time}s") 93 | await asyncio.sleep(delay_time) 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
4 | 5 |