pymecli 0.5.2__tar.gz → 0.5.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymecli-0.5.2 → pymecli-0.5.4}/PKG-INFO +3 -2
- {pymecli-0.5.2 → pymecli-0.5.4}/cli/bitget.py +8 -12
- {pymecli-0.5.2 → pymecli-0.5.4}/cli/gate.py +3 -5
- pymecli-0.5.4/cli/tele.py +355 -0
- pymecli-0.5.4/core/clash.py +368 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/crypto/bitget.py +15 -22
- {pymecli-0.5.2 → pymecli-0.5.4}/crypto/gate.py +5 -7
- {pymecli-0.5.2 → pymecli-0.5.4}/data/template.yaml +25 -1
- {pymecli-0.5.2 → pymecli-0.5.4}/pyproject.toml +3 -1
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/mysql.py +62 -20
- pymecli-0.5.2/core/clash.py +0 -664
- {pymecli-0.5.2 → pymecli-0.5.4}/.gitignore +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/README.md +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/api/__init__.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/api/v1/__init__.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/api/v1/clash.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/api/v1/redis.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/cli/__init__.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/cli/dst.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/cli/example.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/cli/fast.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/cli/redis_csv.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/cli/util.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/core/__init__.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/core/config.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/core/redis_client.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/crypto/__init__.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/data/__init__.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/data/dou_dict.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/data/dou_list.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/data/main.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/models/__init__.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/models/douzero_model.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/models/ocr_model.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/models/response.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/__init__.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/elapsed.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/helper.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/logger.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/path.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/pd.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/pyredis.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/sleep.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/text.py +0 -0
- {pymecli-0.5.2 → pymecli-0.5.4}/utils/toml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pymecli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: My CLI
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/pymecli
|
|
6
6
|
Project-URL: Repository, https://github.com/meme2046/pymecli
|
|
@@ -28,6 +28,7 @@ Requires-Dist: selenium>=4.40.0
|
|
|
28
28
|
Requires-Dist: slpp>=1.2.3
|
|
29
29
|
Requires-Dist: sqlalchemy>=2.0.45
|
|
30
30
|
Requires-Dist: tabulate>=0.9.0
|
|
31
|
+
Requires-Dist: telethon>=1.44.0
|
|
31
32
|
Requires-Dist: toml>=0.10.2
|
|
32
33
|
Requires-Dist: typer>=0.20.1
|
|
33
34
|
Requires-Dist: uvicorn>=0.40.0
|
|
@@ -21,23 +21,19 @@ app = typer.Typer()
|
|
|
21
21
|
@app.command()
|
|
22
22
|
def sync(
|
|
23
23
|
env_path: str = "d:/.env",
|
|
24
|
-
csv_dir: str = "d:/github/meme2046/data",
|
|
25
24
|
):
|
|
26
|
-
"""同步mysql中grid数据到
|
|
25
|
+
"""同步mysql中grid数据到redis"""
|
|
27
26
|
engine = get_database_engine(env_path)
|
|
28
|
-
grid_csv_fp = f"{csv_dir}/bitget_grid_0.csv"
|
|
29
|
-
sf_csv_fp = f"{csv_dir}/bitget_sf_0.csv"
|
|
30
|
-
ff_csv_fp = f"{csv_dir}/bitget_ff_0.csv"
|
|
31
27
|
|
|
32
|
-
asyncio.run(grid_open(engine
|
|
33
|
-
asyncio.run(grid_close(engine
|
|
28
|
+
asyncio.run(grid_open(engine))
|
|
29
|
+
asyncio.run(grid_close(engine))
|
|
34
30
|
|
|
35
|
-
asyncio.run(bitget_sf_open(engine
|
|
36
|
-
asyncio.run(bitget_sf_close(engine
|
|
31
|
+
asyncio.run(bitget_sf_open(engine))
|
|
32
|
+
asyncio.run(bitget_sf_close(engine))
|
|
37
33
|
|
|
38
|
-
asyncio.run(bitget_ff_open(engine
|
|
39
|
-
asyncio.run(bitget_ff_pending(engine
|
|
40
|
-
asyncio.run(bitget_ff_close(engine
|
|
34
|
+
asyncio.run(bitget_ff_open(engine))
|
|
35
|
+
asyncio.run(bitget_ff_pending(engine))
|
|
36
|
+
asyncio.run(bitget_ff_close(engine))
|
|
41
37
|
|
|
42
38
|
|
|
43
39
|
@app.command()
|
|
@@ -21,10 +21,8 @@ app = typer.Typer()
|
|
|
21
21
|
@app.command()
|
|
22
22
|
def rsync(
|
|
23
23
|
env_path: str = "d:/.env",
|
|
24
|
-
csv_dir: str = "d:/github/meme2046/data",
|
|
25
24
|
):
|
|
26
|
-
"""同步mysql中grid数据到
|
|
25
|
+
"""同步mysql中grid数据到redis"""
|
|
27
26
|
engine = get_database_engine(env_path)
|
|
28
|
-
|
|
29
|
-
asyncio.run(
|
|
30
|
-
asyncio.run(grid_close(engine, grid_csv_fp))
|
|
27
|
+
asyncio.run(grid_open(engine))
|
|
28
|
+
asyncio.run(grid_close(engine))
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
from utils.logger import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
app = typer.Typer()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def info(
|
|
14
|
+
dotenv_path: str = typer.Argument(
|
|
15
|
+
help="dotenv file contains TELE_SEESION_STRING", default="d:/.env"
|
|
16
|
+
),
|
|
17
|
+
):
|
|
18
|
+
load_dotenv("d:/.env")
|
|
19
|
+
logger.info(os.getenv("TELE_SESSION_STRING"))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command()
|
|
23
|
+
def check(
|
|
24
|
+
dotenv_path: str = typer.Argument(
|
|
25
|
+
help="dotenv file contains TELE_SESSION_STRING / TELE_API_ID / TELE_API_HASH",
|
|
26
|
+
default="d:/.env",
|
|
27
|
+
),
|
|
28
|
+
):
|
|
29
|
+
"""验证 TELE_SESSION_STRING 是否有效。
|
|
30
|
+
|
|
31
|
+
通过 StringSession 建立客户端并发起一次 get_me 调用:
|
|
32
|
+
能正常返回账号对象即视为 session 有效;get_me 在 auth_key
|
|
33
|
+
未授权 / 被注销时会内部捕获 UnauthorizedError 并返回 None,
|
|
34
|
+
故 me is None 即判定 session 失效。
|
|
35
|
+
需要 .env 中同时配置 TELE_API_ID 与 TELE_API_HASH。
|
|
36
|
+
"""
|
|
37
|
+
load_dotenv(dotenv_path)
|
|
38
|
+
|
|
39
|
+
session_string = os.getenv("TELE_SESSION_STRING")
|
|
40
|
+
api_id = os.getenv("TELE_API_ID")
|
|
41
|
+
api_hash = os.getenv("TELE_API_HASH")
|
|
42
|
+
if not session_string:
|
|
43
|
+
logger.error("TELE_SESSION_STRING 未找到: %s", dotenv_path)
|
|
44
|
+
raise typer.Exit(1)
|
|
45
|
+
if not (api_id and api_hash):
|
|
46
|
+
logger.error("TELE_API_ID / TELE_API_HASH 未找到: %s", dotenv_path)
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
|
|
49
|
+
# 延迟导入:仅在使用 check 子命令时才加载 telethon,避免拖慢其它子命令
|
|
50
|
+
import asyncio
|
|
51
|
+
from typing import Awaitable, cast
|
|
52
|
+
|
|
53
|
+
from telethon import TelegramClient
|
|
54
|
+
from telethon.sessions import StringSession
|
|
55
|
+
|
|
56
|
+
async def _verify():
|
|
57
|
+
client = TelegramClient(StringSession(session_string), int(api_id), api_hash)
|
|
58
|
+
try:
|
|
59
|
+
await client.connect()
|
|
60
|
+
# get_me 会真正发起一次 API 调用,能返回即说明 auth_key 仍然有效
|
|
61
|
+
return await client.get_me()
|
|
62
|
+
finally:
|
|
63
|
+
# telethon 的 disconnect 声明为同步 def,但 loop 运行时实际返回协程,
|
|
64
|
+
# Pylance 据签名推断返回 None 导致 await 报错,用 cast 显式标注
|
|
65
|
+
await cast(Awaitable[None], client.disconnect())
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
me = asyncio.run(_verify())
|
|
69
|
+
except Exception as e:
|
|
70
|
+
# 网络异常等非鉴权问题,打印完整堆栈便于排查
|
|
71
|
+
logger.exception("验证 session 时发生异常: %s", e)
|
|
72
|
+
raise typer.Exit(1)
|
|
73
|
+
|
|
74
|
+
# get_me 内部 catch UnauthorizedError(含 AuthKeyUnregistered/Invalid 等所有子类)后返回 None,
|
|
75
|
+
# 所以 me is None 才是 auth_key 已被服务器拒绝 / session 失效的真实信号
|
|
76
|
+
if me is None:
|
|
77
|
+
logger.error("session 无效: auth_key 未授权或已被服务器注销")
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
|
|
80
|
+
logger.info(
|
|
81
|
+
"session 有效: id=%s name=%s phone=%s",
|
|
82
|
+
getattr(me, "id", None), getattr(me, "first_name", None), getattr(me, "phone", None),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
def login(
|
|
88
|
+
dotenv_path: str = typer.Argument(
|
|
89
|
+
help="dotenv file containing TELE_API_ID / TELE_API_HASH",
|
|
90
|
+
default="d:/.env",
|
|
91
|
+
),
|
|
92
|
+
):
|
|
93
|
+
"""交互式登录并打印 TELE_SESSION_STRING。
|
|
94
|
+
|
|
95
|
+
使用 telethon 的 start() 完成 phone → 验证码 → 2FA 的交互式登录,
|
|
96
|
+
登录成功后用 StringSession.save() 取出可复用 session 串并打印。
|
|
97
|
+
session string 失效周期很长,手动复制到 .env 即可,不做自动回写。
|
|
98
|
+
需要 .env 中已配置 TELE_API_ID 与 TELE_API_HASH。
|
|
99
|
+
"""
|
|
100
|
+
load_dotenv(dotenv_path)
|
|
101
|
+
api_id = os.getenv("TELE_API_ID")
|
|
102
|
+
api_hash = os.getenv("TELE_API_HASH")
|
|
103
|
+
if not (api_id and api_hash):
|
|
104
|
+
logger.error("TELE_API_ID / TELE_API_HASH 未找到: %s", dotenv_path)
|
|
105
|
+
raise typer.Exit(1)
|
|
106
|
+
|
|
107
|
+
# 延迟导入:仅在使用 login 子命令时才加载 telethon
|
|
108
|
+
import asyncio
|
|
109
|
+
from typing import Awaitable, cast
|
|
110
|
+
|
|
111
|
+
from telethon import TelegramClient
|
|
112
|
+
from telethon.sessions import StringSession
|
|
113
|
+
|
|
114
|
+
async def _login():
|
|
115
|
+
# 空 StringSession + start() 触发交互式登录(控制台输入 phone / 验证码 / 2FA 密码)
|
|
116
|
+
session = StringSession()
|
|
117
|
+
client = TelegramClient(session, int(api_id), api_hash)
|
|
118
|
+
try:
|
|
119
|
+
# start() 同 disconnect 一样声明为同步 def,loop 运行时返回协程,用 cast 标注
|
|
120
|
+
await cast(Awaitable[None], client.start())
|
|
121
|
+
# telethon 在 connect 时原地写入 session.auth_key,故持有的 session 引用即登录后状态;
|
|
122
|
+
# 直接用 session.save() 而非 client.session,避开 client.session 宽类型推断的 .save() 报错
|
|
123
|
+
return session.save()
|
|
124
|
+
finally:
|
|
125
|
+
await cast(Awaitable[None], client.disconnect())
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
session_string = asyncio.run(_login())
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.exception("生成 session string 时发生异常: %s", e)
|
|
131
|
+
raise typer.Exit(1)
|
|
132
|
+
|
|
133
|
+
# 只打印,不自动回写 .env:session string 失效周期长,手动加一次即可,避免覆盖风险
|
|
134
|
+
typer.secho("TELE_SESSION_STRING 生成成功,请复制下行并手动写入 .env:", fg=typer.colors.GREEN)
|
|
135
|
+
typer.secho(session_string, fg=typer.colors.CYAN)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command()
|
|
139
|
+
def upload(
|
|
140
|
+
image_path: str = typer.Argument(
|
|
141
|
+
help="要上传的图片文件路径",
|
|
142
|
+
),
|
|
143
|
+
dotenv_path: str = typer.Option(
|
|
144
|
+
help="dotenv file path", default="d:/.env"
|
|
145
|
+
),
|
|
146
|
+
):
|
|
147
|
+
"""上传图片到指定 Telegram 频道。
|
|
148
|
+
|
|
149
|
+
使用 TELE_SESSION_STRING 登录并上传图片到 TELE_ONEDAOSHARE 环境变量指定的频道。
|
|
150
|
+
"""
|
|
151
|
+
load_dotenv(dotenv_path)
|
|
152
|
+
|
|
153
|
+
session_string = os.getenv("TELE_SESSION_STRING")
|
|
154
|
+
api_id = os.getenv("TELE_API_ID")
|
|
155
|
+
api_hash = os.getenv("TELE_API_HASH")
|
|
156
|
+
channel = os.getenv("TELE_ONEDAOSHARE")
|
|
157
|
+
|
|
158
|
+
if not session_string:
|
|
159
|
+
logger.error("TELE_SESSION_STRING 未找到: %s", dotenv_path)
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
if not (api_id and api_hash):
|
|
162
|
+
logger.error("TELE_API_ID / TELE_API_HASH 未找到: %s", dotenv_path)
|
|
163
|
+
raise typer.Exit(1)
|
|
164
|
+
if not channel:
|
|
165
|
+
logger.error("TELE_ONEDAOSHARE 未找到: %s", dotenv_path)
|
|
166
|
+
raise typer.Exit(1)
|
|
167
|
+
if not os.path.isfile(image_path):
|
|
168
|
+
logger.error("图片文件不存在: %s", image_path)
|
|
169
|
+
raise typer.Exit(1)
|
|
170
|
+
|
|
171
|
+
import asyncio
|
|
172
|
+
from typing import Awaitable, cast
|
|
173
|
+
|
|
174
|
+
from telethon import TelegramClient
|
|
175
|
+
from telethon.sessions import StringSession
|
|
176
|
+
|
|
177
|
+
async def _upload():
|
|
178
|
+
client = TelegramClient(StringSession(session_string), int(api_id), api_hash)
|
|
179
|
+
try:
|
|
180
|
+
await client.connect()
|
|
181
|
+
if not await client.is_user_authorized():
|
|
182
|
+
logger.error("session 无效或已失效,请先运行 login 命令重新生成")
|
|
183
|
+
raise typer.Exit(1)
|
|
184
|
+
|
|
185
|
+
# 规范化频道 ID:如果是纯数字,自动加 -100 前缀
|
|
186
|
+
target = channel.strip()
|
|
187
|
+
if target.isdigit():
|
|
188
|
+
target = f"-100{target}"
|
|
189
|
+
logger.info("频道 ID 已自动规范化为: %s", target)
|
|
190
|
+
|
|
191
|
+
target_id = int(target) if target.lstrip('-').isdigit() else None
|
|
192
|
+
|
|
193
|
+
# 方法1:先尝试直接通过 ID 在 dialogs 缓存中查找(更可靠)
|
|
194
|
+
logger.info("正在查找频道: %s", target)
|
|
195
|
+
found_dialog = None
|
|
196
|
+
async for dialog in client.iter_dialogs():
|
|
197
|
+
if dialog.id == target_id:
|
|
198
|
+
found_dialog = dialog
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
if found_dialog:
|
|
202
|
+
logger.info("成功找到频道: %s (ID: %s)", target, found_dialog.id)
|
|
203
|
+
# 使用 dialog.entity 作为目标,避免再次解析
|
|
204
|
+
target = found_dialog.entity
|
|
205
|
+
else:
|
|
206
|
+
# 方法2:退回到 get_entity(支持 @username 等)
|
|
207
|
+
logger.info("缓存中未找到,尝试通过 get_entity 解析: %s", target)
|
|
208
|
+
try:
|
|
209
|
+
entity = await client.get_entity(target)
|
|
210
|
+
if isinstance(entity, list):
|
|
211
|
+
entity = entity[0]
|
|
212
|
+
logger.info("成功解析: %s (ID: %s)", target, getattr(entity, 'id', 'N/A'))
|
|
213
|
+
target = entity
|
|
214
|
+
except ValueError as e:
|
|
215
|
+
logger.error(
|
|
216
|
+
"无法访问频道 %s。请检查:\n"
|
|
217
|
+
"1. ID 是否正确(纯数字需要带 -100 前缀)\n"
|
|
218
|
+
"2. 当前登录的账号是否为该频道成员\n"
|
|
219
|
+
"3. 如果是私有名片,请使用频道的 @username\n"
|
|
220
|
+
"错误详情: %s", target, e
|
|
221
|
+
)
|
|
222
|
+
raise typer.Exit(1)
|
|
223
|
+
|
|
224
|
+
# 发送文件到频道,force_document=False 表示作为图片发送(Telegram 会自动压缩)
|
|
225
|
+
# 如果想原样发送文件,设置 force_document=True
|
|
226
|
+
result = await client.send_file(
|
|
227
|
+
target,
|
|
228
|
+
image_path,
|
|
229
|
+
caption="", # 可以自定义描述
|
|
230
|
+
force_document=False,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# send_file 可能返回 Message 或 List[Message]
|
|
234
|
+
message = result[0] if isinstance(result, list) else result
|
|
235
|
+
|
|
236
|
+
# 下载媒体文件到本地临时目录
|
|
237
|
+
download_link = None
|
|
238
|
+
try:
|
|
239
|
+
if hasattr(message, 'media') and message.media:
|
|
240
|
+
import tempfile
|
|
241
|
+
temp_dir = tempfile.gettempdir()
|
|
242
|
+
local_path = await client.download_media(
|
|
243
|
+
message,
|
|
244
|
+
file=temp_dir
|
|
245
|
+
)
|
|
246
|
+
if local_path:
|
|
247
|
+
download_link = local_path
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.debug("下载文件失败: %s", e)
|
|
250
|
+
|
|
251
|
+
return message, download_link
|
|
252
|
+
finally:
|
|
253
|
+
await cast(Awaitable[None], client.disconnect())
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
message, download_link = asyncio.run(_upload())
|
|
257
|
+
|
|
258
|
+
# 打印详细的上传响应信息
|
|
259
|
+
typer.secho("\n📤 上传成功!", fg=typer.colors.GREEN, bold=True)
|
|
260
|
+
typer.secho("-" * 50, fg=typer.colors.CYAN)
|
|
261
|
+
typer.secho(f" 消息 ID: {message.id}", fg=typer.colors.WHITE)
|
|
262
|
+
typer.secho(f" 频道: {channel}", fg=typer.colors.WHITE)
|
|
263
|
+
|
|
264
|
+
# 尝试获取文件信息
|
|
265
|
+
if hasattr(message, 'media') and message.media:
|
|
266
|
+
media = message.media
|
|
267
|
+
# 递归打印完整结构(类似 JSON 格式)
|
|
268
|
+
typer.secho(f" Media 类型: {type(media).__name__}", fg=typer.colors.YELLOW)
|
|
269
|
+
|
|
270
|
+
# 打印下载的本地文件路径(telesco.pe 公网链接 telethon 无法直接获取)
|
|
271
|
+
if download_link:
|
|
272
|
+
typer.secho(f"\n � 本地下载路径:", fg=typer.colors.MAGENTA, bold=True)
|
|
273
|
+
typer.secho(f" {download_link}", fg=typer.colors.MAGENTA)
|
|
274
|
+
|
|
275
|
+
# 打印消息链接(如果可能)
|
|
276
|
+
if hasattr(message, 'chat') and message.chat:
|
|
277
|
+
chat_id = getattr(message.chat, 'id', None)
|
|
278
|
+
if chat_id:
|
|
279
|
+
typer.secho(f" 聊天 ID: {chat_id}", fg=typer.colors.WHITE)
|
|
280
|
+
|
|
281
|
+
typer.secho("-" * 50, fg=typer.colors.CYAN)
|
|
282
|
+
except typer.Exit:
|
|
283
|
+
raise # 重新抛出 typer.Exit 以保持退出码
|
|
284
|
+
except Exception as e:
|
|
285
|
+
logger.exception("上传图片时发生异常: %s", e)
|
|
286
|
+
raise typer.Exit(1)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@app.command()
|
|
290
|
+
def list_channels(
|
|
291
|
+
dotenv_path: str = typer.Option(
|
|
292
|
+
help="dotenv file path", default="d:/.env"
|
|
293
|
+
),
|
|
294
|
+
):
|
|
295
|
+
"""列出当前账号加入的所有频道及其 ID。
|
|
296
|
+
|
|
297
|
+
方便获取正确的频道 ID 填入 TELE_ONE_DAO_SHARE。
|
|
298
|
+
"""
|
|
299
|
+
load_dotenv(dotenv_path)
|
|
300
|
+
|
|
301
|
+
session_string = os.getenv("TELE_SESSION_STRING")
|
|
302
|
+
api_id = os.getenv("TELE_API_ID")
|
|
303
|
+
api_hash = os.getenv("TELE_API_HASH")
|
|
304
|
+
|
|
305
|
+
if not session_string:
|
|
306
|
+
logger.error("TELE_SESSION_STRING 未找到: %s", dotenv_path)
|
|
307
|
+
raise typer.Exit(1)
|
|
308
|
+
if not (api_id and api_hash):
|
|
309
|
+
logger.error("TELE_API_ID / TELE_API_HASH 未找到: %s", dotenv_path)
|
|
310
|
+
raise typer.Exit(1)
|
|
311
|
+
|
|
312
|
+
import asyncio
|
|
313
|
+
from typing import Awaitable, cast
|
|
314
|
+
|
|
315
|
+
from telethon import TelegramClient
|
|
316
|
+
from telethon.sessions import StringSession
|
|
317
|
+
|
|
318
|
+
async def _list():
|
|
319
|
+
client = TelegramClient(StringSession(session_string), int(api_id), api_hash)
|
|
320
|
+
try:
|
|
321
|
+
await client.connect()
|
|
322
|
+
if not await client.is_user_authorized():
|
|
323
|
+
logger.error("session 无效或已失效,请先运行 login 命令重新生成")
|
|
324
|
+
raise typer.Exit(1)
|
|
325
|
+
|
|
326
|
+
channels = []
|
|
327
|
+
async for dialog in client.iter_dialogs():
|
|
328
|
+
if dialog.is_channel:
|
|
329
|
+
channels.append(dialog)
|
|
330
|
+
return channels
|
|
331
|
+
finally:
|
|
332
|
+
await cast(Awaitable[None], client.disconnect())
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
channels = asyncio.run(_list())
|
|
336
|
+
if not channels:
|
|
337
|
+
typer.secho("你还没有加入任何频道", fg=typer.colors.YELLOW)
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
typer.secho(f"\n你加入的 {len(channels)} 个频道:\n", fg=typer.colors.GREEN)
|
|
341
|
+
typer.secho(f"{'频道名称':<40} {'ID':<25}", fg=typer.colors.CYAN)
|
|
342
|
+
typer.secho("-" * 65, fg=typer.colors.CYAN)
|
|
343
|
+
for ch in channels:
|
|
344
|
+
name = ch.name or "(无名称)"
|
|
345
|
+
typer.secho(f"{name:<40} {ch.id:<25}")
|
|
346
|
+
typer.secho("\n提示:复制 ID(如 -100xxxxxxxx)到 .env 的 TELE_ONEDAOSHARE", fg=typer.colors.YELLOW)
|
|
347
|
+
except typer.Exit:
|
|
348
|
+
raise
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.exception("列出频道时发生异常: %s", e)
|
|
351
|
+
raise typer.Exit(1)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
if __name__ == "__main__":
|
|
355
|
+
logger.info("main")
|