├── pyproject.toml ├── .github └── workflows │ ├── publish.yml │ ├── bulid.yml │ └── publish-test.yml ├── LICENCE ├── setup.py ├── .gitignore ├── README.md └── nonebot_plugin_epicfree ├── __init__.py └── data_source.py /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nonebot_plugin_epicfree" 3 | version = "0.2.6" 4 | description = "EpicGameStore free games promotions plugin for NoneBot2" 5 | authors = ["monsterxcn "] 6 | documentation = "https://github.com/monsterxcn/nonebot_plugin_epicfree#readme" 7 | license = "MIT" 8 | homepage = "https://github.com/monsterxcn/nonebot_plugin_epicfree" 9 | readme = "README.md" 10 | keywords = ["nonebot", "nonebot2", "epic", "free"] 11 | 12 | [tool.poetry.dependencies] 13 | python = ">=3.8,<4.0" 14 | httpx = ">=0.20.0, <1.0.0" 15 | nonebot2 = ">=2.0.0b3" 16 | nonebot-adapter-onebot = ">=2.0.0b1" 17 | nonebot-plugin-apscheduler = ">=0.1.0" 18 | pytz = "*" 19 | 20 | [tool.poetry.dev-dependencies] 21 | 22 | [build-system] 23 | requires = ["poetry-core>=1.0.0"] 24 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # https://clownote.github.io/2021/01/16/blog/PyPiGitHubActions/ 2 | 3 | name: Publish Python 🐍 distributions 📦 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-n-publish: 10 | name: 🐍📦 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Set up Python 3.7 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.7 18 | - name: Install pypa/build 19 | run: >- 20 | python -m 21 | pip install 22 | build 23 | --user 24 | - name: Build a binary wheel and a source tarball 25 | run: >- 26 | python -m 27 | build 28 | --sdist 29 | --wheel 30 | --outdir dist/ 31 | . 32 | - name: Publish distribution 📦 to PyPI 33 | uses: pypa/gh-action-pypi-publish@master 34 | with: 35 | password: ${{ secrets.PYPI_API_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/bulid.yml: -------------------------------------------------------------------------------- 1 | name: Build distributions 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: Build distributions 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Set up Python 3.7 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.7 16 | - name: Install pypa/build 17 | run: >- 18 | python -m 19 | pip install 20 | build 21 | --user 22 | - name: Build binary wheel and source tarball 23 | run: >- 24 | python -m 25 | build 26 | --sdist 27 | --wheel 28 | --outdir dist/ 29 | . 30 | - name: Publish distribution 31 | uses: actions/upload-artifact@v2 32 | with: 33 | name: binary wheel and source tarball 34 | path: | 35 | dist/*.whl 36 | dist/*.tar.gz 37 | if-no-files-found: error 38 | -------------------------------------------------------------------------------- /.github/workflows/publish-test.yml: -------------------------------------------------------------------------------- 1 | # https://clownote.github.io/2021/01/16/blog/PyPiGitHubActions/ 2 | 3 | name: Publish distributions 📦 Test 🚚 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-n-publish-test: 10 | name: 📦🚚 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Set up Python 3.7 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.7 18 | - name: Install pypa/build 19 | run: >- 20 | python -m 21 | pip install 22 | build 23 | --user 24 | - name: Build a binary wheel and a source tarball 25 | run: >- 26 | python -m 27 | build 28 | --sdist 29 | --wheel 30 | --outdir dist/ 31 | . 32 | - name: Publish distribution 📦 to Test PyPI 33 | uses: pypa/gh-action-pypi-publish@master 34 | with: 35 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 36 | repository_url: https://test.pypi.org/legacy/ -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 monsterxcn 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. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="nonebot_plugin_epicfree", 8 | version="0.2.6", 9 | author="monsterxcn", 10 | author_email="monsterxcn@gmail.com", 11 | description="EpicGameStore free games promotions plugin for NoneBot2", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/monsterxcn/nonebot_plugin_epicfree", 15 | project_urls={ 16 | "Bug Tracker": "https://github.com/monsterxcn/nonebot_plugin_epicfree/issues", 17 | }, 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | ], 23 | packages=["nonebot_plugin_epicfree"], 24 | python_requires=">=3.8,<4.0", 25 | install_requires=[ 26 | "nonebot2>=2.0.0b3", 27 | "httpx>=0.20.0,<1.0.0", 28 | "nonebot-adapter-onebot>=2.0.0b1", 29 | "nonebot-plugin-apscheduler>=0.1.0", 30 | "pytz" 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | poetry.lock 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/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

NoneBot Plugin EpicFree


2 | 3 | 4 |

🤖 用于获取 Epic 限免游戏资讯的 NoneBot2 插件


5 | 6 | 7 |

8 | 9 | actions 10 | 11 | 12 | license 13 | 14 | 15 | pypi 16 | 17 | python
18 |


19 | 20 | 21 | **安装方法** 22 | 23 | 24 | 使用以下命令之一快速安装(若配置了 PyPI 镜像,你可能无法及时检索到插件最新版本): 25 | 26 | 27 | ``` zsh 28 | nb plugin install nonebot_plugin_epicfree 29 | 30 | pip install --upgrade nonebot_plugin_epicfree 31 | ``` 32 | 33 | 34 | 重启 Bot 即可体验此插件。 35 | 36 | 37 |
关于 NoneBot2 及相关依赖版本
38 | 39 | 40 | 在已淘汰的 NoneBot2 适配器 [nonebot-adapter-cqhttp](https://pypi.org/project/nonebot-adapter-cqhttp/) 下,切记不要使用 `pip` 或 `nb_cli` 安装此插件。通过拷贝文件夹 `nonebot_plugin_epicfree` 至 NoneBot2 插件目录、手动安装 `nonebot-plugin-apscheduler` 和 `httpx` 依赖的方式仍可正常启用此插件。在未来某个版本会完全移除该适配器支持,请尽快升级至 [nonebot-adapter-onebot](https://pypi.org/project/nonebot-adapter-onebot/)。 41 | 42 | 43 |
44 | 45 | 46 |
关于 go-cqhttp 版本
47 | 48 | 49 | 插件发送消息依赖 [@Mrs4s/go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 的合并转发接口,如需启用私聊响应请务必安装 [v1.0.0-rc2](https://github.com/Mrs4s/go-cqhttp/releases/tag/v1.0.0-rc2) 以上版本的 go-cqhttp。 50 | 51 | 52 |
53 | 54 | 55 | **使用方法** 56 | 57 | 58 | ```python 59 | # nonebot_plugin_epicfree/__init__.py#L27 60 | epic_matcher = on_regex(r"^(epic)?喜(加|\+|+)(一|1)$", priority=2, flags=IGNORECASE) 61 | 62 | # nonebot_plugin_epicfree/__init__.py#L39 63 | sub_matcher = on_regex(r"^喜(加|\+|+)(一|1)(私聊)?订阅(删除|取消)?$", priority=1) 64 | ``` 65 | 66 | 67 | - 发送「喜加一」查找限免游戏 68 | - 发送「喜加一订阅」订阅游戏资讯 69 | - 发送「喜加一订阅删除」取消订阅游戏资讯 70 | 71 | 72 | *\* 插件响应基于正则匹配,所以,甚至「EpIc喜+1」这样的指令都可用!* 73 | 74 | 75 | **环境变量** 76 | 77 | 78 | ``` 79 | RESOURCES_DIR="/data/bot/resources" 80 | EPIC_SCHEDULER="8 8 8" 81 | ``` 82 | 83 | 84 | 限免游戏资讯订阅功能默认在机器人根目录下 `/data/epicfree` 文件夹内生成配置文件。定义 `RESOURCES_DIR` 环境变量即可指定用于存放订阅配置的文件夹,填写包含 `epicfree` 文件夹的 **父级文件夹** 路径即可。如果是 Windows 系统应写成类似 `D:/path/to/resources_dir` 的格式。 85 | 86 | 限免游戏资讯订阅默认 08:08:08 发送(如果当天的游戏已经推送过则不产生推送),定义 `EPIC_SCHEDULER` 环境变量即可指定推送时间,该配置的三个数字依次代表 `hour` `minute` `second`。 87 | 88 | 89 | **特别鸣谢** 90 | 91 | 92 | [@nonebot/nonebot2](https://github.com/nonebot/nonebot2/) | [@Mrs4s/go-cqhttp](https://github.com/Mrs4s/go-cqhttp) | [@DIYgod/RSSHub](https://github.com/DIYgod/RSSHub) | [@SD4RK/epicstore_api](https://github.com/SD4RK/epicstore_api) 93 | 94 | 95 | > 作者是 NoneBot2 新手,代码写的较为粗糙,欢迎提出修改意见或加入此插件开发!溜了溜了... 96 | -------------------------------------------------------------------------------- /nonebot_plugin_epicfree/__init__.py: -------------------------------------------------------------------------------- 1 | from re import IGNORECASE 2 | from traceback import format_exc 3 | from typing import Dict 4 | 5 | from nonebot import get_bot, get_driver, on_regex, require 6 | from nonebot.log import logger 7 | from nonebot.typing import T_State 8 | 9 | try: 10 | from nonebot.adapters.onebot.v11 import Bot, Event, Message # type: ignore 11 | from nonebot.adapters.onebot.v11.event import ( # type: ignore 12 | GroupMessageEvent, 13 | MessageEvent, 14 | ) 15 | except ImportError: 16 | from nonebot.adapters.cqhttp import Bot, Event, Message # type: ignore 17 | from nonebot.adapters.cqhttp.event import ( # type: ignore 18 | GroupMessageEvent, 19 | MessageEvent, 20 | ) 21 | 22 | require("nonebot_plugin_apscheduler") 23 | from nonebot_plugin_apscheduler import scheduler # noqa: E402 24 | 25 | from .data_source import check_push, get_epic_free, subscribe_helper # noqa: E402 26 | 27 | epic_matcher = on_regex(r"^(epic)?喜(加|\+|+)(一|1)$", priority=2, flags=IGNORECASE) 28 | 29 | 30 | @epic_matcher.handle() 31 | async def query_handle(bot: Bot, event: Event): 32 | free = await get_epic_free() 33 | if isinstance(event, GroupMessageEvent): 34 | await bot.send_group_forward_msg(group_id=event.group_id, messages=free) # type: ignore 35 | else: 36 | await bot.send_private_forward_msg(user_id=event.user_id, messages=free) # type: ignore 37 | 38 | 39 | sub_matcher = on_regex(r"^喜(加|\+|+)(一|1)(私聊)?订阅(删除|取消)?$", priority=1) 40 | 41 | 42 | @sub_matcher.handle() 43 | async def sub_handle(bot: Bot, event: MessageEvent, state: T_State): 44 | msg = event.get_plaintext() 45 | state["action"] = "删除" if any(s in msg for s in ["删除", "取消"]) else "启用" 46 | if isinstance(event, GroupMessageEvent): 47 | if ( 48 | (event.sender.role not in ["admin", "owner"]) 49 | and (event.get_user_id() not in bot.config.superusers) 50 | ) or "私聊" in msg: 51 | # 普通群员只会启用私聊订阅 52 | state["sub_type"] = "私聊" 53 | else: 54 | # 管理员用户、主人用户询问需要私聊订阅还是群聊订阅 55 | pass 56 | else: 57 | state["sub_type"] = "私聊" 58 | 59 | 60 | @sub_matcher.got( 61 | "sub_type", prompt=Message.template("回复「私聊」{action}私聊订阅,回复其他内容{action}群聊订阅:") 62 | ) 63 | async def subEpic(bot: Bot, event: MessageEvent, state: T_State): 64 | if any("私聊" in i for i in [event.get_plaintext().strip(), state["sub_type"]]): 65 | state["target_id"] = event.get_user_id() 66 | state["sub_type"] = "私聊" 67 | else: 68 | state["target_id"] = str(event.group_id) # type: ignore 69 | state["sub_type"] = "群聊" 70 | msg = await subscribe_helper(state["action"], state["sub_type"], state["target_id"]) 71 | await sub_matcher.finish(str(msg)) 72 | 73 | 74 | EPIC_SCHEDULER = str(getattr(get_driver().config, "epic_scheduler", "8 8 8")) 75 | hour, minute, second = EPIC_SCHEDULER.split(" ") 76 | 77 | 78 | @scheduler.scheduled_job("cron", hour=hour, minute=minute, second=second) 79 | async def epic_subscribe(): 80 | bot = get_bot() 81 | subscriber = await subscribe_helper() 82 | msg_list = await get_epic_free() 83 | if not check_push(msg_list): 84 | return 85 | try: 86 | assert isinstance(subscriber, Dict) 87 | for group in subscriber["群聊"]: 88 | await bot.send_group_forward_msg(group_id=group, messages=msg_list) 89 | for private in subscriber["私聊"]: 90 | await bot.send_private_forward_msg(user_id=private, messages=msg_list) 91 | except Exception as e: 92 | logger.error(f"Epic 限免游戏资讯定时任务出错 {e.__class__.__name__}\n{format_exc()}") 93 | -------------------------------------------------------------------------------- /nonebot_plugin_epicfree/data_source.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | from pathlib import Path 4 | from traceback import format_exc 5 | from typing import Dict, List, Literal, Union 6 | 7 | from httpx import AsyncClient 8 | from pytz import timezone 9 | 10 | from nonebot import get_driver 11 | from nonebot.log import logger 12 | 13 | try: 14 | from nonebot.adapters.onebot.v11 import Message, MessageSegment # type: ignore 15 | except ImportError: 16 | from nonebot.adapters.cqhttp import Message, MessageSegment # type: ignore 17 | 18 | RES_PARENT = getattr(get_driver().config, "resources_dir", None) 19 | if RES_PARENT and Path(RES_PARENT).exists(): 20 | res_path = Path(RES_PARENT) / "epicfree" 21 | else: 22 | res_path = Path("data/epicfree") 23 | res_path.mkdir(parents=True, exist_ok=True) 24 | CACHE = res_path / "status.json" 25 | PUSHED = res_path / "last_pushed.json" 26 | 27 | 28 | async def subscribe_helper( 29 | method: Literal["读取", "启用", "删除"] = "读取", sub_type: str = "", subject: str = "" 30 | ) -> Union[Dict, str]: 31 | """写入与读取订阅配置""" 32 | 33 | if CACHE.exists(): 34 | status_data = json.loads(CACHE.read_text(encoding="UTF-8")) 35 | else: 36 | status_data = {"群聊": [], "私聊": []} 37 | CACHE.write_text( 38 | json.dumps(status_data, ensure_ascii=False, indent=2), encoding="UTF-8" 39 | ) 40 | # 读取时,返回订阅状态字典 41 | if method == "读取": 42 | return status_data 43 | # 启用订阅时,将新的用户按类别写入至指定数组 44 | elif method == "启用": 45 | if subject in status_data[sub_type]: 46 | return f"{sub_type}{subject} 已经订阅过 Epic 限免游戏资讯了哦!" 47 | status_data[sub_type].append(subject) 48 | # 删除订阅 49 | elif method == "删除": 50 | if subject not in status_data[sub_type]: 51 | return f"{sub_type}{subject} 未曾订阅过 Epic 限免游戏资讯!" 52 | status_data[sub_type].remove(subject) 53 | try: 54 | CACHE.write_text( 55 | json.dumps(status_data, ensure_ascii=False, indent=2), encoding="UTF-8" 56 | ) 57 | return f"{sub_type}{subject} Epic 限免游戏资讯订阅已{method}!" 58 | except Exception as e: 59 | logger.error(f"写入 Epic 订阅 JSON 错误 {e.__class__.__name__}\n{format_exc()}") 60 | return f"{sub_type}{subject} Epic 限免游戏资讯订阅{method}失败惹.." 61 | 62 | 63 | async def query_epic_api() -> List: 64 | """ 65 | 获取所有 Epic Game Store 促销游戏 66 | 67 | 参考 RSSHub ``/epicgames`` 路由 https://github.com/DIYgod/RSSHub/blob/master/lib/v2/epicgames/index.js 68 | """ 69 | 70 | async with AsyncClient(proxies={"all://": None}) as client: 71 | try: 72 | res = await client.get( 73 | "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions", 74 | params={"locale": "zh-CN", "country": "CN", "allowCountries": "CN"}, 75 | headers={ 76 | "Referer": "https://www.epicgames.com/store/zh-CN/", 77 | "Content-Type": "application/json; charset=utf-8", 78 | "User-Agent": ( 79 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" 80 | " (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36" 81 | ), 82 | }, 83 | timeout=10.0, 84 | ) 85 | res_json = res.json() 86 | return res_json["data"]["Catalog"]["searchStore"]["elements"] 87 | except Exception as e: 88 | logger.error(f"请求 Epic Store API 错误 {e.__class__.__name__}\n{format_exc()}") 89 | return [] 90 | 91 | 92 | async def get_epic_free() -> List[MessageSegment]: 93 | """ 94 | 获取 Epic Game Store 免费游戏信息 95 | 96 | 参考 pip 包 epicstore_api 示例 https://github.com/SD4RK/epicstore_api/blob/master/examples/free_games_example.py 97 | """ 98 | 99 | games = await query_epic_api() 100 | if not games: 101 | return [ 102 | MessageSegment.node_custom( 103 | user_id=2854196320, 104 | nickname="EpicGameStore", 105 | content=Message("Epic 可能又抽风啦,请稍后再试("), 106 | ) 107 | ] 108 | else: 109 | logger.debug( 110 | f"获取到 {len(games)} 个游戏数据:\n{('、'.join(game['title'] for game in games))}" 111 | ) 112 | game_cnt, msg_list = 0, [] 113 | for game in games: 114 | game_name = game.get("title", "未知") 115 | try: 116 | if not game.get("promotions"): 117 | continue 118 | game_promotions = game["promotions"]["promotionalOffers"] 119 | upcoming_promotions = game["promotions"]["upcomingPromotionalOffers"] 120 | original_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] 121 | discount_price = game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] 122 | if not game_promotions: 123 | if upcoming_promotions: 124 | logger.info(f"跳过即将推出免费游玩的游戏:{game_name}({discount_price})") 125 | continue # 仅返回正在推出免费游玩的游戏 126 | elif game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] != "0": 127 | logger.info(f"跳过促销但不免费的游戏:{game_name}({discount_price})") 128 | continue 129 | # 处理游戏预览图 130 | for image in game["keyImages"]: 131 | # 修复部分游戏无法找到图片 132 | # https://github.com/HibiKier/zhenxun_bot/commit/92e60ba141313f5b28f89afdfe813b29f13468c1 133 | if image.get("url") and image["type"] in [ 134 | "Thumbnail", 135 | "VaultOpened", 136 | "DieselStoreFrontWide", 137 | "OfferImageWide", 138 | ]: 139 | msg_list.append( 140 | MessageSegment.node_custom( 141 | user_id=2854196320, 142 | nickname="EpicGameStore", 143 | content=Message(MessageSegment.image(image["url"])), 144 | ) 145 | ) 146 | break 147 | # 处理游戏发行信息 148 | game_dev, game_pub = game["seller"]["name"], game["seller"]["name"] 149 | for pair in game["customAttributes"]: 150 | if pair["key"] == "developerName": 151 | game_dev = pair["value"] 152 | elif pair["key"] == "publisherName": 153 | game_pub = pair["value"] 154 | dev_com = f"{game_dev} 开发、" if game_dev != game_pub else "" 155 | companies = ( 156 | f"由 {dev_com}{game_pub} 发行," 157 | if game_pub != "Epic Dev Test Account" 158 | else "" 159 | ) 160 | # 处理游戏限免结束时间 161 | date_rfc3339 = game_promotions[0]["promotionalOffers"][0]["endDate"] 162 | end_date = ( 163 | datetime.strptime(date_rfc3339, "%Y-%m-%dT%H:%M:%S.%f%z") 164 | .astimezone(timezone("Asia/Shanghai")) 165 | .strftime("%m {m} %d {d} %H:%M") 166 | .format(m="月", d="日") 167 | ) 168 | # 处理游戏商城链接(API 返回不包含游戏商店 URL,依经验自行拼接 169 | if game.get("url"): 170 | game_url = game["url"] 171 | else: 172 | slugs = ( 173 | [ 174 | x["pageSlug"] 175 | for x in game.get("offerMappings", []) 176 | if x.get("pageType") == "productHome" 177 | ] 178 | + [ 179 | x["pageSlug"] 180 | for x in game.get("catalogNs", {}).get("mappings", []) 181 | if x.get("pageType") == "productHome" 182 | ] 183 | + [ 184 | x["value"] 185 | for x in game.get("customAttributes", []) 186 | if "productSlug" in x.get("key") 187 | ] 188 | ) 189 | game_url = "https://store.epicgames.com/zh-CN{}".format( 190 | f"/p/{slugs[0]}" if len(slugs) else "" 191 | ) 192 | game_cnt += 1 193 | msg_list.extend( 194 | [ 195 | MessageSegment.node_custom( 196 | user_id=2854196320, 197 | nickname="EpicGameStore", 198 | content=Message(MessageSegment.text(game_url)), 199 | ), 200 | MessageSegment.node_custom( 201 | user_id=2854196320, 202 | nickname="EpicGameStore", 203 | content=Message( 204 | "{} ({})\n\n{}\n\n游戏{}" "将在 {} 结束免费游玩,戳上方链接领取吧~".format( 205 | game_name, 206 | original_price, 207 | game["description"], 208 | companies, 209 | end_date, 210 | ) 211 | ), 212 | ), 213 | ] 214 | ) 215 | except (AttributeError, IndexError, TypeError): 216 | logger.debug(f"处理游戏 {game_name} 时遇到应该忽略的错误\n{format_exc()}") 217 | pass 218 | except Exception as e: 219 | logger.error(f"组织 Epic 订阅消息错误 {e.__class__.__name__}\n{format_exc()}") 220 | # 返回整理为 CQ 码的消息字符串 221 | msg_list.insert( 222 | 0, 223 | MessageSegment.node_custom( 224 | user_id=2854196320, 225 | nickname="EpicGameStore", 226 | content=Message(f"{game_cnt} 款游戏现在免费!" if game_cnt else "暂未找到正在促销的游戏..."), 227 | ), 228 | ) 229 | return msg_list 230 | 231 | 232 | def check_push(msg: List[MessageSegment]) -> bool: 233 | """检查是否需要重新推送""" 234 | 235 | last_text: List[str] = ( 236 | json.loads(PUSHED.read_text(encoding="UTF-8")) if PUSHED.exists() else [] 237 | ) 238 | _msg_text = [x.data["content"][0].data.get("text") for x in msg] 239 | this_text = [s for s in _msg_text if s] 240 | 241 | need_push = this_text != last_text 242 | if need_push: 243 | PUSHED.write_text( 244 | json.dumps(this_text, ensure_ascii=False, indent=2), encoding="UTF-8" 245 | ) 246 | return need_push 247 | --------------------------------------------------------------------------------