diff options
author | clarkzjw <[email protected]> | 2023-02-28 23:27:35 -0800 |
---|---|---|
committer | clarkzjw <[email protected]> | 2023-02-28 23:27:35 -0800 |
commit | e1e8c54eafd61d31aa520593ece8a68d79dbdeeb (patch) | |
tree | 5b82e2636985d3052aed39b22c6c752718600c2c | |
parent | e0a69eb031316ae145f1c40319bceb8ec520ad44 (diff) | |
download | photo-e1e8c54eafd61d31aa520593ece8a68d79dbdeeb.tar.gz |
add python telegram bot to upload photos to cloudflare r2feature/bot
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | bot.py | 178 | ||||
-rw-r--r-- | requirements.txt | 21 |
3 files changed, 200 insertions, 0 deletions
@@ -3,3 +3,4 @@ _site/ | |||
3 | .DS_Store | 3 | .DS_Store |
4 | .ruby-version | 4 | .ruby-version |
5 | .jekyll-cache/ | 5 | .jekyll-cache/ |
6 | .venv/ | ||
@@ -0,0 +1,178 @@ | |||
1 | import io | ||
2 | import os | ||
3 | import logging | ||
4 | import traceback | ||
5 | from PIL import Image | ||
6 | import boto3 | ||
7 | from telegram import Update | ||
8 | from telegram.constants import ParseMode | ||
9 | from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters | ||
10 | from peewee import * | ||
11 | from uuid import uuid4 | ||
12 | from datetime import datetime | ||
13 | import json | ||
14 | |||
15 | db = SqliteDatabase("database/photos.db") | ||
16 | db.connect(reuse_if_open=True) | ||
17 | |||
18 | |||
19 | class BaseModel(Model): | ||
20 | class Meta: | ||
21 | database = db | ||
22 | |||
23 | |||
24 | class Photo(BaseModel): | ||
25 | guid = CharField(unique=True, primary_key=True) | ||
26 | fileId = CharField(max_length=256) | ||
27 | width = IntegerField() | ||
28 | height = IntegerField() | ||
29 | ratio = FloatField() | ||
30 | orientation = CharField(max_length=128) | ||
31 | path = CharField(max_length=256) | ||
32 | caption = CharField(max_length=256) | ||
33 | alt = CharField(max_length=256) | ||
34 | createdAt = DateTimeField() | ||
35 | uploadedAt = DateTimeField() | ||
36 | |||
37 | |||
38 | with db.connection_context(): | ||
39 | db.create_tables([Photo]) | ||
40 | |||
41 | logging.basicConfig( | ||
42 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO | ||
43 | ) | ||
44 | |||
45 | logger = logging.getLogger(__name__) | ||
46 | |||
47 | |||
48 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||
49 | await update.message.reply_text("This is a bot to output image in square shape") | ||
50 | |||
51 | |||
52 | bucket_name = "pixel-jinwei-me" | ||
53 | cf_account_id = os.getenv("CF_ACCOUNT_ID") | ||
54 | aws_access_key_id = os.getenv("CF_R2_KEY_ID") | ||
55 | aws_secret_access_key = os.getenv("CF_R2_ACCESS_KEY_SECRET") | ||
56 | |||
57 | |||
58 | def write_json() -> bool: | ||
59 | with db.connection_context(): | ||
60 | photos = Photo.select() | ||
61 | results = [] | ||
62 | for photo in photos: | ||
63 | results.append({ | ||
64 | "guid": photo.guid, | ||
65 | "fileId": photo.fileId, | ||
66 | "width": photo.width, | ||
67 | "height": photo.height, | ||
68 | "ratio": photo.ratio, | ||
69 | "orientation": photo.orientation, | ||
70 | "path": photo.path, | ||
71 | "caption": photo.caption, | ||
72 | "alt": photo.alt, | ||
73 | "createdAt": photo.createdAt.strftime("%Y-%m-%dT%H:%M:%S.000Z"), | ||
74 | "uploadedAt": photo.uploadedAt.strftime("%Y-%m-%dT%H:%M:%S.000Z") | ||
75 | }) | ||
76 | with open("database/photos.json", "w") as f: | ||
77 | f.write(json.dumps(results)) | ||
78 | return True | ||
79 | |||
80 | |||
81 | def upload_to_s3(key_name: str, file: bytes): | ||
82 | endpoint_ca_central = "https://{}.r2.cloudflarestorage.com".format(cf_account_id) | ||
83 | |||
84 | client = boto3.client("s3", | ||
85 | region_name="auto", | ||
86 | endpoint_url=endpoint_ca_central, | ||
87 | aws_access_key_id=aws_access_key_id, | ||
88 | aws_secret_access_key=aws_secret_access_key) | ||
89 | |||
90 | response = client.list_buckets() | ||
91 | buckets = [b["Name"] for b in response['Buckets']] | ||
92 | |||
93 | if bucket_name not in buckets: | ||
94 | print("{} doesn't exist".format(bucket_name)) | ||
95 | print('Existing buckets:') | ||
96 | for bucket in response['Buckets']: | ||
97 | print(f' {bucket["Name"]}') | ||
98 | return False | ||
99 | |||
100 | response = client.put_object(Body=file, Bucket=bucket_name, Key=key_name, ContentType="image/webp") | ||
101 | status = response["ResponseMetadata"]["HTTPStatusCode"] | ||
102 | if status == 200: | ||
103 | print("upload {} to {} succeed".format(key_name, bucket_name)) | ||
104 | return True | ||
105 | else: | ||
106 | print("upload {} to {} failed, status: {}".format(key_name, bucket_name, status)) | ||
107 | return False | ||
108 | |||
109 | |||
110 | async def process(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||
111 | chat_id = update.message.chat_id | ||
112 | |||
113 | if update.message.document is not None: | ||
114 | names = update.message.document.file_name.split(".") | ||
115 | file_ext = names[1] | ||
116 | filename = names[0] | ||
117 | |||
118 | if str.upper(file_ext) not in ("JPG", "JPEG", "PNG"): | ||
119 | await context.bot.send_message(chat_id, "Image extension `{}` not supported".format(file_ext), | ||
120 | parse_mode=ParseMode.MARKDOWN_V2) | ||
121 | return | ||
122 | |||
123 | file = await update.message.effective_attachment.get_file() | ||
124 | else: | ||
125 | return | ||
126 | |||
127 | await context.bot.send_message(chat_id, "Processing `{}`".format(filename), parse_mode=ParseMode.MARKDOWN_V2) | ||
128 | |||
129 | img = io.BytesIO() | ||
130 | await file.download_to_memory(img) | ||
131 | |||
132 | try: | ||
133 | im = Image.open(img) | ||
134 | output = io.BytesIO() | ||
135 | im.save(output, format="webp", lossless=True, quality=100) | ||
136 | |||
137 | now = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") | ||
138 | key_name = "{}-{}.webp".format(now, filename) | ||
139 | |||
140 | with db.connection_context(): | ||
141 | photo = Photo.create(guid=str.upper(str(uuid4())), | ||
142 | fileId=key_name, | ||
143 | width=im.width, | ||
144 | height=im.height, | ||
145 | ratio=im.width / im.height, | ||
146 | orientation="landscape" if im.width > im.height else "portrait", | ||
147 | path="https://pixelstatic.jinwei.me/{}".format(key_name), | ||
148 | caption="dsadas", | ||
149 | alt="dsada", | ||
150 | createdAt=datetime.now(), | ||
151 | uploadedAt=datetime.now()) | ||
152 | |||
153 | output.seek(0) | ||
154 | upload_to_s3(photo.fileId, output.read()) | ||
155 | |||
156 | write_json() | ||
157 | await update.message.reply_markdown_v2(text="Sending processed result") | ||
158 | |||
159 | await context.bot.send_document(chat_id=update.message.chat_id, | ||
160 | filename="{}-result.{}".format(filename, file_ext), | ||
161 | document=output.getvalue()) | ||
162 | |||
163 | except Exception as e: | ||
164 | await update.message.reply_markdown_v2(text="Error:\n```{}```".format(traceback.format_exc())) | ||
165 | |||
166 | |||
167 | def main() -> None: | ||
168 | tg_token = os.getenv("TG_TOKEN") | ||
169 | application = Application.builder().token(tg_token).build() | ||
170 | |||
171 | application.add_handler(CommandHandler("start", start)) | ||
172 | application.add_handler(MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, process)) | ||
173 | |||
174 | application.run_polling() | ||
175 | |||
176 | |||
177 | if __name__ == "__main__": | ||
178 | main() | ||
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..788075c --- /dev/null +++ b/requirements.txt | |||
@@ -0,0 +1,21 @@ | |||
1 | anyio==3.6.2 | ||
2 | boto3==1.26.81 | ||
3 | botocore==1.29.81 | ||
4 | certifi==2022.12.7 | ||
5 | h11==0.14.0 | ||
6 | h2==4.1.0 | ||
7 | hpack==4.0.0 | ||
8 | httpcore==0.16.3 | ||
9 | httpx==0.23.3 | ||
10 | hyperframe==6.0.1 | ||
11 | idna==3.4 | ||
12 | jmespath==1.0.1 | ||
13 | peewee==3.16.0 | ||
14 | Pillow==9.4.0 | ||
15 | python-dateutil==2.8.2 | ||
16 | python-telegram-bot==20.1 | ||
17 | rfc3986==1.5.0 | ||
18 | s3transfer==0.6.0 | ||
19 | six==1.16.0 | ||
20 | sniffio==1.3.0 | ||
21 | urllib3==1.26.14 | ||