diff --git a/.gitignore b/.gitignore index 60f9b49..83100c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ .venv/ +secrets/ message_stats.json config.yaml \ No newline at end of file diff --git a/adapters/tg.py b/adapters/tg.py index 20b3502..4d37db4 100644 --- a/adapters/tg.py +++ b/adapters/tg.py @@ -10,8 +10,11 @@ from aiogram.filters import CommandStart, Command from aiogram.client.session.aiohttp import AiohttpSession from aiogram import F +from core.post_to_fedi import router as fedi_router + from core.bitflip import handle_bitflip_command from core.link import handle_links +from core.post_to_fedi import handle_auth, handle_post_to_fedi from core.promote import handle_promote_command from core.repeater import MessageRepeater from core.simple import handle_start_command, handle_baka, dummy_handler, handle_info_command @@ -44,6 +47,9 @@ class TelegramAdapter: router.message(Command('t'))(handle_promote_command) # stats 模块 router.message(Command('stats'))(handle_stats_command) + # fedi 模块 + router.message(Command('fauth'))(handle_auth) + router.message(Command('post'))(handle_post_to_fedi) # unpin 模块 # 不知道为什么检测不到频道的消息被置顶这个事件,暂时认为所有的频道消息都是被置顶的 router.message(F.chat.type.in_({'group', 'supergroup'}) & F.sender_chat & ( @@ -63,6 +69,8 @@ class TelegramAdapter: # Include router in dispatcher self.dp.include_router(router) + # 处理联邦宇宙认证相关 + self.dp.include_router(fedi_router) def _setup_middleware(self): """注册中间件""" diff --git a/config.example.yaml b/config.example.yaml index e5630ae..e19b0ff 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -10,6 +10,9 @@ features: # 启用 1/0 翻转 bitflip: enable: true + # 启用转发到联邦宇宙 + fedi: + enable: true # 启用链接解析/清理 link: enable: true diff --git a/core/post_to_fedi.py b/core/post_to_fedi.py new file mode 100644 index 0000000..8572598 --- /dev/null +++ b/core/post_to_fedi.py @@ -0,0 +1,147 @@ +from aiogram.types import Message +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram import Router + +from config import config +from mastodon import Mastodon + +router = Router() + +class AuthStates(StatesGroup): + waiting_for_token = State() + +def check_client_cred_exists(instance: str) -> bool: + """ + 检查实例的凭据文件是否存在 + """ + try: + with open(f'secrets/realbot_{instance}_clientcred.secret', 'r'): + return True + except FileNotFoundError: + return False + +def check_user_cred_exists(instance: str, userid: int) -> bool: + """ + 检查用户凭据文件是否存在 + """ + try: + with open(f'secrets/realbot_{instance}_{userid}_usercred.secret', 'r'): + return True + except FileNotFoundError: + return False + +async def handle_token(instance,mastodon, message: Message): + mastodon.log_in( + code=message.text, + to_file=f"secrets/realbot_{instance}_{message.from_user.id}_usercred.secret" + ) + +async def handle_auth(message: Message, state: FSMContext): + """ + 处理身份验证 + """ + if not config.is_feature_enabled('fedi', message.chat.id): + return + if not message.chat.type == 'private': + await message.reply('请在私聊中使用此命令') + return + instance = message.text.replace('/fauth', '').strip() + if instance == '': + await message.reply('请输入实例域名,例如:`example.com`') + return + if not check_client_cred_exists(instance): + Mastodon.create_app( + 'realbot', + api_base_url='https://{}'.format(instance), + to_file='secrets/realbot_{}_clientcred.secret'.format(instance), + scopes=['read:accounts', 'read:statuses','write:media','write:statuses'] + ) + mastodon = Mastodon(client_id=f'secrets/realbot_{instance}_clientcred.secret', ) + auth_url = mastodon.auth_request_url() + await message.reply('请在浏览器中打开链接进行身份验证:\n{}\n验证完成后,请用得到的 token 回复这条消息'.format(auth_url)) + # 在发送消息后设置状态 + await state.update_data(instance=instance) + await state.set_state(AuthStates.waiting_for_token) + +# 创建处理回复的 handler +@router.message(AuthStates.waiting_for_token) +async def handle_token_reply(message: Message, state: FSMContext): + data = await state.get_data() + instance = data.get('instance') + mastodon = Mastodon(client_id=f'secrets/realbot_{instance}_clientcred.secret') + status = await message.reply('正在处理身份验证,请稍候...') + await handle_token(instance,mastodon,message) + await status.edit_text('身份验证成功!\n现在你可以使用 /post 命令将消息发布到联邦网络。') + # 清除状态 + await state.clear() + +async def handle_post_to_fedi(message: Message): + """ + 处理发布到联邦网络的消息 + """ + if not config.is_feature_enabled('fedi', message.chat.id): + return + if not message.reply_to_message: + await message.reply('请回复要发布的消息') + return + + user_id = message.from_user.id + + # 查找用户绑定的实例 + import os + import glob + + user_cred_pattern = f'secrets/realbot_*_{user_id}_usercred.secret' + user_cred_files = glob.glob(user_cred_pattern) + + if not user_cred_files: + await message.reply('请先使用 /fauth 命令进行身份验证') + return + + # 从文件名中提取实例名 + cred_file = user_cred_files[0] # 假设用户只绑定一个实例 + filename = os.path.basename(cred_file) + # 格式: realbot_{instance}_{userid}_usercred.secret + parts = filename.split('_') + instance = '_'.join(parts[1:-2]) # 提取实例名部分 + + mastodon = Mastodon( + access_token=f'{cred_file}', + api_base_url=f'https://{instance}' + ) + + # 发布消息到联邦网络 + try: + arguments = message.text.replace('/post', '', 1).strip().split(' ') + if len(arguments) >= 1 and arguments[0]: + visibility = arguments[0] # 默认可见性为账号设置的可见性 + else: + visibility = None + + status_message = await message.reply('尝试发布消息到联邦网络...') + # 处理图片附件 + media_ids = [] + if message.reply_to_message.photo: + await status_message.edit_text('正在处理图片附件...') + # 获取最大尺寸的图片 + photo = message.reply_to_message.photo[-1] + file_info = await message.bot.get_file(photo.file_id) + file_data = await message.bot.download_file(file_info.file_path) + + # 上传图片到Mastodon + media = mastodon.media_post(file_data, mime_type='image/png') + media_ids.append(media['id']) + text = message.reply_to_message.text + if media_ids: + text = message.reply_to_message.caption + # 发布消息 + status = mastodon.status_post( + text, + media_ids=media_ids if media_ids else None, + visibility = visibility + ) + status_url = status['url'] + await status_message.edit_text(f'消息已成功发布到联邦网络!\n{status_url}') + except Exception as e: + await message.reply(f'发布失败: {str(e)}') \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6a6e096..987c38a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ requires-python = ">=3.13" dependencies = [ "aiodns==3.5.0", "aiogram==3.21.0", + "mastodon-py==2.0.1", "matrix-nio==0.25.2", "python-abp==0.2.0", "pyyaml>=6.0.2", diff --git a/uv.lock b/uv.lock index 89bf5fc..2eb49ac 100644 --- a/uv.lock +++ b/uv.lock @@ -126,6 +126,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "blurhash" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/96/363eae896ec6a699dfc63f19f9b857c09294fe4d791198f002baa495fc4e/blurhash-1.1.4.tar.gz", hash = "sha256:da56b163e5a816e4ad07172f5639287698e09d7f3dc38d18d9726d9c1dbc4cee", size = 4738, upload-time = "2019-10-11T19:02:28.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/08/163cb166a2464223a5eb8890a92cc1cf7e8c7c79a2c75e497e3d8f3a4711/blurhash-1.1.4-py2.py3-none-any.whl", hash = "sha256:7611c1bc41383d2349b6129208587b5d61e8792ce953893cb49c38beeb400d1d", size = 5307, upload-time = "2019-10-11T19:02:27.484Z" }, +] + [[package]] name = "certifi" version = "2025.7.14" @@ -179,6 +188,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + [[package]] name = "frozenlist" version = "1.7.0" @@ -307,6 +325,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/75/f620449f0056eff0ec7c1b1e088f71068eb4e47a46eb54f6c065c6ad7675/magic_filter-1.0.12-py3-none-any.whl", hash = "sha256:e5929e544f310c2b1f154318db8c5cdf544dd658efa998172acd2e4ba0f6c6a6", size = 11335, upload-time = "2023-10-01T12:33:17.711Z" }, ] +[[package]] +name = "mastodon-py" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blurhash" }, + { name = "decorator" }, + { name = "python-dateutil" }, + { name = "python-magic", marker = "sys_platform != 'win32'" }, + { name = "python-magic-bin", marker = "sys_platform == 'win32'" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/40/8f962b2d1782fd097ea45a344094df7d56f980f1d64c62de0bfa1ef5b0fc/mastodon_py-2.0.1.tar.gz", hash = "sha256:f0a9cf59071347c7ff2ee49487d2520ca661f349f369b68845bdf3e43db1fff3", size = 10988936, upload-time = "2025-03-02T09:46:27.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/d7/632b8b14a13eb33a9fcc5c3354f878fa39dfb5f51ef6283b24fce84c5796/mastodon_py-2.0.1-py3-none-any.whl", hash = "sha256:5bb543ecbd7526bd50675d5d617ec04caa6b10d4002454cbf5641ad612723455", size = 108485, upload-time = "2025-03-02T09:46:22.308Z" }, +] + [[package]] name = "matrix-nio" version = "0.25.2" @@ -526,6 +561,36 @@ version = "0.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3b/30/31c6e4ca48992ee5f4bb8325f249f944ac493898606ca83a7642ff5ee18b/python-abp-0.2.0.tar.gz", hash = "sha256:f36d0e9fdc089587c26036e0403f36d729395fc9f4dbce45baf3a493d1de8112", size = 80013, upload-time = "2020-05-20T13:09:55.536Z" } +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-magic" +version = "0.4.27" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677, upload-time = "2022-06-07T20:16:59.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840, upload-time = "2022-06-07T20:16:57.763Z" }, +] + +[[package]] +name = "python-magic-bin" +version = "0.4.14" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/5d/10b9ac745d9fd2f7151a2ab901e6bb6983dbd70e87c71111f54859d1ca2e/python_magic_bin-0.4.14-py2.py3-none-win32.whl", hash = "sha256:34a788c03adde7608028203e2dbb208f1f62225ad91518787ae26d603ae68892", size = 397784, upload-time = "2017-10-02T16:30:15.806Z" }, + { url = "https://files.pythonhosted.org/packages/07/c2/094e3d62b906d952537196603a23aec4bcd7c6126bf80eb14e6f9f4be3a2/python_magic_bin-0.4.14-py2.py3-none-win_amd64.whl", hash = "sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69", size = 409299, upload-time = "2017-10-02T16:30:18.545Z" }, +] + [[package]] name = "python-socks" version = "2.7.1" @@ -559,6 +624,7 @@ source = { virtual = "." } dependencies = [ { name = "aiodns" }, { name = "aiogram" }, + { name = "mastodon-py" }, { name = "matrix-nio" }, { name = "python-abp" }, { name = "pyyaml" }, @@ -569,6 +635,7 @@ dependencies = [ requires-dist = [ { name = "aiodns", specifier = "==3.5.0" }, { name = "aiogram", specifier = "==3.21.0" }, + { name = "mastodon-py", specifier = "==2.0.1" }, { name = "matrix-nio", specifier = "==0.25.2" }, { name = "python-abp", specifier = "==0.2.0" }, { name = "pyyaml", specifier = ">=6.0.2" }, @@ -665,6 +732,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.1"