├── .gitignore ├── README.md ├── bereal ├── __init__.py ├── bereal.py ├── constants.py └── models │ ├── comment.py │ ├── feed.py │ ├── post.py │ ├── realmoji.py │ └── user.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | __pycache__/ 4 | **/__pycache__/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bereal API 2 | 3 | ## Installation 4 | For now, you have to manually download the `bereal/` folder and add it to your project to use the API. 5 | 1. Download this repository (`git clone https://github.com/othema/bereal-python.git`) 6 | 2. Move the `bereal/` folder to your project 7 | 8 | ## Usage 9 | All functions of the API are rooted in the `BeReal()` class. To start, instantiate it somewhere in your code. 10 | ```python 11 | from bereal import BeReal 12 | 13 | bereal = BeReal() 14 | ``` 15 | 16 | ### Login 17 | To use any functions of the API you must login first. There are two ways of doing this. 18 | #### Login through phone verification 19 | You would use this method if you haven't already logged in. 20 | ```python 21 | from bereal import BeReal 22 | 23 | bereal = BeReal() 24 | bereal.login.send_code("") 25 | bereal.login.verify_code(input("Enter verification code: ")) 26 | ``` 27 | 28 | #### Login through saved ID's 29 | You would use this method if you have already logged in the user and you don't want to send another verification code. 30 | ```python 31 | from bereal import BeReal 32 | 33 | bereal = BeReal() 34 | bereal.login.with_tokens( 35 | "", 36 | "" 37 | ) 38 | ``` 39 | 40 | Your token and refresh token can be accessed through the client after a successful login like this: 41 | ```python 42 | from bereal import BeReal 43 | 44 | bereal = BeReal() 45 | bereal.token 46 | bereal.refresh_token 47 | ``` 48 | 49 | ### Making a post 50 | To make a post, you must pass in two file paths to images with one being the front camera image, and one being the back camera image. 51 | ```python 52 | from bereal import BeReal 53 | 54 | bereal = BeReal() 55 | 56 | # [Login] 57 | 58 | new_post = bereal.post_bereal("[front_image].jpg", "[back_image].jpg") # Returns a Post() object 59 | new_post.post_id 60 | ``` 61 | 62 | ### Accessing your profile data 63 | Your profile data can be accessed using the `me()` method. Here is an example how: 64 | ```python 65 | from bereal import BeReal 66 | 67 | bereal = BeReal() 68 | 69 | # [Login] 70 | 71 | me = bereal.me() 72 | 73 | me.user_id 74 | me.username 75 | me.profile_picture # URL to your profile picture 76 | me.full_name 77 | me.phone_number 78 | me.birthday # A datetime object 79 | me.realmojis # Array of Realmoji() objects saved to your account 80 | ``` 81 | 82 | ### Accessing your feed 83 | As you may know, there are two feeds in BeReal. The friend feed and the discovery feed. 84 | 85 | ```python 86 | from bereal import BeReal 87 | 88 | bereal = BeReal() 89 | 90 | # [Login] 91 | 92 | friend_feed = bereal.feed.friends() 93 | discovery_feed = bereal.feed.discovery() 94 | ``` 95 | 96 | Each function returns a list of `Post()` objects. 97 | 98 | ### Refreshing tokens 99 | After a while, a login token can expire. To overcome this, you can call the `refresh()` method to generate a new login and refresh token without having to send another verification code. 100 | ```python 101 | from bereal import BeReal 102 | 103 | bereal = BeReal() 104 | 105 | # [Login] 106 | 107 | bereal.refresh() 108 | 109 | bereal.token 110 | bereal.refresh_token 111 | ``` 112 | You should save these tokens in a file if you want to allow the user to stay logged in throughout multiple uses of the program. 113 | 114 | ### `Post()` objects 115 | Post objects can be returned from a feed retrieval and they contain data about a post. 116 | ```python 117 | post = friend_feed[0] 118 | 119 | # Methods 120 | post.add_comment("") # Adds a comment to the post from the logged in user 121 | 122 | # Attributes 123 | post.post_id 124 | post.back_camera 125 | post.front_camera 126 | post.caption # The caption associated with the post 127 | post.user # A User() object of the user who posted 128 | post.creation_time # A datetime object 129 | post.realmojis # An array of Realemoji() objects 130 | post.is_public # If the post is viewable on the discovery feed 131 | post.retakes # Amount of retakes 132 | post.comments # An array of Comment() objects 133 | ``` 134 | 135 | ### `Comment()` objects 136 | **TODO**: Add deletion of comments through a `delete()` method 137 | 138 | ```python 139 | comment = post.comments[0] 140 | 141 | # Attributes 142 | comment.comment_id 143 | comment.user # The author of the comment 144 | comment.creation_time # A datetime object 145 | comment.body # The comment text 146 | ``` 147 | 148 | ### `Realmoji()` objects 149 | Realmojis are reactions to a BeReal. A realmoji is an image of a user posing as one of 5 emojis: 150 | - 👍 151 | - 😀 152 | - 😲 153 | - 😍 154 | - 😂 155 | 156 | #### Accessing your saved realmojis 157 | You can have saved realmojis on your account. 158 | ```python 159 | from bereal import BeReal 160 | 161 | bereal = BeReal() 162 | 163 | # [Login] 164 | 165 | saved_realmoji = bereal.me().realmojis[0] 166 | saved_realmoji.url # URL of the realmoji image 167 | saved_realmoji.emoji # 👍, 😀, 😲, 😍 or 😂 168 | ``` 169 | 170 | #### Accessing realmojis on a post 171 | ```python 172 | realmoji = post.realmojis[0] 173 | 174 | realmoji.emoji # 👍, 😀, 😲, 😍 or 😂 175 | realmoji.url # URL of the realmoji image 176 | realmoji.user # User who reacted with that realmoji 177 | realmoji.time # Datetime the post was reacted with the realmoji 178 | ``` 179 | -------------------------------------------------------------------------------- /bereal/__init__.py: -------------------------------------------------------------------------------- 1 | from bereal.bereal import BeReal 2 | from bereal.constants import * 3 | -------------------------------------------------------------------------------- /bereal/bereal.py: -------------------------------------------------------------------------------- 1 | import pendulum 2 | import requests 3 | from uuid import uuid4 4 | from bereal.constants import * 5 | from bereal.models.feed import Feed 6 | from bereal.models.post import Post 7 | from bereal.models.user import Me 8 | from PIL import Image 9 | from io import BytesIO 10 | from datetime import datetime 11 | from urllib.parse import quote_plus 12 | from json import dumps 13 | 14 | 15 | class BeReal: 16 | def __init__(self): 17 | self.login = Login(self) 18 | 19 | self.refresh_token = None 20 | self.token = None 21 | 22 | self.feed = None 23 | 24 | def post_bereal(self, front_file, back_file, caption=None): 25 | # Thanks to: https://github.com/notmarek/BeFake/ 26 | # And: https://github.com/s-alad/toofake/ 27 | 28 | def extension(image): 29 | mime_type = Image.MIME[image.format] 30 | if mime_type != "image/jpeg": 31 | if not image.mode == "RGB": 32 | image = image.convert("RGB") 33 | return image 34 | 35 | def get_data(image): 36 | image_data = BytesIO() 37 | image.save(image_data, format="JPEG", quality=90) 38 | return image_data.getvalue() 39 | 40 | def load_file(path): 41 | with open(path, "rb") as f: 42 | image = extension(Image.open(BytesIO(f.read()))) 43 | size = image.size 44 | data = get_data(image) 45 | return data, size 46 | 47 | def upload_image(file_data, front_camera): 48 | name = f"Photos/{self.me().user_id}/bereal/{uuid4()}-{int(datetime.now().timestamp())}{'-secondary' if front_camera else ''}.jpg" 49 | data = { 50 | "cacheControl": "public,max-age=172800", 51 | "contentType": "image/webp", 52 | "metadata": {"type": "bereal"}, 53 | "name": name 54 | } 55 | headers = { 56 | "x-goog-upload-protocol": "resumable", 57 | "x-goog-upload-command": "start", 58 | "x-firebase-storage-version": "ios/9.4.0", 59 | "x-goog-upload-content-type": "image/webp", 60 | "content-type": "application/json", 61 | "x-firebase-gmpid": "1:405768487586:ios:28c4df089ca92b89", 62 | "Authorization": f"Firebase {self.token}", 63 | "x-goog-upload-content-length": str(len(file_data)), 64 | } 65 | params = {"uploadType": "resumable", "name": name} 66 | 67 | uri = f"https://firebasestorage.googleapis.com/v0/b/storage.bere.al/o/{quote_plus(name)}" 68 | initial_res = requests.post(uri, headers=headers, params=params, data=dumps(data)) 69 | 70 | if initial_res.status_code != 200: 71 | raise Exception(f"Error initiating upload: {initial_res.status_code}") 72 | 73 | upload_url = initial_res.headers["x-goog-upload-url"] 74 | upload_headers = { 75 | "x-goog-upload-command": "upload, finalize", 76 | "x-goog-upload-protocol": "resumable", 77 | "x-goog-upload-offset": "0", 78 | "content-type": "image/jpeg", 79 | } 80 | upload_res = requests.put( 81 | url=upload_url, 82 | headers=upload_headers, 83 | data=file_data 84 | ) 85 | if upload_res.status_code != 200: 86 | raise Exception(f"Error uploading image: {upload_res.status_code}, {upload_res.text}") 87 | return upload_res.json() 88 | 89 | front_data, front_size = load_file(front_file) 90 | back_data, back_size = load_file(back_file) 91 | front_upload = upload_image(front_data, True) 92 | back_upload = upload_image(back_data, False) 93 | 94 | front_url = f"https://{front_upload['bucket']}/{front_upload['name']}" 95 | back_url = f"https://{back_upload['bucket']}/{back_upload['name']}" 96 | 97 | now = pendulum.now() 98 | taken_at = f"{now.to_date_string()}T{now.to_time_string()}Z" 99 | 100 | payload = { 101 | "isPublic": False, 102 | "isLate": False, 103 | "retakeCounter": 0, 104 | "takenAt": taken_at, 105 | # "location": location, 106 | "caption": caption if caption is not None else "", 107 | "backCamera": { 108 | "bucket": "storage.bere.al", 109 | "height": back_size[1], 110 | "width": back_size[0], 111 | "path": back_url.replace("https://storage.bere.al/", ""), 112 | }, 113 | "frontCamera": { 114 | "bucket": "storage.bere.al", 115 | "height": front_size[1], 116 | "width": front_size[0], 117 | "path": front_url.replace("https://storage.bere.al/", ""), 118 | } 119 | } 120 | complete_res = requests.post( 121 | url=API_URL + "/content/post", 122 | json=payload, 123 | headers={"authorization": self.token} 124 | ).json() 125 | 126 | if "error" in complete_res: 127 | raise Exception(f"Error posting your BeReal. (error '{complete_res['errorKey']}')") 128 | 129 | try: 130 | caption = complete_res["caption"] 131 | except KeyError: 132 | caption = None 133 | formatted = { # Format the complete_res data in a way Post() can understand 134 | "id": complete_res["id"], 135 | "comments": [], 136 | "realmojis": [], 137 | "photoURL": complete_res["primary"]["url"], 138 | "secondaryPhotoURL": complete_res["secondary"]["url"], 139 | "user": complete_res["user"], 140 | "isLate": complete_res["isLate"], 141 | "retakeCounter": complete_res["retakeCounter"], 142 | "caption": caption, 143 | "isPublic": "public" in complete_res["visiblity"] 144 | } 145 | # print(complete_res["visibility"]) 146 | return Post(formatted, self, creation_override=datetime(complete_res["createdAt"])) 147 | 148 | def refresh(self): 149 | if self.refresh_token is None: 150 | raise Exception("No refresh token") 151 | res = requests.post( 152 | "https://securetoken.googleapis.com/v1/token", 153 | params={"key": GOOGLE_API_KEY}, 154 | data={ 155 | "refresh_token": self.refresh_token, 156 | "grant_type": "refresh_token" 157 | } 158 | ).json() 159 | 160 | self.token = res["id_token"] 161 | self.refresh_token = res["refresh_token"] 162 | return self.me() 163 | 164 | def me(self): 165 | res = requests.get( 166 | url=API_URL + "/person/me", 167 | headers={"authorization": self.token} 168 | ).json() 169 | return Me(res) 170 | 171 | def on_login(self): 172 | self.feed = Feed(self) 173 | 174 | 175 | class Login: 176 | def __init__(self, bereal): 177 | self._bereal = bereal 178 | self._otp_session = None 179 | 180 | def send_code(self, phone_number): 181 | res = requests.post( 182 | url="https://www.googleapis.com/identitytoolkit/v3/relyingparty/sendVerificationCode", 183 | params={"key": GOOGLE_API_KEY}, 184 | data={ 185 | "phoneNumber": phone_number, 186 | "iosReceipt": IOS_RECEIPT, 187 | "iosSecret": IOS_SECRET 188 | }, 189 | headers=HEADERS 190 | ).json() 191 | if "error" in res: 192 | raise Exception("Error sending OTP - the quota is probably exceeded. Try again in a few minutes.") 193 | self._otp_session = res["sessionInfo"] 194 | return res 195 | 196 | def verify_code(self, code): 197 | if self._otp_session is None: 198 | raise Exception("No OTP session is open") 199 | res = requests.post( 200 | url="https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhoneNumber", 201 | params={"key": GOOGLE_API_KEY}, 202 | data={ 203 | "sessionInfo": self._otp_session, 204 | "code": code, 205 | "operation": "SIGN_UP_OR_IN" 206 | } 207 | ).json() 208 | 209 | self._bereal.token = res["idToken"] 210 | self._bereal.refresh_token = res["refreshToken"] 211 | self._bereal.on_login() 212 | 213 | def with_tokens(self, token, refresh_token): 214 | self._bereal.token = token 215 | self._bereal.refresh_token = refresh_token 216 | self._bereal.on_login() 217 | -------------------------------------------------------------------------------- /bereal/constants.py: -------------------------------------------------------------------------------- 1 | GOOGLE_API_KEY = "AIzaSyDwjfEeparokD7sXPVQli9NsTuhT6fJ6iA" 2 | API_URL = "https://mobile.bereal.com/api" 3 | IOS_RECEIPT = "AEFDNu9QZBdycrEZ8bM_2-Ei5kn6XNrxHplCLx2HYOoJAWx-uSYzMldf66-gI1vOzqxfuT4uJeMXdreGJP5V1pNen_IKJVED3EdKl0ldUyYJflW5rDVjaQiXpN0Zu2BNc1c" 4 | IOS_SECRET = "KKwuB8YqwuM3ku0z" 5 | HEADERS = { 6 | "x-firebase-client": "apple-platform/ios apple-sdk/19F64 appstore/true deploy/cocoapods device/iPhone9,1 fire-abt/8.15.0 fire-analytics/8.15.0 fire-auth/8.15.0 fire-db/8.15.0 fire-dl/8.15.0 fire-fcm/8.15.0 fire-fiam/8.15.0 fire-fst/8.15.0 fire-fun/8.15.0 fire-install/8.15.0 fire-ios/8.15.0 fire-perf/8.15.0 fire-rc/8.15.0 fire-str/8.15.0 firebase-crashlytics/8.15.0 os-version/14.7.1 xcode/13F100", 7 | "user-agent": "FirebaseAuth.iOS/8.15.0 AlexisBarreyat.BeReal/0.22.4 iPhone/14.7.1 hw/iPhone9_1", 8 | "x-ios-bundle-identifier": "AlexisBarreyat.BeReal", 9 | "x-firebase-client-log-type": "0", 10 | "x-client-version": "iOS/FirebaseSDK/8.15.0/FirebaseCore-iOS", 11 | } 12 | -------------------------------------------------------------------------------- /bereal/models/comment.py: -------------------------------------------------------------------------------- 1 | from bereal.models.user import User 2 | from datetime import datetime 3 | 4 | 5 | class Comment: 6 | def __init__(self, data, datetime_override=None): 7 | self.comment_id = data["id"] 8 | self.user = User(data["user"]) 9 | self.body = data["text"] 10 | if datetime_override is None: 11 | self.creation_time = datetime.fromtimestamp(data["creationDate"]["_seconds"]) 12 | else: 13 | self.creation_time = datetime_override 14 | 15 | -------------------------------------------------------------------------------- /bereal/models/feed.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bereal.constants import * 3 | from bereal.models.post import Post 4 | 5 | 6 | class Feed: 7 | def __init__(self, bereal): 8 | self._bereal = bereal 9 | 10 | def friends(self): 11 | res = requests.get( 12 | url=API_URL + "/feeds/friends", 13 | headers={"authorization": self._bereal.token} 14 | ).json() 15 | return [Post(post, self._bereal) for post in res] 16 | 17 | def discovery(self): 18 | res = requests.get( 19 | url=API_URL + "/feeds/discovery", 20 | headers={"authorization": self._bereal.token} 21 | ).json() 22 | return [Post(post, self._bereal) for post in res["posts"]] 23 | -------------------------------------------------------------------------------- /bereal/models/post.py: -------------------------------------------------------------------------------- 1 | from bereal.models.user import User 2 | from bereal.models.comment import Comment 3 | from bereal.models.realmoji import Realmoji 4 | from datetime import datetime 5 | from bereal.constants import * 6 | import requests 7 | 8 | 9 | class Post: 10 | def __init__(self, data, bereal, creation_override=None): 11 | self._bereal = bereal 12 | 13 | self.post_id = data["id"] 14 | self.user = User(data["user"]) 15 | self.front_camera = data["secondaryPhotoURL"] 16 | self.back_camera = data["photoURL"] 17 | self.is_public = data["isPublic"] 18 | self.retakes = data["retakeCounter"] 19 | try: 20 | self.caption = data["caption"] 21 | except KeyError: 22 | self.caption = None 23 | if creation_override is None: 24 | self.creation_time = datetime.fromtimestamp(data["creationDate"]["_seconds"]) 25 | else: 26 | self.creation_time = creation_override 27 | self.comments = [Comment(comment) for comment in data["comment"]] 28 | self.realmojis = [Realmoji(realmoji) for realmoji in data["realMojis"]] 29 | 30 | def add_comment(self, body): 31 | res = requests.post( 32 | url=API_URL + "/content/comments", 33 | data={"content": body}, 34 | params={"postId": self.post_id}, 35 | headers={"authorization": self._bereal.token} 36 | ).json() 37 | return Comment({ # The comment route returns a different format of comment so we need to format it ourselves 38 | "id": res["id"], 39 | "user": res["user"], 40 | "text": res["content"] 41 | }, datetime_override=res["postedAt"]) 42 | 43 | -------------------------------------------------------------------------------- /bereal/models/realmoji.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class MyRealmoji: 5 | def __init__(self, data): 6 | self.url = data["media"]["url"] 7 | self.emoji = data["emoji"] 8 | 9 | 10 | class Realmoji: 11 | def __init__(self, data): 12 | from bereal.models.user import User # To avoid circular import 13 | self.url = data["uri"] 14 | self.emoji = data["emoji"] 15 | self.user = User(data["user"]) 16 | self.time = datetime.fromtimestamp(data["date"]["_seconds"]) 17 | -------------------------------------------------------------------------------- /bereal/models/user.py: -------------------------------------------------------------------------------- 1 | class User: 2 | def __init__(self, data): 3 | self.user_id = data["id"] 4 | self.username = data["username"] 5 | try: 6 | self.profile_picture = data["profilePicture"]["url"] 7 | except KeyError: 8 | self.profile_picture = None 9 | 10 | 11 | class Me(User): 12 | def __init__(self, data): 13 | from bereal.models.realmoji import MyRealmoji # To avoid circular import 14 | super().__init__(data) 15 | self.phone_number = data["phoneNumber"] 16 | self.full_name = data["fullname"] 17 | self.birthday = data["birthdate"] 18 | self.realmojis = [MyRealmoji(realmoji) for realmoji in data["realmojis"]] 19 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from bereal import BeReal 2 | from dotenv import load_dotenv 3 | from os import getenv 4 | 5 | SPAM_AMOUNT = 20 6 | 7 | 8 | def main(): 9 | load_dotenv() 10 | 11 | bereal = BeReal() 12 | # bereal.login.send_code(input("Enter phone number: ")) 13 | # bereal.login.verify_code(input("Enter verification code: ")) 14 | 15 | bereal.login.with_tokens(getenv("TOKEN"), getenv("REFRESH_TOKEN")) 16 | bereal.refresh() 17 | print(bereal.token) 18 | print(bereal.refresh_token) 19 | 20 | feed = bereal.feed.friends() 21 | for post in feed: 22 | if post.user.username == "izzygarbett": 23 | for _ in range(SPAM_AMOUNT): 24 | post.add_comment("Test") 25 | print("Spam") 26 | 27 | 28 | main() 29 | --------------------------------------------------------------------------------