From 89871c6b8dbd50d25494d2379d9c2a7a189abcdf Mon Sep 17 00:00:00 2001 From: gakkiyomi Date: Sun, 3 Dec 2023 20:37:59 +0800 Subject: [PATCH] Develop (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * release v2.0.0 * refacotr: rename cli_config => options * feat: 撤回上条消息 #67 * chore: ws支持多个 * 项目名变更 * optimze: 优雅退出 * 账号多开 * doc: 文档添加 --- README.md | 21 +++--- config.ini | 4 +- requirements.txt | 3 +- setup.py | 24 +++---- src/api/__api__.py | 15 ---- src/api/__init__.py | 80 +++++++++++---------- src/api/article.py | 103 ++++++++++++++++++++++++++++ src/api/base.py | 45 ++++++++++++ src/api/chatroom.py | 26 +++++-- src/{core => api}/config.py | 6 +- src/api/user.py | 12 ++-- src/api/ws.py | 53 ++++++++++++++ src/core/__init__.py | 99 ++++++++++++++++---------- src/core/blacklist.py | 3 +- src/core/chatroom.py | 78 +++++++++++++-------- src/core/command.py | 83 ++++++++++++++++++---- src/core/redpacket.py | 5 +- src/core/user.py | 19 ----- src/core/websocket.py | 53 -------------- src/main.py | 24 +++++-- src/utils/{utils.py => __init__.py} | 4 ++ src/utils/version.py | 2 +- 22 files changed, 511 insertions(+), 251 deletions(-) delete mode 100644 src/api/__api__.py create mode 100644 src/api/article.py create mode 100644 src/api/base.py rename src/{core => api}/config.py (91%) create mode 100644 src/api/ws.py delete mode 100644 src/core/websocket.py rename src/utils/{utils.py => __init__.py} (92%) diff --git a/README.md b/README.md index 4518cca..6179ca4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![摸鱼派cn.png](https://b3logfile.com/file/2023/05/摸鱼派-cn-owZQT8f.png) -# pwl-chat-python +# fishpi-pyclient > 摸鱼派聊天室 python 命令行客户端 @@ -13,13 +13,13 @@ 执行 ```bash -pip install pwl-chat-python +pip install fishpi-pyclient ``` ## 运行 ```bash -pwl-chat-python -u username -p password -c <两步验证码> +fishpi-pyclient -u username -p password -c <两步验证码> ``` ## 调试 @@ -32,18 +32,18 @@ python core.py ## 功能 +- 🥷 账号多开 + - 一键切换 + - 更多功能请期待 - 💬 聊天模式 - 💬 聊天吹水 - 🤖️ 自动复读 - 🤖️ 自动领取昨日奖励 + - 🌛 发送清风明月 + - 聊天室消息撤回 - 🧠 自言自语 - 自定义语句池 - 定时发送 - - 🧧 自动化抢红包(脚本哥) - - 自定义抢红包延时 - - 心跳红包防止踩坑 - - 心跳红包风险预测 - - ~~猜拳红包百分百胜率~~ - 命令模式 - 命令/聊天模式切换 - (聊天模式也可以执行命令) @@ -68,9 +68,14 @@ python core.py - 猜拳红包 - 设置抢红包等待时间 - 抢猜拳红包最大限制 + - 🧧 自动化抢红包(脚本哥) + - 自定义抢红包延时 + - 心跳红包防止踩坑 + - 心跳红包风险预测 ## 效果 +![fenshen.png](https://file.fishpi.cn/2023/12/账号分身-0a25be81.png) ![image.png](https://file.fishpi.cn/2023/06/image-d4da9bf7.png) ![redpacket](https://file.fishpi.cn/2023/06/image-d0ad7756.png) ![image.png](https://pwl.stackoverflow.wiki/2022/01/image-f74aae7e.png) diff --git a/config.ini b/config.ini index 2ef21e3..2446181 100644 --- a/config.ini +++ b/config.ini @@ -3,7 +3,9 @@ username=xxx ; 摸鱼派用户密码 password=xxx - +;账号多开 +sockpuppet_usernames=xxx,yyy +sockpuppet_passwords=xxx_password,yyy_password [redPacket] ; 是否开启抢红包模式 openRedPacket=true diff --git a/requirements.txt b/requirements.txt index 6061e4f..da6c902 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ requests==2.22.0 websocket_client==1.2.3 schedule==1.1.0 -click==8.1.3 \ No newline at end of file +click==8.1.3 +objprint==0.2.3 \ No newline at end of file diff --git a/setup.py b/setup.py index bd0ad76..f081b47 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,11 @@ -from setuptools import setup +import io +import os +import sys +from shutil import rmtree + +from setuptools import Command, setup + +from src.utils.version import __version__ #!/usr/bin/env python # -*- coding: utf-8 -*- @@ -6,18 +13,11 @@ # Note: To use the 'upload' functionality of this file, you must: # $ pipenv install twine --dev -import io -import os -import sys -from shutil import rmtree -from src.utils.version import __version__ - -from setuptools import setup, Command # Package meta-data. -NAME = 'pwl-chat-python' +NAME = 'fishpi-pyclient' DESCRIPTION = '摸鱼派聊天室python客户端' -URL = 'https://github.com/gakkiyomi/pwl-chat-python' +URL = 'https://github.com/gakkiyomi/fishpi-pyclient' EMAIL = 'gakkiyomi@gmail.com' AUTHOR = 'gakkiyomi' REQUIRES_PYTHON = '>=3.9' @@ -25,7 +25,7 @@ # What packages are required for this module to be executed? REQUIRED = [ - 'requests', 'websocket-client', 'click', 'schedule' + 'requests', 'websocket-client', 'click', 'schedule', 'objprint' ] # What packages are optional? @@ -122,7 +122,7 @@ def run(self): }, entry_points={ 'console_scripts': [ - 'pwl-chat-python = src.main:cli', + 'fishpi-pyclient = src.main:cli', ], } diff --git a/src/api/__api__.py b/src/api/__api__.py deleted file mode 100644 index a9b061b..0000000 --- a/src/api/__api__.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- - -class Response: - def __init__(self, code: int = 0, msg: str = '', data: any = {}): - self.code = code - self.msg = msg - self.data = data - - -class Base(object): - def __init__(self, key=''): - self.set_token(key) - - def set_token(self, api_key=''): - self.api_key = api_key diff --git a/src/api/__init__.py b/src/api/__init__.py index 1b70f21..31606ef 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -1,58 +1,56 @@ # -*- coding: utf-8 -*- - -import hashlib import json -import sys +from typing import Any import requests -from src.utils.utils import HOST, UA +from src.api.base import Base +from src.utils import HOST, UA -from .__api__ import Base -from .chatroom import ChatRoom -from .user import User +from .article import ArticleAPI +from .chatroom import ChatRoomAPI +from .user import UserAPI -class FishPi(Base): - def __init__(self): - self.ws_calls = [] - self.ws = None - self.current_user = '' - self.user = User() - self.chatroom = ChatRoom() - Base.__init__(self) +class UserInfo(object): + + def __init__(self, username: str, password: str, api_key: str) -> None: + self.username = username + self.password = password + self.api_key = api_key + self.ws: dict[str, Any] = {} + self.is_online = False + + def online(self, func) -> None: + if (len(self.api_key) != 0): + API.set_token(self.api_key) + API.set_current_user(self.username) + else: + API.login(self.username, self.password) + self.api_key = API.api_key + func() + self.is_online = True - def add_listener(self, listener): - self.ws_calls.append(listener) + def offline(self) -> None: + keys = list(self.ws.keys()) + for key in keys: + self.ws[key].stop() + self.is_online = False - def set_current_user(self, username): - self.current_user = username + +class FishPi(Base): + def __init__(self): + self.sockpuppets: dict[str, UserInfo] = {} + self.user = UserAPI() + self.chatroom = ChatRoomAPI() + self.article = ArticleAPI() + super().__init__(self) def set_token(self, key): - Base.set_token(self, key) + super().set_token(key) self.user.set_token(key) self.chatroom.set_token(key) - - def login(self, username: str, password: str, mfa_code='') -> bool: - params = { - 'nameOrEmail': username, - 'userPassword': hashlib.md5(str(password).encode('utf-8')).hexdigest(), - 'mfaCode': mfa_code - } - res = requests.post(f"{HOST}/api/getKey", - json=params, headers={'User-Agent': UA}) - rsp = json.loads(res.text) - if rsp['code'] == 0: - self.set_token(rsp['Key']) - self.set_current_user(username) - print(f'登陆成功! 更多功能与趣味游戏请访问网页端: {HOST}') - return True - elif rsp['code'] == -1 and rsp['msg'] == '两步验证失败,请填写正确的一次性密码': - print("请输入两步验证码:") - return False - else: - print(f"登陆失败: {rsp['msg']}") - sys.exit() + self.article.set_token(key) def get_breezemoons(self, page: int = 1, size: int = 10) -> dict | None: res = requests.get( diff --git a/src/api/article.py b/src/api/article.py new file mode 100644 index 0000000..2414b27 --- /dev/null +++ b/src/api/article.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +import json +from enum import Enum +from typing import Any + +import requests + +from src.api import Base +from src.utils import HOST, UA + + +class ArticleType(Enum): + RECENT = 'recent' + HOT = RECENT+'/hot' + GOOD = RECENT+'/good' + REPLY = RECENT+'/reply' + + +class Article(object): + def __init__(self, *args: Any, **kwargs: Any) -> None: + for dict in args: + for key in dict: + setattr(self, key, dict[key]) + for key in kwargs: + setattr(self, key, kwargs[key]) + + def vote(self, api) -> None: + api.vote_for_article(self.oId) + + def thanks(self, api) -> None: + api.thanks_for_article(self.oId) + + def comment(self, api, comment: str) -> None: + api.comment_article(self.oId, comment) + + +class ArticleAPI(Base): + def vote_for_article(self, article_id: str) -> None: + if self.api_key == '': + return None + resp = requests.post( + f'{HOST}/vote/up/article', headers={'User-Agent': UA}, json={ + 'apiKey': self.api_key, + 'dataId': article_id + }) + if resp.status_code == 200: + data = json.loads(resp.text) + if 'code' in data and data['code'] == 0: + if data['type'] == -1: + print('点赞成功') + else: + print('取消点赞') + else: + print('点赞失败: ' + data['msg']) + else: + print('点赞失败') + + def thanks_for_article(self, article_id: str) -> None: + res = requests.post(f'{HOST}/article/thank?articleId={article_id}', headers={'User-Agent': UA}, json={ + 'apiKey': self.api_key + }) + response = json.loads(res.text) + if 'code' in response and response['code'] == 0: + print('感谢文章成功') + else: + print('感谢文章失败: ' + response['msg']) + + def list_articles(self, type: ArticleType = ArticleType.RECENT, page: int = 1, size: int = 20) -> dict: + res = requests.get( + f'{HOST}/api/articles/{type}?p={page}&size={size}', headers={'User-Agent': UA}, json={ + 'apiKey': self.api_key + }) + response = json.loads(res.text) + if 'code' in response and response['code'] == 0: + return response + else: + print('获取帖子列表失败: ' + response['msg']) + + def get_article(self, article_id: str) -> Article: + res = requests.get( + f'{HOST}/api/article/{article_id}', headers={'User-Agent': UA}, json={ + 'apiKey': self.api_key + }) + response = json.loads(res.text) + if 'code' in response and response['code'] == 0: + return Article(response['data']) + else: + print('获取帖子详情失败: ' + response['msg']) + + def comment_article(self, article_id: str, comment: str) -> Article: + res = requests.post(f'{HOST}/comment/{article_id}', headers={'User-Agent': UA}, json={ + 'apiKey': self.api_key, + 'articleId': article_id, + 'commentAnonymous': False, + 'commentVisible': False, + 'commentContent': comment + }) + response = json.loads(res.text) + if 'code' in response and response['code'] == 0: + print('评论成功') + else: + print('评论失败: ' + response['msg']) diff --git a/src/api/base.py b/src/api/base.py new file mode 100644 index 0000000..705588f --- /dev/null +++ b/src/api/base.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +import hashlib +import json +import sys + +import requests + +from src.utils import HELP, HOST, UA + + +class Base(object): + def __init__(self, key=''): + self.current_user = '' + self.set_token(key) + + def set_token(self, api_key=''): + self.api_key = api_key + + def set_current_user(self, username): + self.current_user = username + + def login(self, username: str, password: str, mfa_code=''): + params = { + 'nameOrEmail': username, + 'userPassword': hashlib.md5(str(password).encode('utf-8')).hexdigest(), + 'mfaCode': mfa_code + } + res = requests.post(f"{HOST}/api/getKey", + json=params, headers={'User-Agent': UA}) + rsp = json.loads(res.text) + if rsp['code'] == 0: + self.set_token(rsp['Key']) + self.set_current_user(username) + print(f'登陆成功! 更多功能与趣味游戏请访问网页端: {HOST}') + print(HELP) + elif rsp['code'] == -1 and rsp['msg'] == '两步验证失败,请填写正确的一次性密码': + self.set_token('') + print("请输入两步验证码:") + while len(self.api_key) == 0: + code = input("") + self.login(username, password, code) + else: + print(f"登陆失败: {rsp['msg']}") + sys.exit(0) diff --git a/src/api/chatroom.py b/src/api/chatroom.py index 9dd89fe..bf3c3cf 100644 --- a/src/api/chatroom.py +++ b/src/api/chatroom.py @@ -4,14 +4,17 @@ import requests -from src.utils.utils import HOST, UA +from src.api import Base +from src.utils import HOST, UA from src.utils.version import __version__ -from .__api__ import Base from .redpacket import * -class ChatRoom(Base): +class ChatRoomAPI(Base): + + def __init__(self, last_msg_id=None): + self.last_msg_id = last_msg_id def more(self, page: int = 1) -> None | dict: if self.api_key == '': @@ -20,7 +23,7 @@ def more(self, page: int = 1) -> None | dict: headers={'User-Agent': UA}) return json.loads(resp.text) - def send(self, message: str) -> dict | None: + def send(self, message: str) -> None: if self.api_key == '': return None params = {'apiKey': self.api_key, 'content': message, @@ -31,7 +34,20 @@ def send(self, message: str) -> dict | None: if ('code' in ret_json and ret_json['code'] == -1): print(ret_json['msg']) - def send_redpacket(self, redpacket: RedPacket = RedPacket('最后的发', 128, 5, RedPacketType.RANDOM)): + def revoke(self, msg_id: str) -> None: + if self.api_key == '': + return None + params = {'apiKey': self.api_key, + 'client': f'Python/客户端v{__version__}'} + ret = requests.delete(f'{HOST}/chat-room/revoke/{msg_id}', + json=params, headers={'User-Agent': UA}) + ret_json = json.loads(ret.text) + if ('code' in ret_json and ret_json['code'] == -1): + print(ret_json["msg"]) + else: + print('撤回成功') + + def send_redpacket(self, redpacket: RedPacket = RedPacket('最后的发', 128, 5, RedPacketType.RANDOM)) -> None: content = f'[redpacket]{json.dumps(redpacket.__json__())}[/redpacket]' self.send(content) diff --git a/src/core/config.py b/src/api/config.py similarity index 91% rename from src/core/config.py rename to src/api/config.py index dd58ac6..1fc2133 100644 --- a/src/core/config.py +++ b/src/api/config.py @@ -17,6 +17,10 @@ def __init__(self, username='', password='', mfa_code=''): self.username = username self.password = password self.mfa_code = mfa_code + self.accounts: list[tuple[str, ...]] = [] + + def add_account(self, username='', password=''): + self.accounts.append((username, password)) class ChatConfig(object): @@ -40,7 +44,7 @@ def __init__(self, auth: AuthConfig = None, redpacket: RedPacketConfig = None, c self.cfg_path = cfg_path -class CliConfig(object): +class CliOptions(object): def __init__(self, username: str = '', password: str = '', code: str = '', file_path: str = None): self.username = username self.password = password diff --git a/src/api/user.py b/src/api/user.py index 53c619d..7a6549f 100644 --- a/src/api/user.py +++ b/src/api/user.py @@ -4,16 +4,14 @@ import requests -from src.utils.utils import HOST, UA +from src.api import Base +from src.utils import HOST, UA -from .__api__ import Base +class UserAPI(Base): -class User(Base): - - def __init__(self, key='', reward=False): - super().__init__(key) - self.reward = reward + def __init__(self): + self.reward = False def get_user_info(self, username: str) -> None | dict: if self.api_key == '': diff --git a/src/api/ws.py b/src/api/ws.py new file mode 100644 index 0000000..5f3c99e --- /dev/null +++ b/src/api/ws.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +import json +import ssl +import threading +from abc import ABC, abstractmethod + +import websocket + +from src.api import API + + +class WS(ABC): + def __init__(self, ws_url: str, ws_calls: list[str]) -> None: + self.ws_url = ws_url + self.ws_calls = ws_calls + + @abstractmethod + def on_open(self, obj): + pass + + def on_error(self, obj, error): + print(error) + + @abstractmethod + def on_close(self, obj, close_status_code, close_msg): + pass + + def on_message(self, obj, message): + data = json.loads(message) + for call in self.ws_calls: + call(API, data) + + def start(self): + threading.Thread(target=aysnc_start_ws, args=(self,)).start() + + def stop(self): + self.instance.close() + self.instance = None + API.sockpuppets[API.current_user].ws.pop(self.ws_url) + self.ws_calls = None + self.ws_url = None + + +def aysnc_start_ws(ws: WS): + websocket.enableTrace(False) + ws_instance = websocket.WebSocketApp(f"wss://{ws.ws_url}?apiKey={API.api_key}", + on_open=ws.on_open, + on_message=ws.on_message, + on_error=ws.on_error, + on_close=ws.on_close) + ws.instance = ws_instance + API.sockpuppets[API.current_user].ws[ws.ws_url] = ws + ws_instance.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) diff --git a/src/core/__init__.py b/src/core/__init__.py index e33bafe..e424994 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -2,17 +2,23 @@ import os import sys from abc import ABC, abstractmethod -from configparser import ConfigParser +from configparser import ConfigParser, NoOptionError from typing import Any -from src.api import FishPi +import schedule + +from src.api import FishPi, UserInfo +from src.api.config import ( + GLOBAL_CONFIG, + AuthConfig, + ChatConfig, + CliOptions, + RedPacketConfig, +) from src.core.command import init_cli -from src.core.user import check_in, login -from src.core.websocket import init_chatroom +from src.core.user import check_in -from .chatroom import init_soliloquize, listener -from .config import GLOBAL_CONFIG, AuthConfig, ChatConfig, CliConfig, RedPacketConfig -from .redpacket import render_redpacket +from .chatroom import ChatRoom, init_soliloquize class Initor(ABC): @@ -26,17 +32,17 @@ def __iter__(self): node = node.next @abstractmethod - def exec(self, api: FishPi, cli_config: CliConfig) -> None: + def exec(self, api: FishPi, options: CliOptions) -> None: pass - def init(self, api: FishPi, cli_config: CliConfig) -> None: - self.exec(api, cli_config) - self.next.init(api, cli_config) + def init(self, api: FishPi, options: CliOptions) -> None: + self.exec(api, options) + self.next.init(api, options) class FileConfigInitor(Initor): - def exec(self, api: FishPi, cli_config: CliConfig) -> None: - file_path = cli_config.file_path + def exec(self, api: FishPi, options: CliOptions) -> None: + file_path = options.file_path if file_path is None: file_path = f'{os.getcwd()}/config.ini' config = ConfigParser() @@ -51,12 +57,11 @@ def exec(self, api: FishPi, cli_config: CliConfig) -> None: GLOBAL_CONFIG.chat_config = init_chat_config(config) GLOBAL_CONFIG.cfg_path = file_path except Exception as e: - breakpoint() print(f'{file_path}配置文件不合法') class DefualtConfigInitor(Initor): - def exec(self, api: FishPi, cli_config: CliConfig) -> None: + def exec(self, api: FishPi, options: CliOptions) -> None: print("生成默认配置") GLOBAL_CONFIG.auth_config = AuthConfig() GLOBAL_CONFIG.redpacket_config = RedPacketConfig() @@ -65,7 +70,7 @@ def exec(self, api: FishPi, cli_config: CliConfig) -> None: class EnvConfigInitor(Initor): - def exec(self, api: FishPi, cli_config: CliConfig) -> None: + def exec(self, api: FishPi, options: CliOptions) -> None: GLOBAL_CONFIG.auth_config.username = os.environ.get( "FISH_PI_USERNAME", '') GLOBAL_CONFIG. auth_config.password = os.environ.get( @@ -73,53 +78,62 @@ def exec(self, api: FishPi, cli_config: CliConfig) -> None: class CilConfigInitor(Initor): - def exec(self, api: FishPi, cli_config: CliConfig) -> None: - init_userinfo_with_cli_config(cli_config) + def exec(self, api: FishPi, options: CliOptions) -> None: + init_userinfo_with_options(options) class LoginInitor(Initor): - def exec(self, api: FishPi, cli_config: CliConfig) -> None: + def exec(self, api: FishPi, options: CliOptions) -> None: if GLOBAL_CONFIG.auth_config.username is None: print('用户名不能为空') sys.exit(0) if GLOBAL_CONFIG.auth_config.password is None: print('密码不能为空') sys.exit(0) - login(api) + api.login(GLOBAL_CONFIG.auth_config.username, + GLOBAL_CONFIG.auth_config.password) + if len(GLOBAL_CONFIG.auth_config.accounts) != 0: + for account in GLOBAL_CONFIG.auth_config.accounts: + api.sockpuppets[account[0]] = UserInfo( + account[0], account[1], '') + api.sockpuppets[api.current_user] = UserInfo( + api.current_user, GLOBAL_CONFIG.auth_config.password, api.api_key) + api.sockpuppets[api.current_user].is_online = True check_in(api) class ChaRoomInitor(Initor): - def exec(self, api: FishPi, cli_config: CliConfig) -> None: - api.add_listener(listener) - api.add_listener(render_redpacket) + def exec(self, api: FishPi, options: CliOptions) -> None: init_soliloquize(api) - init_chatroom(api) + if GLOBAL_CONFIG.chat_config.soliloquize_switch: + schedule.run_pending() + chatroom = ChatRoom() + chatroom.start() class CliInitor(Initor): - def exec(self, api: FishPi, cli_config: CliConfig) -> None: + def exec(self, api: FishPi, options: CliOptions) -> None: init_cli(api) class InitChain(object): - def __init__(self, api: FishPi = None, cli_config: CliConfig = None) -> None: + def __init__(self, api: FishPi = None, options: CliOptions = None) -> None: self.head: Initor = None self.api = api - self.cli_config = cli_config + self.options = options def __call__(self, *args: Any, **kwds: Any) -> None: self.api = kwds['api'] - self.cli_config = kwds['cli_config'] + self.options = kwds['options'] self.init() def append(self, *args) -> None: curr_node = self.head - sample_generator = (i for i in args) + initors = (i for i in args) if curr_node is None: - self.head = next(sample_generator) + self.head = next(initors) curr_node = self.head - for initor in sample_generator: + for initor in initors: curr_node.next = initor curr_node = curr_node.next @@ -131,7 +145,7 @@ def init(self): LoginInitor(), ChaRoomInitor(), CliInitor()) - self.head.init(self.api, self.cli_config) + self.head.init(self.api, self.options) def int_redpacket_config(config: ConfigParser) -> RedPacketConfig: @@ -160,13 +174,24 @@ def int_redpacket_config(config: ConfigParser) -> RedPacketConfig: def init_auth_config(config: ConfigParser) -> None: GLOBAL_CONFIG.auth_config = AuthConfig(config.get('auth', 'username'), config.get('auth', 'password')) + try: + sockpuppet_usernames = config.get( + 'auth', 'sockpuppet_usernames').replace(',', ',').split(',') + sockpuppet_passwords = config.get( + 'auth', 'sockpuppet_passwords').replace(',', ',').split(',') + sockpuppets = zip(sockpuppet_usernames, sockpuppet_passwords) + for sockpuppet in sockpuppets: + GLOBAL_CONFIG.auth_config.add_account( + sockpuppet[0].strip(), sockpuppet[1].strip()) + except NoOptionError: + pass -def init_userinfo_with_cli_config(cli_config: CliConfig) -> None: - if cli_config.username is not None and cli_config.password is not None: - GLOBAL_CONFIG.auth_config.username = cli_config.username - GLOBAL_CONFIG.auth_config.password = cli_config.password - GLOBAL_CONFIG.auth_config.mfa_code = cli_config.code +def init_userinfo_with_options(options: CliOptions) -> None: + if options.username is not None and options.password is not None: + GLOBAL_CONFIG.auth_config.username = options.username + GLOBAL_CONFIG.auth_config.password = options.password + GLOBAL_CONFIG.auth_config.mfa_code = options.code def init_chat_config(config: ConfigParser) -> ChatConfig: diff --git a/src/core/blacklist.py b/src/core/blacklist.py index 2ff684d..5acfe86 100644 --- a/src/core/blacklist.py +++ b/src/core/blacklist.py @@ -3,8 +3,7 @@ import re from src.api import FishPi - -from .config import GLOBAL_CONFIG +from src.api.config import GLOBAL_CONFIG def release_someone(api: FishPi, username: str) -> None: diff --git a/src/core/chatroom.py b/src/core/chatroom.py index 8bb9f94..70c6974 100644 --- a/src/core/chatroom.py +++ b/src/core/chatroom.py @@ -4,10 +4,11 @@ import schedule -from src.api import FishPi +from src.api import API, FishPi +from src.api.config import GLOBAL_CONFIG +from src.api.ws import WS -from .config import GLOBAL_CONFIG -from .redpacket import rush_redpacket +from .redpacket import render_redpacket, rush_redpacket REPEAT_POOL = {} # 复读池 @@ -39,32 +40,53 @@ def soliloquize(api: FishPi) -> None: executor = ThreadPoolExecutor(max_workers=5) -def listener(api: FishPi, message: dict) -> None: +def render(api: FishPi, message: dict) -> None: if message["type"] == "msg": if message["content"].find("redPacket") != -1: executor.submit(rush_redpacket, api, message) else: - time = message["time"] - user = message["userName"] - user_nick_name = message["userNickname"] - if len( - GLOBAL_CONFIG.chat_config.blacklist - ) > 0 and GLOBAL_CONFIG.chat_config.blacklist.__contains__(user): - return - if user == GLOBAL_CONFIG.auth_config.username: - print(f"\t\t\t\t\t\t[{time}]") - print(f'\t\t\t\t\t\t你说: {message["md"]}') - else: - if "client" in message: - print(f'[{time}] 来自({message["client"]})') - else: - print(f"[{time}]") - if len(user_nick_name) > 0: - print(f"{user_nick_name}({user})说:") - else: - print(f"{user}说:") - print(message["md"]) - print("\r\n") - if GLOBAL_CONFIG.chat_config.repeat_mode_switch: - msg = message["md"] - repeat(api, msg) + renderChatroomMsg(api, message) + + +def renderChatroomMsg(api: FishPi, message: dict) -> None: + time = message["time"] + user = message["userName"] + user_nick_name = message["userNickname"] + if len(GLOBAL_CONFIG.chat_config.blacklist) > 0 and GLOBAL_CONFIG.chat_config.blacklist.__contains__(user): + return + if user == api.current_user: + print(f"\t\t\t\t\t\t[{time}]") + print(f'\t\t\t\t\t\t你说: {message["md"]}') + api.chatroom.last_msg_id = message['oId'] + else: + if "client" in message: + print(f'[{time}] 来自({message["client"]})') + else: + print(f"[{time}]") + if len(user_nick_name) > 0: + print(f"{user_nick_name}({user})说:") + else: + print(f"{user}说:") + print(message["md"]) + print("\r\n") + if GLOBAL_CONFIG.chat_config.repeat_mode_switch: + msg = message["md"] + repeat(api, msg) + + +class ChatRoom(WS): + WS_URL = 'fishpi.cn/chat-room-channel' + + def __init__(self) -> None: + super().__init__(ChatRoom.WS_URL, [render, render_redpacket]) + + def on_open(self, obj): + print(f'欢迎{API.current_user}进入聊天室!') + if len(GLOBAL_CONFIG.chat_config.blacklist) > 0: + print('小黑屋成员: ' + str(GLOBAL_CONFIG.chat_config.blacklist)) + + def on_error(self, obj, error): + super().on_error(obj, error) + + def on_close(self, obj, close_status_code, close_msg): + print("已经离开聊天室") diff --git a/src/core/command.py b/src/core/command.py index fc65c0e..5d3f694 100644 --- a/src/core/command.py +++ b/src/core/command.py @@ -3,9 +3,12 @@ from abc import ABC, abstractmethod from typing import Tuple -from src.api import FishPi +from objprint import op + +from src.api import FishPi, UserInfo +from src.api.config import GLOBAL_CONFIG from src.api.redpacket import RedPacket, RedPacketType, RPSRedPacket, SpecifyRedPacket -from src.utils.utils import ( +from src.utils import ( COMMAND_GUIDE, RP_RE, RP_SEND_TO_CODE_RE, @@ -14,9 +17,8 @@ ) from .blacklist import ban_someone, release_someone -from .config import GLOBAL_CONFIG +from .chatroom import ChatRoom from .user import render_online_users, render_user_info -from .websocket import chatroom_out, init_chatroom class Command(ABC): @@ -32,7 +34,8 @@ def exec(self, api: FishPi, args: Tuple[str, ...]): class DefaultCommand(Command): def exec(self, api: FishPi, args: Tuple[str, ...]): - if api.ws is not None: + curr_user = api.sockpuppets[api.current_user] + if curr_user.ws is not None: if GLOBAL_CONFIG.chat_config.answer_mode: api.chatroom.send( f"鸽 {' '.join(args)}") @@ -44,20 +47,25 @@ def exec(self, api: FishPi, args: Tuple[str, ...]): class EnterCil(Command): def exec(self, api: FishPi, args: Tuple[str, ...]): - if api.ws is None: - print("已经进入交互模式了") + curr_user = api.sockpuppets[api.current_user] + if len(curr_user.ws) == 0: + print("已在交互模式中") else: - chatroom_out(api) + keys = list(curr_user.ws.keys()) + for key in keys: + curr_user.ws[key].stop() print("进入交互模式") class EnterChatroom(Command): def exec(self, api: FishPi, args: Tuple[str, ...]): - if api.ws is None: - init_chatroom(api) + curr_user = api.sockpuppets[api.current_user] + if ChatRoom.WS_URL in curr_user.ws: + print("已在聊天室中") else: - chatroom_out(api) - init_chatroom(api) + cr = ChatRoom() + curr_user.ws[ChatRoom.WS_URL] = cr + cr.start() class AnswerMode(Command): @@ -98,6 +106,13 @@ def exec(self, api: FishPi, args: Tuple[str, ...]): print("当前活跃度: " + str(api.user.get_liveness_info()["liveness"])) +class BrushLivenessCommand(Command): + def exec(self, api: FishPi, args: Tuple[str, ...]): + articles = map(lambda article: api.article.get_article( + article_id=article['oId']), api.article.list_articles()) + # TODO + + class GetRewardCommand(Command): def exec(self, api: FishPi, args: Tuple[str, ...]): api.user.get_yesterday_reward() @@ -106,7 +121,13 @@ def exec(self, api: FishPi, args: Tuple[str, ...]): class GetPointCommand(Command): def exec(self, api: FishPi, args: Tuple[str, ...]): print( - "当前积分: " + str(api.user.get_user_info(GLOBAL_CONFIG.auth_config.username)["userPoint"])) + f'当前积分: {str(api.user.get_user_info(api.current_user)["userPoint"])}') + + +class RevokeMessageCommand(Command): + def exec(self, api: FishPi, args: Tuple[str, ...]): + if api.chatroom.last_msg_id is not None: + api.chatroom.revoke(api.chatroom.last_msg_id) class BlackListCommand(Command): @@ -131,6 +152,38 @@ def exec(self, api: FishPi, args: Tuple[str, ...]): render_user_info(userInfo) +class ShowCurrentUserCommand(Command): + def exec(self, api: FishPi, args: Tuple[str, ...]): + print('当前用户') + op(api.sockpuppets[api.current_user], exclude=["ws"]) + + +class ShowSockpuppetCommand(Command): + def exec(self, api: FishPi, args: Tuple[str, ...]): + print('分身账户') + for user in api.sockpuppets.values(): + op(user, exclude=["ws"]) + + +class ChangeCurrentUserCommand(Command): + def exec(self, api: FishPi, args: Tuple[str, ...]): + target_user_name = " ".join(args) + print(f'账户切换 {api.current_user} ===> {target_user_name}') + api.sockpuppets[api.current_user].offline() + if target_user_name in api.sockpuppets: + api.sockpuppets[target_user_name].online(ChatRoom().start) + else: + print('请输入密码:') + api_key = '' + while len(api_key) == 0: + password = input("") + api.login(target_user_name, password) + api_key = api.api_key + api.sockpuppets[target_user_name] = UserInfo( + target_user_name, password, api_key) + api.sockpuppets[target_user_name].online(ChatRoom().start) + + class PointTransferCommand(Command): def exec(self, api: FishPi, args: Tuple[str, ...]): res = re.fullmatch(TRANSFER_RE, ' '.join(args)) @@ -261,8 +314,12 @@ def init_cli(api: FishPi): cli_handler.add_command('#answer', AnswerMode()) cli_handler.add_command('#checked', CheckInCommand()) cli_handler.add_command('#reward', GetRewardCommand()) + cli_handler.add_command('#revoke', RevokeMessageCommand()) cli_handler.add_command('#liveness', GetLivenessCommand()) cli_handler.add_command('#point', GetPointCommand()) + cli_handler.add_command('#me', ShowCurrentUserCommand()) + cli_handler.add_command('#account', ShowSockpuppetCommand()) + cli_handler.add_command('#change', ChangeCurrentUserCommand()) cli_handler.add_command('#user', GetUserInfoCommand()) cli_handler.add_command('#online-users', OnlineUserCommand()) cli_handler.add_command('#blacklist', BlackListCommand()) diff --git a/src/core/redpacket.py b/src/core/redpacket.py index b605da3..b9d63e4 100644 --- a/src/core/redpacket.py +++ b/src/core/redpacket.py @@ -5,9 +5,8 @@ import time from src.api import FishPi -from src.utils.utils import RPS_LOSED, RPS_SUCCESS, RPS_ZERO - -from .config import GLOBAL_CONFIG +from src.api.config import GLOBAL_CONFIG +from src.utils import RPS_LOSED, RPS_SUCCESS, RPS_ZERO CODE = enum.Enum('REDPACKET_CODE', ['SUCCESS', 'LOSED', 'NOT_ME', "ZERO"]) diff --git a/src/core/user.py b/src/core/user.py index 9fc6c67..14b34d8 100644 --- a/src/core/user.py +++ b/src/core/user.py @@ -1,11 +1,6 @@ # -*- coding: utf-8 -*- -import time - from src.api import FishPi -from src.utils.utils import * - -from .config import GLOBAL_CONFIG def render_user_info(userInfo): @@ -34,17 +29,3 @@ def check_in(api: FishPi): print('未登录') else: api.user.get_yesterday_reward() - - -def login(api: FishPi): - success = api.login(GLOBAL_CONFIG.auth_config.username, - GLOBAL_CONFIG.auth_config.password, GLOBAL_CONFIG.auth_config.mfa_code) - if success: - print(HELP) - else: - while len(api.api_key) == 0: - code = input("") - api.login(GLOBAL_CONFIG.auth_config.username, - GLOBAL_CONFIG.auth_config.password, code) - if len(api.api_key) > 0: - break diff --git a/src/core/websocket.py b/src/core/websocket.py deleted file mode 100644 index 009f9f7..0000000 --- a/src/core/websocket.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -import _thread -import json -import ssl - -import schedule -import websocket - -from src.api import API, FishPi - -from .config import GLOBAL_CONFIG - - -def on_message(ws, message): - data = json.loads(message) - for call in API.ws_calls: - call(API, data) - - -def on_error(ws, error): - print(error) - - -def on_close(ws, close_status_code, close_msg): - print("已经离开聊天室,可以执行命令 #chatroom 重新进入聊天室") - - -def on_open(ws): - print(f'欢迎{API.current_user}进入聊天室!') - if len(GLOBAL_CONFIG.chat_config.blacklist) > 0: - print('小黑屋成员: ' + str(GLOBAL_CONFIG.chat_config.blacklist)) - if GLOBAL_CONFIG.chat_config.soliloquize_switch: - schedule.run_pending() - - -def chatroom_out(api: FishPi): - api.ws.close() - api.ws = None - - -def __chatroom_in(api: FishPi): - websocket.enableTrace(False) - ws = websocket.WebSocketApp("wss://fishpi.cn/chat-room-channel?apiKey=" + api.api_key, - on_open=on_open, - on_message=on_message, - on_error=on_error, - on_close=on_close) - api.ws = ws - ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) - - -def init_chatroom(api: FishPi): - _thread.start_new_thread(__chatroom_in, (api,)) diff --git a/src/main.py b/src/main.py index d536338..90aefe5 100644 --- a/src/main.py +++ b/src/main.py @@ -1,14 +1,18 @@ # -*- coding: utf-8 -*- +import signal +import sys + import click +import schedule from src.api import API +from src.api.config import CliOptions from src.core import FishPiInitor -from src.core.config import CliConfig from src.utils.version import __version__ -def run(config: CliConfig): - FishPiInitor(api=API, cli_config=config) +def run(options: CliOptions): + FishPiInitor(api=API, options=options) @click.command() @@ -18,8 +22,20 @@ def run(config: CliConfig): @click.option("--code", "-c", type=click.STRING, help="两步验证码") @click.option("--file_path", "-f", type=click.STRING, help="配置文件路径") def cli(username: str, password: str, code: str, file_path: str) -> str: - run(CliConfig(username, password, code, file_path)) + run(CliOptions(username, password, code, file_path)) + + +def signal_handler(sig, frame): + schedule.clear() + for user in API.sockpuppets.values(): + keys = list(user.ws.keys()) + for key in keys: + user.ws[key].stop() + print("\n收到 Ctrl+C 信号,程序即将退出...") + sys.exit(0) + +signal.signal(signal.SIGINT, signal_handler) if __name__ == "__main__": cli() diff --git a/src/utils/utils.py b/src/utils/__init__.py similarity index 92% rename from src/utils/utils.py rename to src/utils/__init__.py index df8bafa..538fec0 100644 --- a/src/utils/utils.py +++ b/src/utils/__init__.py @@ -22,10 +22,14 @@ [#answer] 进入|退出 答题模式 [#checked] 查看当前是否签到 [#reward] 领取昨日活跃奖励 +[#revoke] 撤回最近一条聊天室消息 [#transfer] 32 Gakkiyomi 送给你 (积分 用户 留言) [#point] 查看当前个人积分 [#online-users] 查看当前在线的用户列表 [#user username] 输入 #user 用户名 可查看此用户详细信息 (#user Gakkiyomi) +[#me] 查看当前在线账号 +[#account] 查看分身账号 +[#change] 账号切换 #change Gakkiyomi [#blacklist] 查看黑名单列表 [#ban username] 将某人送入黑名单 [#unban username] 将某人解除黑名单 diff --git a/src/utils/version.py b/src/utils/version.py index 12755f7..9492d72 100644 --- a/src/utils/version.py +++ b/src/utils/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = "1.5.3" +__version__ = "2.0.0"