From 797a09f583352fd749ab5ffea87b17c4aca09189 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:33:27 -0300 Subject: [PATCH] feat: process profile image --- package.json | 1 - requirements.txt | 1 + src/main/events/index.ts | 1 + .../events/profile/process-profile-image.ts | 11 ++ src/main/events/profile/update-profile.ts | 68 +++++---- src/main/services/download/python-instance.ts | 8 + src/preload/index.ts | 2 + src/renderer/src/declaration.d.ts | 3 + .../user-edit-profile.tsx | 139 ++++++++++-------- torrent-client/main.py | 35 ++++- torrent-client/profile_image_processor.py | 39 +++++ yarn.lock | 9 -- 12 files changed, 210 insertions(+), 107 deletions(-) create mode 100644 src/main/events/profile/process-profile-image.ts create mode 100644 torrent-client/profile_image_processor.py diff --git a/package.json b/package.json index 1727e3833..321f3af6c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "electron-log": "^5.1.4", "electron-updater": "^6.1.8", "fetch-cookie": "^3.0.1", - "file-type": "^19.0.0", "flexsearch": "^0.7.43", "i18next": "^23.11.2", "i18next-browser-languagedetector": "^7.2.1", diff --git a/requirements.txt b/requirements.txt index b14880038..3685495b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ cx_Logging; sys_platform == 'win32' lief; sys_platform == 'win32' pywin32; sys_platform == 'win32' psutil +Pillow diff --git a/src/main/events/index.ts b/src/main/events/index.ts index b876c9442..bea1984c7 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -53,6 +53,7 @@ import "./profile/get-me"; import "./profile/undo-friendship"; import "./profile/update-friend-request"; import "./profile/update-profile"; +import "./profile/process-profile-image"; import "./profile/send-friend-request"; import { isPortableVersion } from "@main/helpers"; diff --git a/src/main/events/profile/process-profile-image.ts b/src/main/events/profile/process-profile-image.ts new file mode 100644 index 000000000..506592272 --- /dev/null +++ b/src/main/events/profile/process-profile-image.ts @@ -0,0 +1,11 @@ +import { registerEvent } from "../register-event"; +import { PythonInstance } from "@main/services"; + +const processProfileImage = async ( + _event: Electron.IpcMainInvokeEvent, + path: string +) => { + return await PythonInstance.processProfileImage(path); +}; + +registerEvent("processProfileImage", processProfileImage); diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts index 50d2ab66e..c3ec1337d 100644 --- a/src/main/events/profile/update-profile.ts +++ b/src/main/events/profile/update-profile.ts @@ -1,46 +1,54 @@ import { registerEvent } from "../register-event"; -import { HydraApi } from "@main/services"; -import axios from "axios"; +import { HydraApi, PythonInstance } from "@main/services"; import fs from "node:fs"; import path from "node:path"; -import { fileTypeFromFile } from "file-type"; -import { UpdateProfileProps, UserProfile } from "@types"; +import type { UpdateProfileProps, UserProfile } from "@types"; +import { omit } from "lodash-es"; +import axios from "axios"; + +interface PresignedResponse { + presignedUrl: string; + profileImageUrl: string; +} const patchUserProfile = async (updateProfile: UpdateProfileProps) => { - return HydraApi.patch("/profile", updateProfile); + return HydraApi.patch("/profile", updateProfile); +}; + +const getNewProfileImageUrl = async (localImageUrl: string) => { + const { imagePath, mimeType } = + await PythonInstance.processProfileImage(localImageUrl); + + const stats = fs.statSync(imagePath); + const fileBuffer = fs.readFileSync(imagePath); + const fileSizeInBytes = stats.size; + + const { presignedUrl, profileImageUrl } = + await HydraApi.post(`/presigned-urls/profile-image`, { + imageExt: path.extname(imagePath).slice(1), + imageLength: fileSizeInBytes, + }); + + await axios.put(presignedUrl, fileBuffer, { + headers: { + "Content-Type": mimeType, + }, + }); + + return profileImageUrl; }; const updateProfile = async ( _event: Electron.IpcMainInvokeEvent, updateProfile: UpdateProfileProps -): Promise => { +) => { if (!updateProfile.profileImageUrl) { - return patchUserProfile(updateProfile); + return patchUserProfile(omit(updateProfile, "profileImageUrl")); } - const newProfileImagePath = updateProfile.profileImageUrl; - - const stats = fs.statSync(newProfileImagePath); - const fileBuffer = fs.readFileSync(newProfileImagePath); - const fileSizeInBytes = stats.size; - - const profileImageUrl = await HydraApi.post(`/presigned-urls/profile-image`, { - imageExt: path.extname(newProfileImagePath).slice(1), - imageLength: fileSizeInBytes, - }) - .then(async (preSignedResponse) => { - const { presignedUrl, profileImageUrl } = preSignedResponse; - - const mimeType = await fileTypeFromFile(newProfileImagePath); - - await axios.put(presignedUrl, fileBuffer, { - headers: { - "Content-Type": mimeType?.mime, - }, - }); - return profileImageUrl as string; - }) - .catch(() => undefined); + const profileImageUrl = await getNewProfileImageUrl( + updateProfile.profileImageUrl + ).catch(() => undefined); return patchUserProfile({ ...updateProfile, profileImageUrl }); }; diff --git a/src/main/services/download/python-instance.ts b/src/main/services/download/python-instance.ts index 37ec17dbf..4a41c2dc3 100644 --- a/src/main/services/download/python-instance.ts +++ b/src/main/services/download/python-instance.ts @@ -166,6 +166,14 @@ export class PythonInstance { this.downloadingGameId = -1; } + static async processProfileImage(imagePath: string) { + return this.rpc + .post<{ imagePath: string; mimeType: string }>("/profile-image", { + image_path: imagePath, + }) + .then((response) => response.data); + } + private static async handleRpcError(_error: unknown) { await this.rpc.get("/healthcheck").catch(() => { logger.error( diff --git a/src/preload/index.ts b/src/preload/index.ts index c98ed25f3..28552ae7a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -141,6 +141,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("undoFriendship", userId), updateProfile: (updateProfile: UpdateProfileProps) => ipcRenderer.invoke("updateProfile", updateProfile), + processProfileImage: (imagePath: string) => + ipcRenderer.invoke("processProfileImage", imagePath), getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"), updateFriendRequest: (userId: string, action: FriendRequestAction) => ipcRenderer.invoke("updateFriendRequest", userId, action), diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index f1f6eba91..28bf415ef 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -144,6 +144,9 @@ declare global { getMe: () => Promise; undoFriendship: (userId: string) => Promise; updateProfile: (updateProfile: UpdateProfileProps) => Promise; + processProfileImage: ( + path: string + ) => Promise<{ imagePath: string; mimeType: string }>; getFriendRequests: () => Promise; updateFriendRequest: ( userId: string, diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx index 6ecdb8a14..6c2b1cbde 100644 --- a/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx +++ b/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx @@ -5,7 +5,8 @@ import { UserProfile } from "@types"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "../user.css"; -import { SPACING_UNIT } from "@renderer/theme.css"; +import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; export interface UserEditProfileProps { userProfile: UserProfile; @@ -21,9 +22,10 @@ export const UserEditProfile = ({ const [form, setForm] = useState({ displayName: userProfile.displayName, profileVisibility: userProfile.profileVisibility, - imageProfileUrl: null as string | null, + profileImageUrl: null as string | null, }); const [isSaving, setIsSaving] = useState(false); + const [isLoadingImage, setIsLoadingImage] = useState(false); const { patchUser } = useUserDetails(); @@ -53,9 +55,16 @@ export const UserEditProfile = ({ }); if (filePaths && filePaths.length > 0) { - const path = filePaths[0]; + setIsLoadingImage(true); - setForm({ ...form, imageProfileUrl: path }); + const { imagePath } = await window.electron + .processProfileImage(filePaths[0]) + .catch(() => { + return { imagePath: null }; + }) + .finally(() => setIsLoadingImage(false)); + + setForm({ ...form, profileImageUrl: imagePath }); } }; @@ -85,66 +94,78 @@ export const UserEditProfile = ({ }); }; - const avatarUrl = useMemo(() => { - if (form.imageProfileUrl) return `local:${form.imageProfileUrl}`; + const profileImageUrl = useMemo(() => { + if (form.profileImageUrl) return `local:${form.profileImageUrl}`; if (userProfile.profileImageUrl) return userProfile.profileImageUrl; return null; }, [form, userProfile]); + const profileImageContent = () => { + if (isLoadingImage) { + return ; + } + + if (profileImageUrl) { + return ( + {userProfile.displayName} + ); + } + + return ; + }; + return ( -
- - - setForm({ ...form, displayName: e.target.value })} - /> - - ({ - key: visiblity.value, - value: visiblity.value, - label: visiblity.label, - }))} - /> - - - + + + setForm({ ...form, displayName: e.target.value })} + /> + + ({ + key: visiblity.value, + value: visiblity.value, + label: visiblity.label, + }))} + /> + + + + ); }; diff --git a/torrent-client/main.py b/torrent-client/main.py index a2ea190b6..7fbc49d89 100644 --- a/torrent-client/main.py +++ b/torrent-client/main.py @@ -4,6 +4,7 @@ import urllib.parse import psutil from torrent_downloader import TorrentDownloader +from profile_image_processor import ProfileImageProcessor torrent_port = sys.argv[1] http_port = sys.argv[2] @@ -73,16 +74,30 @@ def do_GET(self): def do_POST(self): global torrent_downloader - if self.path == "/action": - if self.headers.get(self.rpc_password_header) != rpc_password: - self.send_response(401) - self.end_headers() - return + if self.headers.get(self.rpc_password_header) != rpc_password: + self.send_response(401) + self.end_headers() + return - content_length = int(self.headers['Content-Length']) - post_data = self.rfile.read(content_length) - data = json.loads(post_data.decode('utf-8')) + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + data = json.loads(post_data.decode('utf-8')) + if self.path == "/profile-image": + parsed_image_path = data['image_path'] + + try: + parsed_image_path, mime_type = ProfileImageProcessor.process_image(parsed_image_path) + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + + self.wfile.write(json.dumps({'imagePath': parsed_image_path, 'mimeType': mime_type}).encode('utf-8')) + except: + self.send_response(400) + self.end_headers() + + elif self.path == "/action": if torrent_downloader is None: torrent_downloader = TorrentDownloader(torrent_port) @@ -99,6 +114,10 @@ def do_POST(self): self.send_response(200) self.end_headers() + else: + self.send_response(404) + self.end_headers() + if __name__ == "__main__": httpd = HTTPServer(("", int(http_port)), Handler) diff --git a/torrent-client/profile_image_processor.py b/torrent-client/profile_image_processor.py new file mode 100644 index 000000000..7d4976db4 --- /dev/null +++ b/torrent-client/profile_image_processor.py @@ -0,0 +1,39 @@ +from PIL import Image +import tempfile +import os, uuid + +class ProfileImageProcessor: + + @staticmethod + def get_parsed_image_data(image_path): + Image.MAX_IMAGE_PIXELS = 933120000 + + image = Image.open(image_path) + + try: + image.seek(1) + except EOFError: + mime_type = image.get_format_mimetype() + return image_path, mime_type + else: + newUUID = str(uuid.uuid4()) + new_image_path = os.path.join(tempfile.gettempdir(), newUUID) + ".webp" + image.save(new_image_path) + + new_image = Image.open(new_image_path) + mime_type = new_image.get_format_mimetype() + + return new_image_path, mime_type + + + @staticmethod + def process_image(image_path): + return ProfileImageProcessor.get_parsed_image_data(image_path) + + +if __name__ == "__main__": + result = ProfileImageProcessor.get_parsed_image_data("D:\Imagens\807b5c4b02e765bb4930b7c66662ef4b.gif") + print(result) + + result = ProfileImageProcessor.get_parsed_image_data("D:/Imagens/20240416_233352~2.png") + print(result) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b4040484e..9c8464385 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4342,15 +4342,6 @@ file-type@^18.7.0: strtok3 "^7.0.0" token-types "^5.0.1" -file-type@^19.0.0: - version "19.0.0" - resolved "https://registry.npmjs.org/file-type/-/file-type-19.0.0.tgz" - integrity sha512-s7cxa7/leUWLiXO78DVVfBVse+milos9FitauDLG1pI7lNaJ2+5lzPnr2N24ym+84HVwJL6hVuGfgVE+ALvU8Q== - dependencies: - readable-web-to-node-stream "^3.0.2" - strtok3 "^7.0.0" - token-types "^5.0.1" - file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz"