├── .gitignore ├── Dockerfile ├── Procfile ├── README.md ├── conf_back.json ├── main.py ├── requirements.txt ├── typescript ├── package.json └── src │ ├── bot.ts │ ├── encryption.ts │ └── notion.ts └── utils ├── conf.py ├── encryption.py ├── latitude.py ├── notion.py └── telegram.py /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | 4 | # Mypy 5 | .mypy_cache/ 6 | 7 | # Elastic Beanstalk Files 8 | .elasticbeanstalk/* 9 | !.elasticbeanstalk/*.cfg.yml 10 | !.elasticbeanstalk/*.global.yml 11 | 12 | # Test generated files 13 | media/images/test_*.png 14 | media/ad_images/*.* 15 | 16 | # Created by https://www.gitignore.io 17 | 18 | ### OSX ### 19 | .DS_Store 20 | .AppleDouble 21 | .LSOverride 22 | 23 | # Icon must end with two \r 24 | Icon 25 | 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear on external disk 31 | .Spotlight-V100 32 | .Trashes 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | 42 | ### Python ### 43 | # Byte-compiled / optimized / DLL files 44 | __pycache__/ 45 | *.py[cod] 46 | 47 | # C extensions 48 | *.so 49 | 50 | # Distribution / packaging 51 | .Python 52 | env/ 53 | build/ 54 | develop-eggs/ 55 | dist/ 56 | downloads/ 57 | eggs/ 58 | lib/ 59 | lib64/ 60 | parts/ 61 | sdist/ 62 | var/ 63 | *.egg-info/ 64 | .installed.cfg 65 | *.egg 66 | 67 | # PyInstaller 68 | # Usually these files are written by a python script from a template 69 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 70 | *.manifest 71 | *.spec 72 | 73 | # Installer logs 74 | pip-log.txt 75 | pip-delete-this-directory.txt 76 | 77 | # Unit test / coverage reports 78 | htmlcov/ 79 | .tox/ 80 | .coverage 81 | .cache 82 | nosetests.xml 83 | coverage.xml 84 | 85 | # Translations 86 | *.mo 87 | *.pot 88 | 89 | # Sphinx documentation 90 | docs/_build/ 91 | 92 | # PyBuilder 93 | target/ 94 | 95 | 96 | ### Django ### 97 | *.log 98 | *.pyc 99 | local_settings.py 100 | 101 | .env 102 | db.sqlite3 103 | /.python-version 104 | local 105 | runserver_local.sh 106 | *.orig 107 | nohup.out 108 | node_modules/ 109 | conf.json 110 | src/notion.js 111 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | COPY . /code 4 | 5 | WORKDIR /code 6 | 7 | RUN pip install -i https://mirrors.aliyun.com/pypi/simple/ --upgrade pip setuptools \ 8 | && pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt \ 9 | && rm -rf ~/.cache/* \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | CMD python -u main.py -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn main:app -w 1 -k aiohttp.worker.GunicornWebWorker -b 0.0.0.0:9000 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudPiece 2 | 3 | 本着 All in one 的思想,实在不想再换或者加笔记了(累了),所以就有了 CloudPiece。 4 | 用来记录开车、神游、喝酒等等时迸发的新思路、新玩法,如果你也喜欢 或 能帮到你, 5 | 请 star。 6 | 7 | 8 | ### 技术选型 9 | 10 | Aiohttp + aiogram + Notion + sentry + 腾讯云函数 11 | 用 Aiohttp 做 web 服务,用 telegram 官方的 SDK aiogram 连接 telegram, 12 | Notion database 做数据库和笔记,sentry 做日志记录,腾讯云函数做服务器。 13 | 14 | 15 | ### 配置说明 16 | 注意这里因为要和朋友一起使用,所以有两个 integrations, 17 | 一个用来记录授权数据(private integrations), 数据也是记录在 Notion; 18 | 一个是用来转发消息的(public integrations), public integrations 会将发到 CloudPiece 的数据转存到 Notion。 **本服务不记录任何数据,只做转发**; 19 | 20 | 如果你是自己使用,完全不用这么复杂,直接使用一个 private integrations 就能实现,也不用记录授权关系,直接 fork 修改就好。 21 | 22 | 或者直接使用[该服务](https://telegram.me/CloudPieceBot), 为次还特意加了[留言板](https://joys.notion.site/c144f89764564f928c31f162e0ff307a) 23 | ```json 24 | { 25 | "relation_code": "private integrations 授权码", 26 | "relation_database_id": "授权关系数据库id", 27 | "client_id": "public integrations 中的 client_id", 28 | "client_secret": "public integrations 中的 client_secret", 29 | "redirect_uri": "public integrations 回调地址", 30 | "telegram_token": "telegram bot token", 31 | "key": "Oauth2 对 state 进行加密解密的key(保证 telegram 和 notion 授权一对一的关系)", 32 | "webhook_host": "腾讯云函数网关地址", 33 | "notion_version": "notion api 版本", 34 | "sentry_address": "sentry 项目key" 35 | } 36 | ``` 37 | 38 | ### 启动 39 | 40 | ```shell 41 | # 启动后有个问题,我没尝试,不知道怎么连接 telegram, 42 | # 我本地只是为了测试 Notion, 43 | # 和 telegram 联动的测试都是部署后测的 44 | uvicorn app:app --host 0.0.0.0 --port 9000 45 | # 或 46 | docker run -it -p 9000:9000 cloudpiece 47 | ``` 48 | 49 | ### 独立部署 50 | 因为是镜像部署,比较简单,这里就不详细列了,简单说下: 51 | 1. 需要先搞一个 telegram bot; 52 | 2. 将 conf_back.json 改为 conf.json,修改里边的配置; 53 | 3. build 自己的镜像,并上传到腾讯云镜像仓库; 54 | 4. 到云函数页面,选择 web 函数,镜像部署,直接部署就可以了 55 | (可以先创建 ap i网关,第一部要用,也可以部署创建,修改参数再 build、部署一次。 56 | 不要问我为什么知道😂); 57 | 5. 另我申请了腾讯云函数预置并发的内测,建议也申请个,否则发一个消息, 58 | 几十秒才有回馈,总感觉出错了,这体验很不好😯。 59 | 60 | 61 | ### TODO: 62 | - [ ] 为新建的 page 添加标题功能; 63 | - [ ] bookmark、embed 分类写入; 64 | 65 | ### 后续 66 | 一些小伙伴不用 telegram,用微信,但没有开发过微信生态的东西,后续再说。 67 | -------------------------------------------------------------------------------- /conf_back.json: -------------------------------------------------------------------------------- 1 | { 2 | "relation_code": "secret_2QPxxx", 3 | "relation_database_id": "7a2xxx", 4 | "client_id": "4d2xxx", 5 | "client_secret": "secret_AKHxxx", 6 | "redirect_uri": "https://service-8poxxx.tencentcs.com/release/auth", 7 | "telegram_token": "191:AAE-ApGxxx", 8 | "key": "yuvjtdcK1fn!f%SN)&!", 9 | "webhook_host": "https://service-8poxxx.tencentcs.com/release/release", 10 | "notion_version": "2021-08-16", 11 | "sentry_address": "https://fcdxxx" 12 | } 13 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import base64 3 | import os 4 | 5 | import requests 6 | import sentry_sdk 7 | from sentry_sdk.integrations.aiohttp import AioHttpIntegration 8 | from aiohttp import web 9 | from aiogram import Bot 10 | from aiogram.types import Message, ContentType 11 | from aiogram.contrib.middlewares.logging import LoggingMiddleware 12 | from aiogram.dispatcher import Dispatcher 13 | from aiogram.dispatcher.webhook import SendMessage 14 | from aiogram.utils.executor import set_webhook 15 | 16 | from utils.notion import create, update, get_database_id, delete_relation, CloudPiece 17 | from utils.encryption import AESCipher 18 | from utils.telegram import get_total_file_path 19 | 20 | conf = os.environ 21 | API_TOKEN = conf.get("telegram_token") 22 | CLIENT_ID = conf.get("client_id") 23 | CLIENT_SECRET = conf.get("client_secret") 24 | REDIRECT_URI = conf.get("redirect_uri") 25 | AES = AESCipher(conf.get("key")) 26 | 27 | sentry_sdk.init( 28 | conf.get("sentry_address"), 29 | traces_sample_rate=1.0, 30 | integrations=[AioHttpIntegration()] 31 | ) 32 | 33 | # webhook settings 34 | WEBHOOK_HOST = conf.get('webhook_host') 35 | WEBHOOK_PATH = '/echo' 36 | WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}" 37 | 38 | # webserver settings 39 | WEBAPP_HOST = '0.0.0.0' 40 | WEBAPP_PORT = 9000 41 | 42 | logging.basicConfig(level=logging.INFO) 43 | 44 | bot = Bot(token=API_TOKEN) 45 | dp = Dispatcher(bot) 46 | dp.middleware.setup(LoggingMiddleware()) 47 | unsupported = "暂不支持该消息类型的存储,目前仅支持文本、图片、视频、GIF、文件" 48 | 49 | 50 | @dp.message_handler(commands=['start']) 51 | async def start(message: Message): 52 | text = """ 53 | 欢迎使用【CloudPiece】 54 | CloudPiece 能够快速记录你的想法到 Notion 笔记中。快速记录,不流失任何一个灵感。 55 | 1. 请拷贝 [模板](https://joys.notion.site/fa90a1d7e8404e1286f66941dafd4155) 到自己的 Notion 中 56 | 2. 使用 /bind 命令授权 CloudPiece 访问,在授权页面选择你刚刚拷贝的模板(注:错误的选择将无法正常使用 CloudPiece) 57 | 3. 输入 test,你将收到 【已存储】的反馈,这时你的想法已经写入到 Notion,快去 Notion 看看吧~ 58 | 59 | 如果 CloudPiece 不能使你满意,你可以到 [github](https://github.com/JoysKang/CloudPiece/issues) 提 issues, 60 | 或到[留言板](https://joys.notion.site/c144f89764564f928c31f162e0ff307a) 留言, 61 | 或发邮件至 licoricepieces@gmail.com, 62 | 或直接使用 /unbind 进行解绑,然后到拷贝的模板页,点击右上角的 Share 按钮,从里边移除 CloudPiece。 63 | """ 64 | chat_id = str(message.chat.id) 65 | return SendMessage(chat_id=chat_id, text=text, parse_mode="Markdown", disable_web_page_preview=True) 66 | 67 | 68 | @dp.message_handler(commands=['bind']) 69 | async def bind(message: Message): 70 | """绑定""" 71 | username = message.chat.username 72 | chat_id = str(message.chat.id) 73 | if not create(name=username, chat_id=chat_id): 74 | return SendMessage(chat_id, "已绑定, 无需再次绑定") 75 | 76 | state = AES.encrypt(chat_id) # 加密 77 | reply_message = f"[点击授权](https://api.notion.com/v1/oauth/authorize?owner=user" \ 78 | f"&client_id={CLIENT_ID}" \ 79 | f"&redirect_uri={REDIRECT_URI}" \ 80 | "&response_type=code" \ 81 | f"&state={state})" 82 | return SendMessage(chat_id, reply_message, parse_mode="Markdown", disable_web_page_preview=True) 83 | 84 | 85 | @dp.message_handler(commands=['unbind']) 86 | async def unbind(message: Message): 87 | """解除绑定(删除 relation关系 )""" 88 | chat_id = str(message.chat.id) 89 | delete_relation(chat_id) 90 | 91 | return SendMessage(chat_id, "解绑完成,如需继续使用,请先使用 /bind 进行绑定") 92 | 93 | 94 | @dp.errors_handler() 95 | async def errors(exception): 96 | print(exception, "=====") 97 | 98 | 99 | @dp.message_handler(content_types=ContentType.PHOTO) 100 | async def photo_handler(message: Message): 101 | """ 102 | {"message_id": 286, "from": {"id": 682824243, "is_bot": false, "first_name": "F", "last_name": "joys", "username": "joyskaren", "language_code": "zh-hans"}, "chat": {"id": 682824243, "first_name": "F", "last_name": "joys", "username": "joyskaren", "type": "private"}, "date": 1631746843, "photo": [{"file_id": "AgACAgUAAxkBAAIBHmFCexuQ1bp0N47YwArKM5YcPdokAAJUrzEbhcgQVmmEKN_iB7NbAQADAgADcwADIAQ", "file_unique_id": "AQADVK8xG4XIEFZ4", "file_size": 1028, "width": 90, "height": 46}, {"file_id": "AgACAgUAAxkBAAIBHmFCexuQ1bp0N47YwArKM5YcPdokAAJUrzEbhcgQVmmEKN_iB7NbAQADAgADbQADIAQ", "file_unique_id": "AQADVK8xG4XIEFZy", "file_size": 9704, "width": 320, "height": 163}, {"file_id": "AgACAgUAAxkBAAIBHmFCexuQ1bp0N47YwArKM5YcPdokAAJUrzEbhcgQVmmEKN_iB7NbAQADAgADeAADIAQ", "file_unique_id": "AQADVK8xG4XIEFZ9", "file_size": 12657, "width": 436, "height": 222}], "caption": "压缩图片"} 103 | :param message: 104 | :return: 105 | """ 106 | chat_id = message.chat.id 107 | cloud_piece = CloudPiece(chat_id) 108 | if None in (cloud_piece.database_id, cloud_piece.access_token): 109 | return SendMessage(chat_id, "database_id or access_token lack") 110 | 111 | file_id = message.photo[-1].file_id 112 | file_info = await bot.get_file(file_id) 113 | file_path = get_total_file_path(file_info.file_path) 114 | cloud_piece.image(file_path, message.caption) 115 | result, url = cloud_piece.save() 116 | if result: 117 | return SendMessage(chat_id, f"已存储, [现在编辑]({url})", parse_mode="Markdown", disable_web_page_preview=True) 118 | 119 | return SendMessage(chat_id, "存储失败") 120 | 121 | 122 | @dp.message_handler(content_types=ContentType.DOCUMENT) 123 | async def document_handler(message: Message): 124 | """ 125 | 不压缩的图片,算文档 126 | {"message_id": 284, "from": {"id": 682824243, "is_bot": false, "first_name": "F", "last_name": "joys", "username": "joyskaren", "language_code": "zh-hans"}, "chat": {"id": 682824243, "first_name": "F", "last_name": "joys", "username": "joyskaren", "type": "private"}, "date": 1631746717, "document": {"file_name": "2021-09-15.201013.png", "mime_type": "image/png", "thumb": {"file_id": "AAMCBQADGQEAAgEcYUJ6nWHPFcqdm_jJ6w-p4AksE0AAAuwDAAKFyBhWMSxfrcFlbvcBAAdtAAMgBA", "file_unique_id": "AQAD7AMAAoXIGFZy", "file_size": 9208, "width": 320, "height": 163}, "file_id": "BQACAgUAAxkBAAIBHGFCep1hzxXKnZv4yesPqeAJLBNAAALsAwAChcgYVjEsX63BZW73IAQ", "file_unique_id": "AgAD7AMAAoXIGFY", "file_size": 33225}, "caption": "test"} 127 | 真是文件 128 | {"message_id": 285, "from": {"id": 682824243, "is_bot": false, "first_name": "F", "last_name": "joys", "username": "joyskaren", "language_code": "zh-hans"}, "chat": {"id": 682824243, "first_name": "F", "last_name": "joys", "username": "joyskaren", "type": "private"}, "date": 1631746783, "document": {"file_name": "新建文件.txt", "mime_type": "text/plain", "file_id": "BQACAgUAAxkBAAIBHWFCet8RB9iWOrzJ0z6SnIWI9LQhAALtAwAChcgYVsW0GgABkA8qWSAE", "file_unique_id": "AgAD7QMAAoXIGFY", "file_size": 5}, "caption": "文件"} 129 | :param message: 130 | :return: 131 | """ 132 | chat_id = message.chat.id 133 | cloud_piece = CloudPiece(chat_id) 134 | if None in (cloud_piece.database_id, cloud_piece.access_token): 135 | return SendMessage(chat_id, "database_id or access_token lack") 136 | 137 | file_id = message.document.file_id 138 | file_info = await bot.get_file(file_id) 139 | file_path = get_total_file_path(file_info.file_path) 140 | cloud_piece.document(file_path, message.caption) 141 | result, url = cloud_piece.save() 142 | if result: 143 | return SendMessage(chat_id, f"已存储, [现在编辑]({url})", parse_mode="Markdown", disable_web_page_preview=True) 144 | 145 | return SendMessage(chat_id, "存储失败") 146 | 147 | 148 | @dp.message_handler(content_types=ContentType.VIDEO) 149 | async def video_handler(message: Message): 150 | """ 151 | {"message_id": 287, "from": {"id": 682824243, "is_bot": false, "first_name": "F", "last_name": "joys", "username": "joyskaren", "language_code": "zh-hans"}, "chat": {"id": 682824243, "first_name": "F", "last_name": "joys", "username": "joyskaren", "type": "private"}, "date": 1631746989, "video": {"duration": 14, "width": 416, "height": 640, "file_name": "1 (106).mp4", "mime_type": "video/mp4", "thumb": {"file_id": "AAMCBQADGQEAAgEfYUJ7rSeorGrhyuNAO7UoI1juc6wAAu4DAAKFyBhWmITq7CWOlKkBAAdtAAMgBA", "file_unique_id": "AQAD7gMAAoXIGFZy", "file_size": 11560, "width": 208, "height": 320}, "file_id": "BAACAgUAAxkBAAIBH2FCe60nqKxq4crjQDu1KCNY7nOsAALuAwAChcgYVpiE6uwljpSpIAQ", "file_unique_id": "AgAD7gMAAoXIGFY", "file_size": 5698704}, "caption": "视频"} 152 | :param message: 153 | :return: 154 | """ 155 | chat_id = message.chat.id 156 | cloud_piece = CloudPiece(chat_id) 157 | if None in (cloud_piece.database_id, cloud_piece.access_token): 158 | return SendMessage(chat_id, "database_id or access_token lack") 159 | 160 | file_id = message.video.file_id 161 | file_info = await bot.get_file(file_id) 162 | file_path = get_total_file_path(file_info.file_path) 163 | cloud_piece.video(file_path, message.caption) 164 | result, url = cloud_piece.save() 165 | if result: 166 | return SendMessage(chat_id, f"已存储, [现在编辑]({url})", parse_mode="Markdown", disable_web_page_preview=True) 167 | 168 | return SendMessage(chat_id, "存储失败") 169 | 170 | 171 | @dp.message_handler(content_types=ContentType.ANIMATION) 172 | async def animation_handler(message: Message): 173 | """ 174 | {"message_id": 287, "from": {"id": 682824243, "is_bot": false, "first_name": "F", "last_name": "joys", "username": "joyskaren", "language_code": "zh-hans"}, "chat": {"id": 682824243, "first_name": "F", "last_name": "joys", "username": "joyskaren", "type": "private"}, "date": 1631746989, "video": {"duration": 14, "width": 416, "height": 640, "file_name": "1 (106).mp4", "mime_type": "video/mp4", "thumb": {"file_id": "AAMCBQADGQEAAgEfYUJ7rSeorGrhyuNAO7UoI1juc6wAAu4DAAKFyBhWmITq7CWOlKkBAAdtAAMgBA", "file_unique_id": "AQAD7gMAAoXIGFZy", "file_size": 11560, "width": 208, "height": 320}, "file_id": "BAACAgUAAxkBAAIBH2FCe60nqKxq4crjQDu1KCNY7nOsAALuAwAChcgYVpiE6uwljpSpIAQ", "file_unique_id": "AgAD7gMAAoXIGFY", "file_size": 5698704}, "caption": "视频"} 175 | :param message: 176 | :return: 177 | """ 178 | chat_id = message.chat.id 179 | cloud_piece = CloudPiece(chat_id) 180 | if None in (cloud_piece.database_id, cloud_piece.access_token): 181 | return SendMessage(chat_id, "database_id or access_token lack") 182 | 183 | file_id = message.animation.file_id 184 | file_info = await bot.get_file(file_id) 185 | file_path = get_total_file_path(file_info.file_path) 186 | cloud_piece.video(file_path, message.caption) 187 | result, url = cloud_piece.save() 188 | if result: 189 | return SendMessage(chat_id, f"已存储, [现在编辑]({url})", parse_mode="Markdown", disable_web_page_preview=True) 190 | 191 | return SendMessage(chat_id, "存储失败") 192 | 193 | 194 | @dp.message_handler(content_types=ContentType.LOCATION) 195 | async def location_handler(message: Message): 196 | """ 197 | 位置 198 | {"message_id": 289, "from": {"id": 682824243, "is_bot": false, "first_name": "F", "last_name": "joys", "username": "joyskaren", "language_code": "zh-hans"}, "chat": {"id": 682824243, "first_name": "F", "last_name": "joys", "username": "joyskaren", "type": "private"}, "date": 1631747382, "location": {"latitude": 36.129698, "longitude": 113.141452}} 199 | :param message: 200 | :return: 201 | """ 202 | chat_id = message.chat.id 203 | return SendMessage(chat_id, unsupported) 204 | 205 | 206 | @dp.message_handler(content_types=ContentType.TEXT) 207 | async def text_handler(message: Message): 208 | chat_id = message.chat.id 209 | cloud_piece = CloudPiece(chat_id) 210 | if None in (cloud_piece.database_id, cloud_piece.access_token): 211 | return SendMessage(chat_id, "database_id or access_token lack") 212 | 213 | if message.entities: 214 | offset = 0 215 | for entity in message.entities: 216 | if entity.type == "url": 217 | cloud_piece.text(message.text[offset: entity.offset]) # 前面的文本 218 | cloud_piece.bookmark(message.text[entity.offset: entity.length + entity.offset]) 219 | else: 220 | cloud_piece.text(message.text[offset: entity.length + entity.offset]) 221 | offset = entity.length + entity.offset 222 | result, url = cloud_piece.save() 223 | if result: 224 | return SendMessage(chat_id, f"已存储, [现在编辑]({url})", parse_mode="Markdown", disable_web_page_preview=True) 225 | 226 | cloud_piece.text(message.text) 227 | result, url = cloud_piece.save() 228 | if result: 229 | return SendMessage(chat_id, f"已存储, [现在编辑]({url})", parse_mode="Markdown", disable_web_page_preview=True) 230 | 231 | return SendMessage(chat_id, "存储失败") 232 | 233 | 234 | @dp.message_handler(content_types=ContentType.ANY) 235 | async def other_handler(message: Message): 236 | """ 237 | 链接 238 | {"message_id": 288, "from": {"id": 682824243, "is_bot": false, "first_name": "F", "last_name": "joys", "username": "joyskaren", "language_code": "zh-hans"}, "chat": {"id": 682824243, "first_name": "F", "last_name": "joys", "username": "joyskaren", "type": "private"}, "date": 1631747159, "text": "https://map.baidu.com/poi/%E6%96%B0%E4%B9%A1%E4%B8%9C%E7%AB%99/@12683140.43,4181023.18,11.87z?uid=024242f8281b5de0902ca33d&info_merge=1&isBizPoi=false&ugc_type=3&ugc_ver=1&device_ratio=2&compat=1&seckey=646f10cb77181888e7eef50bc07d45274c5fe0c85c4ae6a2b8a66de6bcc21f3371287d727a5322140481ce64c70ad1b657a6e09c31496fd4129b0c48c0873df5e65fdd5dad42907939e1c29f1e11ff580221bd14f507d16fad988972b921284165fbbccef2db3357a30d08caba9a19fdf2c10a5c840df1137ccbed3c5e40ca918cda30ea2f66f38a7e17e010fcbd4e928a8511b6e804161898d38f712965abbc0f6b83ff2f5fbc1c9570e45839f1841ba7270a4c39c64b6770fbd429074f3b1c6ff61418188ec88e01d6caae3961a0f895782a5894e76547d1e2e280a9ccbf9572ceb18c2badf993e2f815ce10e09c04&pcevaname=pc4.1&newfrom=zhuzhan_webmap&querytype=detailConInfo&da_src=shareurl", "entities": [{"type": "url", "offset": 0, "length": 762}]} 239 | 240 | 音频 241 | {"message_id": 292, "from": {"id": 682824243, "is_bot": false, "first_name": "F", "last_name": "joys", "username": "joyskaren", "language_code": "zh-hans"}, "chat": {"id": 682824243, "first_name": "F", "last_name": "joys", "username": "joyskaren", "type": "private"}, "date": 1631752942, "voice": {"duration": 2, "mime_type": "audio/ogg", "file_id": "AwACAgUAAxkBAAIBJGFCku7P2ja9BS39AAHLA_28X7ya8AAC-AMAAoXIGFZWhpP-i8pVYyAE", "file_unique_id": "AgAD-AMAAoXIGFY", "file_size": 8003}} 242 | :param message: 243 | :return: 244 | """ 245 | return SendMessage(message.chat.id, unsupported) 246 | 247 | 248 | async def on_startup(dp): 249 | await bot.set_webhook(WEBHOOK_URL) 250 | 251 | 252 | async def on_shutdown(dp): 253 | logging.warning('Shutting down..') 254 | 255 | await bot.delete_webhook() 256 | 257 | await dp.storage.close() 258 | await dp.storage.wait_closed() 259 | 260 | logging.warning('Bye!') 261 | 262 | 263 | async def auth(request): 264 | """授权回调""" 265 | # 向 https://api.notion.com/v1/oauth/token 发起请求 266 | code = request.rel_url.query["code"] 267 | data = { 268 | "grant_type": "authorization_code", 269 | "code": code, 270 | "redirect_uri": REDIRECT_URI 271 | } 272 | 273 | authorization = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode("utf-8")).decode("utf-8") 274 | headers = { 275 | 'content-type': 'application/json', 276 | 'Authorization': f'Basic {authorization}' 277 | } 278 | result = requests.post('https://api.notion.com/v1/oauth/token', json=data, headers=headers) 279 | if result.status_code != 200: 280 | print(result.content, "----") 281 | return web.json_response({"message": "Failure"}) 282 | 283 | json_data = result.json() 284 | # 根据 chat_id、code、json_data 更新数据库 285 | access_token = json_data.get('access_token') 286 | database_id = get_database_id(access_token) 287 | 288 | state = request.rel_url.query["state"] 289 | chat_id = AES.decrypt(state) # 解密 290 | update(chat_id=chat_id, access_token=access_token, database_id=database_id, code=code) 291 | 292 | SendMessage(chat_id=chat_id, text="授权成功") 293 | return web.json_response({"message": "Success"}) 294 | 295 | 296 | async def root(request): 297 | return web.json_response({"message": "Success"}) 298 | 299 | 300 | 301 | if __name__ == '__main__': 302 | # start 303 | app = web.Application() 304 | app.add_routes([web.get('/', root)]) 305 | app.add_routes([web.get('/auth', auth)]) 306 | 307 | executor = set_webhook( 308 | dispatcher=dp, 309 | webhook_path=WEBHOOK_PATH, 310 | on_startup=on_startup, 311 | on_shutdown=on_shutdown, 312 | skip_updates=True, 313 | web_app=app 314 | ) 315 | executor.run_app(host=WEBAPP_HOST, 316 | port=WEBAPP_PORT) 317 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.26.0 2 | starlette==0.16.0 3 | aiogram==2.14.3 4 | aiohttp==3.7.4.post0 5 | pycryptodome==3.10.1 6 | sentry_sdk==1.4.1 7 | gunicorn==20.1.0 8 | -------------------------------------------------------------------------------- /typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud-piece", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node dist/bot.js", 7 | "build": "tsc src/*.ts --outDir dist" 8 | }, 9 | "dependencies": { 10 | "@notionhq/client": "^1.0.4", 11 | "crypto-js": "^4.1.1", 12 | "koa": "^2.13.4", 13 | "koa-body": "^5.0.0", 14 | "telegraf": "^4.7.0", 15 | "typescript": "^4.6.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /typescript/src/bot.ts: -------------------------------------------------------------------------------- 1 | import {Telegraf} from 'telegraf' 2 | const Koa = require('koa') 3 | const koaBody = require('koa-body') 4 | const safeCompare = require('safe-compare') 5 | 6 | const { createRelation, deleteRelation } = require("./notion") 7 | const { encrypt, decrypt } = require("./encryption") 8 | 9 | 10 | const token = process.env.telegramToken 11 | if (token === undefined) { 12 | throw new Error('BOT_TOKEN must be provided!') 13 | } 14 | 15 | const bot = new Telegraf(token, { 16 | telegram: { webhookReply: true } 17 | }) 18 | 19 | bot.command('start', (ctx) => { 20 | // Explicit usage 21 | const text = ` 22 | 欢迎使用【CloudPiece】 23 | CloudPiece 能够快速记录你的想法到 Notion 笔记中。快速记录,不流失任何一个灵感。 24 | 1. 请拷贝 [模板](https://joys.notion.site/fa90a1d7e8404e1286f66941dafd4155) 到自己的 Notion 中 25 | 2. 使用 /bind 命令授权 CloudPiece 访问,在授权页面选择你刚刚拷贝的模板(注:错误的选择将无法正常使用 CloudPiece) 26 | 3. 输入 test,你将收到 【已存储】的反馈,这时你的想法已经写入到 Notion,快去 Notion 看看吧~ 27 | 28 | 如果 CloudPiece 不能使你满意,你可以到 [github](https://github.com/JoysKang/CloudPiece/issues) 提 issues, 29 | 或到[留言板](https://joys.notion.site/c144f89764564f928c31f162e0ff307a) 留言, 30 | 或发邮件至 licoricepieces@gmail.com, 31 | 或直接使用 /unbind 进行解绑,然后到拷贝的模板页,点击右上角的 Share 按钮,从里边移除 CloudPiece。 32 | ` 33 | ctx.telegram.sendMessage(ctx.message.chat.id, text, { 34 | parse_mode: 'Markdown', 35 | disable_web_page_preview: true, 36 | }) 37 | 38 | // Using context shortcut 39 | ctx.leaveChat() 40 | }) 41 | 42 | // 绑定 43 | bot.command('bind', async (ctx) => { 44 | // const username = ctx.message.chat.username 45 | const username = "" 46 | const chat_id = ctx.message.chat.id 47 | const isCreated = await createRelation(username, chat_id) 48 | if (!isCreated) { 49 | return ctx.reply('already bound, no need to bind again') 50 | } 51 | 52 | const state = encrypt(ctx.message.chat.id) 53 | const text = "[点击授权](https://api.notion.com/v1/oauth/authorize?owner=user&client_id=" + 54 | process.env.clientId + "&redirect_uri=" + process.env.redirectUri + "&response_type=code&state=" + state 55 | await ctx.telegram.sendMessage(ctx.message.chat.id, text, { 56 | parse_mode: 'Markdown', 57 | disable_web_page_preview: true, 58 | }) 59 | // Using context shortcut 60 | ctx.leaveChat() 61 | }) 62 | 63 | // 解绑 64 | bot.command('unbind', async (ctx) => { 65 | // Explicit usage 66 | await deleteRelation(ctx.message.chat.id) 67 | 68 | const text = "解绑完成,如需继续使用,请先使用 /bind 进行绑定" 69 | await ctx.telegram.sendMessage(ctx.message.chat.id, text, { 70 | parse_mode: 'Markdown', 71 | disable_web_page_preview: true, 72 | }) 73 | // Using context shortcut 74 | ctx.leaveChat() 75 | }) 76 | 77 | bot.on('text', (ctx) => { 78 | // Explicit usage 79 | ctx.telegram.sendMessage(ctx.message.chat.id, `Hello ${ctx.state.role}`) 80 | 81 | // Using context shortcut 82 | ctx.reply(`Hello ${ctx.state.role}`) 83 | }) 84 | 85 | bot.on('callback_query', (ctx) => { 86 | // Explicit usage 87 | ctx.telegram.answerCbQuery(ctx.callbackQuery.id) 88 | 89 | // Using context shortcut 90 | ctx.answerCbQuery() 91 | }) 92 | 93 | bot.on('inline_query', (ctx) => { 94 | const result = [] 95 | // Explicit usage 96 | ctx.telegram.answerInlineQuery(ctx.inlineQuery.id, result) 97 | 98 | // Using context shortcut 99 | ctx.answerInlineQuery(result) 100 | }) 101 | 102 | // bot.launch() 103 | 104 | const secretPath = `/telegraf/${bot.secretPathComponent()}` 105 | 106 | // webhook 107 | bot.telegram.setWebhook(`${process.env.webhookHost}${secretPath}`) 108 | 109 | // Enable graceful stop 110 | // process.once('SIGINT', () => bot.stop('SIGINT')) 111 | // process.once('SIGTERM', () => bot.stop('SIGTERM')) 112 | 113 | const app = new Koa() 114 | app.use(koaBody()) 115 | app.use(async (ctx, next) => { 116 | if (safeCompare(secretPath, ctx.url)) { 117 | await bot.handleUpdate(ctx.request.body) 118 | ctx.status = 200 119 | return 120 | } 121 | return next() 122 | }) 123 | const PORT = process.env.PORT || 3000; 124 | app.listen(PORT, () => { 125 | console.log(`Our app is running on port ${ PORT }`); 126 | }); 127 | -------------------------------------------------------------------------------- /typescript/src/encryption.ts: -------------------------------------------------------------------------------- 1 | const CryptoJS = require("crypto-js"); 2 | 3 | export function encrypt(text: string) { 4 | return CryptoJS.AES.encrypt(text, process.env.key).toString(); 5 | } 6 | 7 | export function decrypt(text: string) { 8 | return CryptoJS.AES.decrypt(text, process.env.key).toString(CryptoJS.enc.Utf8); 9 | } 10 | -------------------------------------------------------------------------------- /typescript/src/notion.ts: -------------------------------------------------------------------------------- 1 | const { Client } = require("@notionhq/client") 2 | 3 | const relationNotion = new Client({ auth: process.env.relationCode }); 4 | 5 | export async function createRelation(username: string, chatId: string): Promise { 6 | const [_, __, pageId] = await getDatabaseIdAndAccessToken(chatId) 7 | if (pageId) { 8 | return false 9 | } 10 | 11 | const pageArgs = { 12 | "parent": {"database_id": process.env.relationDatabaseId}, 13 | "properties": { 14 | "Name": { 15 | "title": [ 16 | { 17 | "text": { 18 | "content": username 19 | } 20 | } 21 | ] 22 | }, 23 | "ChatId": { 24 | "rich_text": [ 25 | { 26 | "text": { 27 | "content": chatId 28 | } 29 | } 30 | ] 31 | } 32 | }, 33 | } 34 | try { 35 | await relationNotion.pages.create(pageArgs) 36 | return true 37 | } catch (error) { 38 | console.log(error) 39 | return false 40 | } 41 | } 42 | 43 | export async function deleteRelation(chatId: string) { 44 | const [_, __, pageId] = await getDatabaseIdAndAccessToken(chatId) 45 | const response = await relationNotion.blocks.delete({ 46 | block_id: pageId, 47 | }); 48 | console.log(response); 49 | } 50 | 51 | export async function updateRelation(chatId: string, accessToken: string, databaseId: string, code: string) { 52 | const [_, __, pageId] = await getDatabaseIdAndAccessToken(chatId) 53 | const pageArgs = { 54 | "parent": {"database_id": process.env.relationDatabaseId}, 55 | "properties": {} 56 | } 57 | if (accessToken) { 58 | pageArgs.properties["AccessToken"] = { 59 | "rich_text": [ 60 | { 61 | "text": { 62 | "content": accessToken 63 | } 64 | } 65 | ] 66 | } 67 | } 68 | if (databaseId) { 69 | pageArgs.properties["DatabaseId"] = { 70 | "rich_text": [ 71 | { 72 | "text": { 73 | "content": databaseId 74 | } 75 | } 76 | ] 77 | } 78 | } 79 | if (code) { 80 | pageArgs.properties["Code"] = { 81 | "rich_text": [ 82 | { 83 | "text": { 84 | "content": code 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | 91 | await relationNotion.pages.update({ 92 | page_id: pageId, 93 | properties: pageArgs.properties 94 | }); 95 | } 96 | 97 | // 获取用户基础数据 98 | async function getDatabaseIdAndAccessToken(chatId: string): Promise { 99 | const response = await relationNotion.databases.query({ 100 | database_id: process.env.relationDatabaseId, 101 | filter: { 102 | or: [ 103 | { 104 | property: 'ChatId', 105 | rich_text: { 106 | equals: chatId, 107 | }, 108 | }, 109 | ], 110 | } 111 | }); 112 | if (response.results.length === 0) { 113 | console.log("No entry found") 114 | return [] 115 | } 116 | 117 | if (response.results[0].properties.DatabaseId.rich_text === null || 118 | response.results[0].properties.AccessToken.rich_text === null) { 119 | console.log("No entry found") 120 | return [] 121 | } 122 | 123 | let databaseId: string = '' 124 | if (response.results[0].properties.DatabaseId.rich_text.length !== 0) { 125 | databaseId = response.results[0].properties.DatabaseId.rich_text[0].plain_text 126 | } 127 | 128 | let accessToken: string = '' 129 | if (response.results[0].properties.AccessToken.rich_text.length !== 0) { 130 | accessToken = response.results[0].properties.AccessToken.rich_text[0].plain_text 131 | } 132 | 133 | const pageId: string = response.results[0].id 134 | return [databaseId, accessToken, pageId] 135 | } 136 | 137 | function writeTitle(title: string): { title: { text: { content: string } }[] } { 138 | return { 139 | "title": [ 140 | { 141 | "text": { 142 | "content": title, 143 | }, 144 | }, 145 | ] 146 | } 147 | } 148 | 149 | function writeIcon(): { type: string; emoji: string } { 150 | return { 151 | type: "emoji", 152 | emoji: "🥬" 153 | } 154 | } 155 | 156 | function writeCover(url: string): { type: string, external: { url: string } } { 157 | return { 158 | type: "external", 159 | external: { 160 | url: url 161 | } 162 | } 163 | } 164 | 165 | function writeText(text: string) { 166 | return { 167 | "object": "block", 168 | "type": "paragraph", 169 | "paragraph": { 170 | "rich_text": [ 171 | { 172 | "type": "text", 173 | "text": { 174 | "content": text 175 | } 176 | } 177 | ] 178 | } 179 | } 180 | } 181 | 182 | function writeImage(url: string) { 183 | return { 184 | "type": "image", 185 | "image": { 186 | "type": "external", 187 | "external": { 188 | "url": url 189 | } 190 | } 191 | } 192 | } 193 | 194 | function writeBookmark(url: string): { bookmark: { url: string }; type: string;} { 195 | return { 196 | "type": "bookmark", 197 | "bookmark": { 198 | "url": url 199 | } 200 | } 201 | } 202 | 203 | function writeMap(url: string) { 204 | return { 205 | "object": "block", 206 | "type": "embed", 207 | "embed": { 208 | "url": url 209 | } 210 | } 211 | } 212 | 213 | function writeDocument(url: string) { 214 | return { 215 | "type": "file", 216 | "file": { 217 | "type": "external", 218 | "external": { 219 | "url": url 220 | } 221 | } 222 | } 223 | } 224 | 225 | function writeVideo(url: string) { 226 | return { 227 | "type": "video", 228 | "video": { 229 | "type": "external", 230 | "external": { 231 | "url": url 232 | } 233 | } 234 | } 235 | } 236 | 237 | async function writePage(chatId: string, databaseId: string, accessToken: string, title: string, content: string): Promise { 238 | const pageArgs = { 239 | parent: { 240 | database_id: databaseId, 241 | }, 242 | properties: { 243 | }, 244 | children: [] 245 | } 246 | pageArgs["icon"] = writeIcon() 247 | pageArgs["cover"] = writeCover("https://upload.wikimedia.org/wikipedia/commons/6/62/Tuscankale.jpg") 248 | pageArgs["properties"]["Name"] = writeTitle(title) 249 | pageArgs["children"].push(writeText(content)) 250 | pageArgs["children"].push(writeBookmark("https://zh.odysseydao.com/pathways/intro-to-web3")) 251 | 252 | const clientNotion = new Client({ auth: accessToken }); 253 | try { 254 | await clientNotion.pages.create(pageArgs) 255 | return true 256 | } catch (error) { 257 | console.log(error) 258 | return false 259 | } 260 | } 261 | 262 | export async function writeNotion(chatId: string, title: string, content: string): Promise { 263 | const [databaseId, accessToken, _] = await getDatabaseIdAndAccessToken(chatId) 264 | if (databaseId === "" || accessToken === "") { 265 | return false 266 | } 267 | return await writePage(chatId, databaseId, accessToken, title, content) 268 | } 269 | 270 | // (async () => { 271 | // // const [databaseId, accessToken, pageId] = await getDatabaseIdAndAccessToken("682824244") 272 | // // console.log(await writePage("", databaseId, accessToken, "Tuscan Kale", "Lacinato kale is a variety of kale with a long tradition in Italian cuisine, especially that of Tuscany. It is also known as Tuscan kale, Italian kale, dinosaur kale, kale, flat back kale, palm tree kale, or black Tuscan palm.")) 273 | 274 | // await createRelation("joys", "682824244") 275 | // // await updateRelation("682824244", "682824244", process.env.relationDatabaseId, "joys") 276 | // // await deleteRelation("682824244") 277 | // })(); -------------------------------------------------------------------------------- /utils/conf.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def load_json(file_path="../conf.json"): 5 | try: 6 | with open(file_path, 'r') as file: 7 | return json.load(file) 8 | except: 9 | return {} 10 | -------------------------------------------------------------------------------- /utils/encryption.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from Crypto import Random 3 | import hashlib 4 | 5 | 6 | class AESCipher: 7 | def __init__(self, key): 8 | self.bs = 32 9 | self.key = bytes.fromhex(hashlib.sha256(key.encode()).hexdigest()) 10 | 11 | def encrypt(self, raw): 12 | content_padding = self._pad(raw).encode() 13 | iv = Random.new().read(AES.block_size) 14 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 15 | encrypt_bytes = cipher.encrypt(content_padding) 16 | return (iv + encrypt_bytes).hex() 17 | 18 | def decrypt(self, enc): 19 | enc = bytes.fromhex(enc) 20 | iv = enc[:AES.block_size] 21 | enc = enc[AES.block_size:] 22 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 23 | encrypt_bytes = enc 24 | decrypt_bytes = cipher.decrypt(encrypt_bytes) 25 | return self._unpad(decrypt_bytes.decode('utf-8')) 26 | 27 | def _pad(self, s): 28 | return s + (self.bs - len(s.encode()) % self.bs) * chr(self.bs - len(s.encode()) % self.bs) 29 | 30 | def _unpad(self, s): 31 | return s[:-ord(s[-1:])] 32 | 33 | 34 | if __name__ == "__main__": 35 | key = "UwO&fqEUiEZW%p7KP8)Xwv1qDxbfOmSD" 36 | text = "123" 37 | AES = AESCipher(key) 38 | etext = AES.encrypt(text) 39 | print(etext) 40 | print(AES.decrypt(etext)) 41 | -------------------------------------------------------------------------------- /utils/latitude.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class Degree(object): 5 | def __init__(self): 6 | pass 7 | 8 | @staticmethod 9 | def dd_to_dms(dd): 10 | """ 11 | 十进制度转为度分秒 12 | Paramaters: 13 | dd : 十进制度 14 | Return: 15 | dms : 度分秒 16 | """ 17 | degree = int(float(dd)) 18 | minute = int((float(dd) - degree) * 60) 19 | second = round((float(dd) - degree - float(minute) / 60) * 3600, 2) 20 | return f'{degree}°{minute}' + '\'' + str(second) + "\"" 21 | 22 | @staticmethod 23 | def dms_to_dd(degree, minute, second): 24 | """ 25 | 度分秒转为十进制度 26 | Paramater: 27 | degree : 度 28 | minute : 分 29 | second : 秒 30 | Return: 31 | dd : 十进制度 32 | """ 33 | return degree + minute / 60 + second / 60 / 60 34 | 35 | @staticmethod 36 | def parse_dms(dms): 37 | """ 38 | 解析度分秒字符串 39 | Paramater: 40 | dms : 度分秒字符串 41 | Returns: 42 | degree : 度 43 | minute : 分 44 | second : 秒 45 | """ 46 | parts = re.split('[°′″]', dms) 47 | degree = float(parts[0]) 48 | minute = float(parts[1]) 49 | second = float(parts[2]) 50 | return {"degree": degree, "minute": minute, "second": second} 51 | 52 | 53 | if __name__ == '__main__': 54 | print(Degree.dd_to_dms(36.129698)) 55 | -------------------------------------------------------------------------------- /utils/notion.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import requests 5 | 6 | 7 | conf = os.environ 8 | relation_database_id = conf.get('relation_database_id') 9 | relation_code = conf.get('relation_code') 10 | notion_version = conf.get('notion_version') 11 | 12 | 13 | class CloudPiece: 14 | """ 15 | notion 16 | """ 17 | 18 | def __init__(self, chat_id): 19 | self.chat_id = chat_id 20 | self.database_id, self.access_token, self.page_id = get_data(self.chat_id) 21 | self.headers = { 22 | 'Authorization': f'Bearer {self.access_token}', 23 | 'Notion-Version': '2021-05-13', 24 | 'Content-Type': 'application/json', 25 | } 26 | self.body = { 27 | "parent": {"database_id": self.database_id}, 28 | "properties": { 29 | "Name": { 30 | "title": [ 31 | { 32 | "text": { 33 | "content": " " 34 | } 35 | } 36 | ] 37 | } 38 | }, 39 | "children": [] 40 | } 41 | 42 | def get_title(self): 43 | pass 44 | 45 | def video(self, url, caption=""): 46 | if caption: 47 | self.text(caption, is_save=False) 48 | 49 | self.body["children"].append({ 50 | "type": "video", 51 | "video": { 52 | "type": "external", 53 | "external": { 54 | "url": url 55 | } 56 | } 57 | }) 58 | 59 | def document(self, url, caption=""): 60 | if caption: 61 | self.text(caption, is_save=False) 62 | 63 | self.body["children"].append({ 64 | "type": "file", 65 | "file": { 66 | "type": "external", 67 | "external": { 68 | "url": url 69 | } 70 | } 71 | }) 72 | 73 | def image(self, url, caption=""): 74 | if caption: 75 | self.text(caption, is_save=False) 76 | 77 | self.body["children"].append({ 78 | "type": "image", 79 | "image": { 80 | "type": "external", 81 | "external": { 82 | "url": url 83 | } 84 | } 85 | }) 86 | 87 | def text(self, text, is_save=True): 88 | self.body["children"].append({ 89 | "object": "block", 90 | "type": "paragraph", 91 | "paragraph": { 92 | "text": [ 93 | { 94 | "type": "text", 95 | "text": { 96 | "content": text 97 | } 98 | } 99 | ] 100 | } 101 | }) 102 | 103 | def bookmark(self, url): 104 | self.body["children"].append({ 105 | "object": "block", 106 | "type": "bookmark", 107 | "bookmark": { 108 | "url": url 109 | } 110 | }) 111 | 112 | def maps(self, url, caption=""): 113 | if caption: 114 | self.text(caption, is_save=False) 115 | 116 | self.body["children"].append({ 117 | "object": "block", 118 | "type": "embed", 119 | "embed": { 120 | "url": url 121 | } 122 | }) 123 | 124 | def save(self): 125 | response = requests.post('https://api.notion.com/v1/pages', headers=self.headers, 126 | data=json.dumps(self.body)) 127 | 128 | if response.status_code == 200: 129 | return True, json.loads(response.content).get('url') 130 | 131 | return False, "" 132 | 133 | 134 | def get_data(chat_id): 135 | """根据 chat_id 获取 database_id、code""" 136 | database_id, access_token, page_id = None, None, None 137 | _data = '{ "filter": { "or": [ { "property": "ChatId", "rich_text": {"equals": "' + str(chat_id) + '"}} ] } }' 138 | _data = _data.encode() 139 | 140 | headers = { 141 | 'Authorization': f'Bearer {relation_code}', 142 | 'Notion-Version': f'{notion_version}', 143 | 'Content-Type': 'application/json', 144 | } 145 | response = requests.post( 146 | f'https://api.notion.com/v1/databases/{relation_database_id}/query', 147 | headers=headers, data=_data) 148 | if response.status_code != 200: 149 | return "", "", "" 150 | 151 | content = json.loads(response.content) 152 | try: 153 | result = content["results"][0] 154 | except IndexError: 155 | return database_id, access_token, page_id 156 | 157 | database_id = result["properties"]["DatabaseId"]["rich_text"] 158 | access_token = result["properties"]["AccessToken"]["rich_text"] 159 | 160 | if database_id and access_token: 161 | database_id = database_id[0]["plain_text"] 162 | access_token = access_token[0]["plain_text"] 163 | page_id = result["id"] # 存在 page_id 则说明当前 chat_id 已有记录,不需要重复写 164 | 165 | return database_id, access_token, page_id 166 | 167 | 168 | def write(database_id, code, text): 169 | headers = { 170 | 'Authorization': f'Bearer {code}', 171 | 'Notion-Version': '2021-05-13', 172 | 'Content-Type': 'application/json', 173 | } 174 | data = { 175 | "parent": {"database_id": database_id}, 176 | "properties": { 177 | "Name": { 178 | "title": [ 179 | { 180 | "text": { 181 | "content": " " 182 | } 183 | } 184 | ] 185 | } 186 | }, 187 | "children": [ 188 | { 189 | "object": "block", 190 | "type": "paragraph", 191 | "paragraph": { 192 | "text": [ 193 | { 194 | "type": "text", 195 | "text": { 196 | "content": text 197 | } 198 | } 199 | ] 200 | } 201 | } 202 | ] 203 | } 204 | 205 | response = requests.post('https://api.notion.com/v1/pages', headers=headers, data=json.dumps(data)) 206 | if response.status_code == 200: 207 | return True 208 | 209 | return False 210 | 211 | 212 | def update(chat_id="", access_token="", database_id="", code=""): 213 | headers = { 214 | 'Authorization': f'Bearer {relation_code}', 215 | 'Notion-Version': f'{notion_version}', 216 | 'Content-Type': 'application/json', 217 | } 218 | 219 | page_id = get_page_id(chat_id) 220 | 221 | data = { 222 | "parent": {"database_id": relation_database_id}, 223 | "properties": {} 224 | } 225 | if not (access_token or database_id or code): 226 | return False 227 | 228 | if access_token: 229 | data['properties']["AccessToken"] = { 230 | "rich_text": [ 231 | { 232 | "text": { 233 | "content": access_token 234 | } 235 | } 236 | ] 237 | } 238 | 239 | if database_id: 240 | data['properties']["DatabaseId"] = { 241 | "rich_text": [ 242 | { 243 | "text": { 244 | "content": database_id 245 | } 246 | } 247 | ] 248 | } 249 | 250 | if code: 251 | data['properties']["Code"] = { 252 | "rich_text": [ 253 | { 254 | "text": { 255 | "content": code 256 | } 257 | } 258 | ] 259 | } 260 | 261 | response = requests.patch(f'https://api.notion.com/v1/pages/{page_id}', 262 | headers=headers, data=json.dumps(data)) 263 | if response.status_code == 200: 264 | return True 265 | 266 | return False 267 | 268 | 269 | def create(name, chat_id=""): 270 | """ 271 | 更新或创建数据库记录 272 | """ 273 | headers = { 274 | 'Authorization': f'Bearer {relation_code}', 275 | 'Notion-Version': f'{notion_version}', 276 | 'Content-Type': 'application/json', 277 | } 278 | 279 | # 先判断 chat_id 是否已存在,不存在再写入,已存在的直接跳过 280 | _, _, page_id = get_data(chat_id) 281 | if page_id: 282 | return False 283 | 284 | data = { 285 | "parent": {"database_id": relation_database_id}, 286 | "properties": { 287 | "Name": { 288 | "title": [ 289 | { 290 | "text": { 291 | "content": name 292 | } 293 | } 294 | ] 295 | }, 296 | "ChatId": { 297 | "rich_text": [ 298 | { 299 | "text": { 300 | "content": chat_id 301 | } 302 | } 303 | ] 304 | } 305 | }, 306 | } 307 | 308 | response = requests.post('https://api.notion.com/v1/pages', headers=headers, data=json.dumps(data)) 309 | if response.status_code == 200: 310 | return True 311 | 312 | return False 313 | 314 | 315 | def delete_relation(chat_id): 316 | """删除记录""" 317 | headers = { 318 | 'Authorization': f'Bearer {relation_code}', 319 | 'Notion-Version': f'{notion_version}', 320 | 'Content-Type': 'application/json', 321 | } 322 | data = { 323 | "parent": {"database_id": relation_database_id}, 324 | "properties": {}, 325 | "archived": True 326 | } 327 | 328 | page_id = get_page_id(chat_id) 329 | if not page_id: 330 | return True 331 | 332 | response = requests.patch(f'https://api.notion.com/v1/pages/{page_id}', 333 | headers=headers, data=json.dumps(data)) 334 | if response.status_code == 200: 335 | return True 336 | 337 | return False 338 | 339 | 340 | def get_page_id(chat_id=None): 341 | """根据 chat_id 获取 database_id、code""" 342 | _data = '{ "filter": { "or": [ { "property": "ChatId", "rich_text": {"equals": "' + str(chat_id) + '"}} ] } }' 343 | _data = _data.encode() 344 | 345 | headers = { 346 | 'Authorization': f'Bearer {relation_code}', 347 | 'Notion-Version': f'{notion_version}', 348 | 'Content-Type': 'application/json', 349 | } 350 | response = requests.post( 351 | f'https://api.notion.com/v1/databases/{relation_database_id}/query', 352 | headers=headers, data=_data) 353 | if response.status_code != 200: 354 | return "", "" 355 | 356 | content = json.loads(response.content) 357 | result = content["results"][0] 358 | return result["id"] 359 | 360 | 361 | def get_database_id(access_token=""): 362 | headers = { 363 | 'Authorization': f'Bearer {access_token}', 364 | 'Notion-Version': f'{notion_version}', 365 | 'Content-Type': 'application/json', 366 | } 367 | response = requests.post('https://api.notion.com/v1/search', headers=headers) 368 | if response.status_code != 200: 369 | return "", "" 370 | 371 | content = json.loads(response.content) 372 | result = content["results"][0] 373 | return result["id"].replace("-", "") 374 | 375 | 376 | if __name__ == "__main__": 377 | chat_id = "366052963" 378 | # cloud_piece = CloudPiece(chat_id) 379 | # cloud_piece.maps("https://www.google.com/maps/place/36%C2%B007'46.9%22N+113%C2%B008'29.2%22E") 380 | # cloud_piece.text("test") 381 | # cloud_piece.bookmark("https://juejin.cn/post/7013221168249307150") 382 | # cloud_piece.video("https://", "text") 383 | _, _, page_id = get_data(chat_id) 384 | -------------------------------------------------------------------------------- /utils/telegram.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def get_total_file_path(file_path=""): 5 | """获取 file path""" 6 | return f"https://api.telegram.org/file/bot{os.environ.get('telegram_token')}/{file_path}" 7 | --------------------------------------------------------------------------------