├── __init__.py ├── main.py ├── auth.py ├── README.md └── api.py /__init__.py: -------------------------------------------------------------------------------- 1 | # locket/__init__.py 2 | from .auth import Auth 3 | from .api import LocketAPI 4 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from locket import Auth, LocketAPI 3 | import json 4 | 5 | 6 | auth = Auth('email', 'password') 7 | token = auth.get_token() 8 | 9 | api = LocketAPI(token) 10 | 11 | account_info = api.GetAccountInfo() 12 | # print(json.dumps(account_info, indent=4)) 13 | momment = api.getLastMoment() 14 | print(momment) -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | # locket/auth.py 2 | import json 3 | import uuid 4 | import requests 5 | 6 | class Auth: 7 | def __init__(self, email, password): 8 | self.email = email 9 | self.password = password 10 | self.device_id = self.generate_device_id() 11 | self.token = None 12 | 13 | @staticmethod 14 | def generate_device_id(): 15 | return str(uuid.uuid4()).upper() 16 | 17 | def create_token(self): 18 | request_data = { 19 | "email": self.email, 20 | "password": self.password, 21 | "clientType": "CLIENT_TYPE_IOS", 22 | "returnSecureToken": True 23 | } 24 | 25 | url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=AIzaSyCQngaaXQIfJaH0aS2l7REgIjD7nL431So" 26 | headers = { 27 | "Accept": "*/*", 28 | "Accept-Encoding": "gzip, deflate, br", 29 | "Accept-Language": "en", 30 | "baggage": "sentry-environment=production,sentry-public_key=78fa64317f434fd89d9cc728dd168f50,sentry-release=com.locket.Locket@1.82.0+3,sentry-trace_id=90310ccc8ddd4d059b83321054b6245b", 31 | "Connection": "keep-alive", 32 | "Content-Length": "117", 33 | "Content-Type": "application/json", 34 | "Host": "www.googleapis.com", 35 | "sentry-trace": "90310ccc8ddd4d059b83321054b6245b-3a4920b34e94401d-0", 36 | "User-Agent": "FirebaseAuth.iOS/10.23.1 com.locket.Locket/1.82.0 iPhone/18.0 hw/iPhone12_1", 37 | "X-Client-Version": "iOS/FirebaseSDK/10.23.1/FirebaseCore-iOS", 38 | "X-Firebase-AppCheck": "eyJraWQiOiJNbjVDS1EiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxOjY0MTAyOTA3NjA4Mzppb3M6Y2M4ZWI0NjI5MGQ2OWIyMzRmYTYwNiIsImF1ZCI6WyJwcm9qZWN0c1wvNjQxMDI5MDc2MDgzIiwicHJvamVjdHNcL2xvY2tldC00MjUyYSJdLCJwcm92aWRlciI6ImRldmljZV9jaGVja19kZXZpY2VfaWRlbnRpZmljYXRpb24iLCJpc3MiOiJodHRwczpcL1wvZmlyZWJhc2VhcHBjaGVjay5nb29nbGVhcGlzLmNvbVwvNjQxMDI5MDc2MDgzIiwiZXhwIjoxNzIyMTY3ODk4LCJpYXQiOjE3MjIxNjQyOTgsImp0aSI6ImlHUGlsT1dDZGg4Mll3UTJXRC1neEpXeWY5TU9RRFhHcU5OR3AzTjFmRGcifQ.lqTOJfdoYLpZwYeeXtRliCdkVT7HMd7_Lj-d44BNTGuxSYPIa9yVAR4upu3vbZSh9mVHYS8kJGYtMqjP-L6YXsk_qsV_gzKC2IhVAV6KbPDRHdevMfBC6fRiOSVn7vt749GVFdZqAuDCXhCILsaMhvgDBgZoDilgAPtpNwyjz-VtRB7OdOUbuKTCqdoSOX0SJWVUMyuI8nH0-unY--YRctunK8JHZDxBaM_ahVggYPWBCpzxq9Yeq8VSPhadG_tGNaADStYPaeeUkZ7DajwWqH5ze6ESpuFNgAigwPxCM735_ZiPeD7zHYwppQA9uqTWszK9v9OvWtFCsgCEe22O8awbNbuEBTKJpDQ8xvZe8iEYyhfUPncER3S-b1CmuXR7tFCdTgQe5j7NGWjFvN_CnL7D2nudLwxWlpqwASCHvHyi8HBaJ5GpgriTLXAAinY48RukRDBi9HwEzpRecELX05KTD2lTOfQCjKyGpfG2VUHP5Xm36YbA3iqTDoDXWMvV", 39 | "X-Firebase-GMPID": "1:641029076083:ios:cc8eb46290d69b234fa606", 40 | "X-Ios-Bundle-Identifier": "com.locket.Locket" 41 | } 42 | 43 | response = requests.post(url, headers=headers, json=request_data) 44 | 45 | if response.ok: 46 | self.token = response.json().get('idToken') 47 | return self.token 48 | else: 49 | raise Exception('Failed to login') 50 | 51 | def get_token(self): 52 | if not self.token: 53 | self.create_token() 54 | return self.token 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hành Trình Tạo API Module Cho Locket Camera: Những Khám Phá Đầy Bất Ngờ 2 | 3 | ## Ngày 1: Khởi Đầu Với Những Kỳ Vọng Lớn Lao 4 | 5 | Ý tưởng về việc tạo ra một module API cho Locket Camera đến với tôi vào một buổi chiều đầy nắng. Tôi háo hức bắt tay vào việc, hy vọng sẽ nhanh chóng đạt được những kết quả đầu tiên. Mục tiêu đầu tiên của tôi là `getUserByUsername` – một API lấy thông tin tài khoản từ username. Tôi tự tin rằng chỉ cần bắt request và đưa token vào header là mọi thứ sẽ hoạt động. 6 | 7 | Nhưng cuộc đời không như là mơ. Token trong phần authorization của Locket Camera chỉ có hiệu lực trong 15 phút. Điều này khiến tôi cảm thấy mệt mỏi và quyết định tạm dừng, để lại bài toán khó cho ngày hôm sau. 8 | 9 | ## Ngày 2: Thử Thách Và Thành Công Đầu Tiên 10 | 11 | Sau một đêm suy nghĩ, tôi nhận ra rằng phải tìm cách lấy token authorization một cách tự động. Tôi đã tập trung vào API login và thử nghiệm với `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=AIzaSyCQngaaXQIfJaH0aS2l7REgIjD7nL431So`. Payload của request bao gồm email và password. Cuối cùng, tôi nhận được token và thêm nó vào header authorization. Thật tuyệt vời! Request đến `getUserByUsername` đã thành công. 12 | 13 | Tuy nhiên, niềm vui chưa kéo dài được bao lâu. Tôi phát hiện ra rằng cần phải có thêm một giá trị trong header là `X-Firebase-AppCheck` để thực sự hoàn tất các request khác. Tôi thử mã hóa payload dạng JWT với thuật toán HSA256 nhưng không thành công. 14 | 15 | ## Ngày 3: Khám Phá Những Bí Mật Của API 16 | 17 | Không nản lòng, tôi tiếp tục tìm cách khác. Tôi nhận ra rằng một số API không cần header `X-Firebase-AppCheck`. Điều này mở ra một cơ hội lớn cho tôi: 18 | 19 | DƯỚI ĐÂY LÀ LIST API CẦN 'X-FIREBASE-CHECKAPP' Ở HEADER: 20 | 21 | 1. **API `getLastMomment`**: Chỉ cần token từ login là có thể hoạt động mà không cần `X-Firebase-AppCheck`. 22 | - URL: `https://api.locketcamera.com/getLatestMomentV2` 23 | - Payload: 24 | ```json 25 | { 26 | "data": { 27 | "excluded_users": [], 28 | "sync_token": "5oImx73nFjTb1FTRkBC2", 29 | "last_fetch": { 30 | "@type": "type.googleapis.com/google.protobuf.Int64Value", 31 | "value": "1722073779706" 32 | }, 33 | "fetch_streak": false, 34 | "should_count_missed_moments": true 35 | } 36 | } 37 | ``` 38 | 39 | 2. **API `changeProfileInfo`**: Thay đổi thông tin hồ sơ. 40 | - URL: `https://api.locketcamera.com/changeProfileInfo` 41 | - Payload 1: 42 | ```json 43 | { 44 | "data": { 45 | "badge": "locket_gold" 46 | } 47 | } 48 | ``` 49 | - Payload 2: 50 | ```json 51 | { 52 | "data": { 53 | "last_name": "Nguyen", 54 | "first_name": "Anh" 55 | } 56 | } 57 | ``` 58 | 59 | 3. **API `updateEmailAddress`**: Cập nhật địa chỉ email. 60 | - URL: `https://api.locketcamera.com/updateEmailAddress` 61 | - Payload: 62 | ```json 63 | { 64 | "data": { 65 | "email": "example@gmail.com" 66 | } 67 | } 68 | ``` 69 | 70 | 4. **API `sendVerificationCode`**: Đổi số điện thoại. 71 | - URL: `https://api.locketcamera.com/sendVerificationCode` 72 | - Payload: 73 | ```json 74 | { 75 | "data": { 76 | "phone": "+84987654321", 77 | "operation": "change_number", 78 | "platform": "ios", 79 | "is_retry": false 80 | } 81 | } 82 | ``` 83 | 84 | 5. **API `sendChatMessageV2`**: Gửi tin nhắn. 85 | - URL: `https://api.locketcamera.com/sendChatMessageV2` 86 | - Payload: 87 | ```json 88 | { 89 | "data": { 90 | "receiver_uid": "bmLzyn7UQTPtN7budBYirrWdDUb2", 91 | "client_token": "72652F96-0C4D-4CFE-9D59-B3251029B897", 92 | "msg": "Ê bro", 93 | "moment_uid": null, 94 | "from_memory": false 95 | } 96 | } 97 | ``` 98 | 99 | 6. **API `createAccountWithEmailPassword`**: Tạo tài khoản mới. 100 | - URL: `https://api.locketcamera.com/createAccountWithEmailPassword` 101 | - Payload: 102 | ```json 103 | { 104 | "data": { 105 | "password": "your_password", 106 | "client_token": "9e4add9a8ac5b41328cff18e93aa0ff87a814c80", 107 | "client_email_verif": true, 108 | "email": "example@gmail.com", 109 | "platform": "ios" 110 | } 111 | } 112 | ``` 113 | 114 | 7. **API `deleteUserAccount`**: Xóa tài khoản người dùng. 115 | - URL: `https://api.locketcamera.com/deleteUserAccount` 116 | - Chỉ cần token authorization và `X-Firebase-AppCheck`. 117 | -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | class LocketAPI: 5 | def __init__(self, token): 6 | self.token = token 7 | # Store common headers in a dictionary for reuse 8 | self.headers = { 9 | 'Accept': '*/*', 10 | 'Accept-Language': 'en-GB,en;q=0.9', 11 | 'Authorization': f'Bearer {self.token}', 12 | 'Connection': 'keep-alive', 13 | 'Content-Type': 'application/json', 14 | 'X-Client-Version': 'iOS/FirebaseSDK/10.23.1/FirebaseCore-iOS', 15 | 'X-Firebase-GMPID': '1:641029076083:ios:cc8eb46290d69b234fa606', 16 | 'X-Ios-Bundle-Identifier': 'com.locket.Locket', 17 | 'X-Firebase-AppCheck': ( 18 | 'eyJraWQiOiJNbjVDS1EiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.' 19 | 'eyJzdWIiOiIxOjY0MTAyOTA3NjA4Mzppb3M6Y2M4ZWI0NjI5MGQ2OWIyMzRmYTYwNiIsImF1ZCI6WyJwcm9qZWN0c1wvNjQxMDI5MDc2MDgzIiwicHJvamVjdHNcL2xvY2tldC00MjUyYSJdLCJwcm92aWRlciI6ImRldmljZV9jaGVja19kZXZpY2VfaWRlbnRpZmljYXRpb24iLCJpc3MiOiJodHRwczpcL1wvZmlyZWJhc2VhcHBjaGVjay5nb29nbGVhcGlzLmNvbVwvNjQxMDI5MDc2MDgzIiwiZXhwIjoxNzIyMjQwNjcwLCJpYXQiOjE3MjIyMzcwNzAsImp0aSI6ImFMTmF3aHlBc3E2a2ROT1FRTS1PT1FwX2gyTlU1ZDZGZUdIcUZoYTJZWXMifQ.' 20 | 'C1dXXEB_4q1-hWNkEV66HmycPNRiTHLn3nBoVrwmIEQ2opJ6S9rO4h7_K2_EdsMQkut_p-dGU8GiWZyBLi6MohzIfANfWggYS_Et2l6ZjCGJish-lt6FlIForpe4PAnG6OPreEL1qyzjFqD5IBN0FvdKuhEFMpDwBHQeSuubpkfRaki67jxR016cAZy6VDb42H2dqTH2t7rhwr5VCzErtzEKm711DTrFm0Rxgnvk8TcqOhjno6CDkUvfFc4RYMDmPVIuuX6H8zNBDVcvR5LFmZD5eo38lUwwQU1BoyQfgEMXp2w86MjtYm6KrF7U9TUfrgMz9I5e66oFBn5vqIUE594Pi7jmkcxbt_mW29FH3B4HIIAzvI-4WrVgGSkVidq6kZGKDfBt5NjxBYzfDiOtWtnUyUJmziZAbXayrYkRoJP2g8DS2Dsc-NvwIXVV_29YdgxYFIW1PjhTp2gmXMVTb4uHHUaMmd0j4Y4NgtgPwcVswSwawgy3e6C6-K01X6Xx' 21 | ) 22 | } 23 | 24 | def getUserByUsername(self, username): 25 | if not username: 26 | raise ValueError("Username is required") 27 | 28 | request_payload = { 29 | "data": { 30 | "username": username, 31 | } 32 | } 33 | 34 | response = requests.post( 35 | 'https://api.locketcamera.com/getUserByUsername', 36 | headers=self.headers, 37 | json=request_payload 38 | ) 39 | 40 | if response.ok: 41 | return response.json() 42 | else: 43 | raise Exception(f'API request failed with status code {response.status_code}: {response.text}') 44 | 45 | def changeNameAccount(self, last="", first=""): 46 | """Changes the first and last name of the account. 47 | 48 | Args: 49 | last (str, optional): The new last name. Defaults to "". 50 | first (str, optional): The new first name. Defaults to "". 51 | 52 | Returns: 53 | dict: The JSON response from the API if successful. 54 | 55 | Raises: 56 | Exception: If the API request fails. 57 | """ 58 | request_payload = { 59 | "data": { 60 | "last_name": last, 61 | "first_name": first, 62 | } 63 | } 64 | 65 | response = requests.post( 66 | 'https://api.locketcamera.com/changeProfileInfo', 67 | headers=self.headers, 68 | json=request_payload 69 | ) 70 | 71 | if response.ok: 72 | return response.json() 73 | else: 74 | raise Exception(f'API request failed with status code {response.status_code}: {response.text}') 75 | 76 | def GetAccountInfo(self): 77 | """Gets the account info using the provided token. 78 | 79 | Returns: 80 | dict: The JSON response from the API if successful. 81 | 82 | Raises: 83 | Exception: If the API request fails. 84 | """ 85 | url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo?key=AIzaSyCQngaaXQIfJaH0aS2l7REgIjD7nL431So" 86 | headers = { 87 | "Accept": "*/*", 88 | "Accept-Encoding": "gzip, deflate, br", 89 | "Accept-Language": "en", 90 | "Content-Type": "application/json", 91 | "Host": "www.googleapis.com", 92 | "User-Agent": "FirebaseAuth.iOS/10.23.1 com.locket.Locket/1.82.0 iPhone/18.0 hw/iPhone12_1", 93 | "X-Client-Version": "iOS/FirebaseSDK/10.23.1/FirebaseCore-iOS", 94 | "X-Firebase-GMPID": "1:641029076083:ios:cc8eb46290d69b234fa606", 95 | "X-Ios-Bundle-Identifier": "com.locket.Locket" 96 | } 97 | request_payload = { 98 | "idToken": self.token 99 | } 100 | 101 | response = requests.post(url, headers=headers, json=request_payload) 102 | 103 | if response.ok: 104 | return response.json() 105 | else: 106 | raise Exception(f'API request failed with status code {response.status_code}: {response.text}') 107 | 108 | def getLastMoment(self): 109 | """Gets the latest moment using the provided token. 110 | 111 | Returns: 112 | dict: The JSON response from the API if successful. 113 | 114 | Raises: 115 | Exception: If the API request fails. 116 | """ 117 | request_payload = { 118 | "data": { 119 | "excluded_users": [], 120 | "fetch_streak": False, 121 | "should_count_missed_moments": True 122 | } 123 | } 124 | 125 | response = requests.post( 126 | 'https://api.locketcamera.com/getLatestMomentV2', 127 | headers=self.headers, 128 | json=request_payload 129 | ) 130 | 131 | if response.ok: 132 | return response.json() 133 | else: 134 | raise Exception(f'API request failed with status code {response.status_code}: {response.text}') 135 | --------------------------------------------------------------------------------