├── .gitignore ├── client ├── pages │ ├── react │ │ ├── react.module.scss │ │ └── index.tsx │ ├── realmojis │ │ ├── realmojis.module.scss │ │ └── index.tsx │ ├── layout.tsx │ ├── memories │ │ └── memories.module.scss │ ├── allMemories │ │ ├── allMemories.module.scss │ │ └── index.tsx │ ├── help │ │ ├── help.module.scss │ │ └── index.tsx │ ├── api │ │ ├── memoriesV1.ts │ │ ├── profile.ts │ │ ├── me.ts │ │ ├── delete.ts │ │ ├── friends.ts │ │ ├── memoriesV2.ts │ │ ├── feed.ts │ │ ├── memories.ts │ │ ├── all.ts │ │ ├── react.ts │ │ ├── comment.ts │ │ ├── otp │ │ │ ├── vonage │ │ │ │ ├── send.ts │ │ │ │ └── verify.ts │ │ │ └── fire │ │ │ │ ├── send.ts │ │ │ │ └── verify.ts │ │ ├── refresh.ts │ │ └── add │ │ │ ├── realmoji.ts │ │ │ └── post.ts │ ├── _document.tsx │ ├── feed │ │ ├── feed.module.scss │ │ └── index.tsx │ ├── _app.tsx │ ├── profile │ │ ├── profile.module.scss │ │ └── [id].tsx │ ├── me │ │ ├── me.module.scss │ │ └── index.tsx │ ├── post │ │ ├── post.module.scss │ │ ├── post-with-camera │ │ │ ├── postcamera.module.scss │ │ │ └── index.tsx │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── public │ ├── locales │ │ └── de │ │ │ └── help.json │ ├── TooFake.png │ ├── favicon.ico │ ├── vercel.svg │ └── next.svg ├── styles │ ├── palette.scss │ ├── common.scss │ ├── globals.css │ └── loader.module.scss ├── components │ ├── divider │ │ ├── divider.module.scss │ │ └── divider.tsx │ ├── memoire │ │ ├── memoire.module.scss │ │ ├── memoireV2.tsx │ │ └── memoire.tsx │ ├── realmoji │ │ ├── realmoji.module.scss │ │ └── realmoji.tsx │ ├── navbar │ │ ├── navbar.module.scss │ │ └── navbar.tsx │ └── instant │ │ └── instant.module.scss ├── utils │ ├── device.ts │ ├── authHeaders.ts │ ├── constants.ts │ ├── remove.ts │ ├── logout.ts │ ├── myself.ts │ └── check.ts ├── models │ ├── moji.ts │ ├── user.ts │ ├── friend.ts │ ├── memoryV2.ts │ ├── realmoji.ts │ ├── memory.ts │ ├── comment.ts │ └── instance.ts ├── next.config.js ├── next-i18next.config.js ├── tsconfig.json ├── .gitignore └── package.json ├── Dockerfile ├── .github └── workflows │ └── dockerimage.yml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | -------------------------------------------------------------------------------- /client/pages/react/react.module.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/locales/de/help.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/styles/palette.scss: -------------------------------------------------------------------------------- 1 | $bg: #121212; -------------------------------------------------------------------------------- /client/public/TooFake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-alad/toofake/HEAD/client/public/TooFake.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-alad/toofake/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/styles/common.scss: -------------------------------------------------------------------------------- 1 | @mixin center() { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | } -------------------------------------------------------------------------------- /client/components/divider/divider.module.scss: -------------------------------------------------------------------------------- 1 | .divider { 2 | background-color: #fff; 3 | height: 2px; 4 | margin: 20px 0px; 5 | } -------------------------------------------------------------------------------- /client/utils/device.ts: -------------------------------------------------------------------------------- 1 | export function generateDeviceId(): string { 2 | return Array.from(Array(16), () => Math.floor(Math.random() * 36).toString(36)).join(''); 3 | } -------------------------------------------------------------------------------- /client/models/moji.ts: -------------------------------------------------------------------------------- 1 | interface Moji { 2 | id: string; 3 | emoji: string; 4 | url: string; 5 | userId: string; 6 | type: string; 7 | } 8 | 9 | export default Moji; -------------------------------------------------------------------------------- /client/utils/authHeaders.ts: -------------------------------------------------------------------------------- 1 | import getSignedHeaders from "happy-headers"; 2 | 3 | export function getAuthHeaders(token: string) { 4 | return { 5 | Authorization: `Bearer ${token}`, 6 | ...getSignedHeaders() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/components/divider/divider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './divider.module.scss'; 3 | 4 | export default function Divider() { 5 | return ( 6 |
7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /client/utils/constants.ts: -------------------------------------------------------------------------------- 1 | let PROXY = ( 2 | process.env.VERCEL ? "https://us-east1-toofake.cloudfunctions.net/toofakeproxy?target=" : "" 3 | ) 4 | 5 | let GAPIKEY = "AIzaSyCgNTZt6gzPMh-2voYXOvrt_UR_gpGl83Q" 6 | 7 | console.log("PROXY", PROXY) 8 | 9 | export { PROXY, GAPIKEY }; -------------------------------------------------------------------------------- /client/utils/remove.ts: -------------------------------------------------------------------------------- 1 | function remove() { 2 | localStorage.removeItem("token"); 3 | localStorage.removeItem("refresh_token"); 4 | localStorage.removeItem("expiration"); 5 | localStorage.removeItem("uid"); 6 | localStorage.removeItem("is_new_user"); 7 | localStorage.removeItem("token_type"); 8 | localStorage.removeItem("myself") 9 | } -------------------------------------------------------------------------------- /client/pages/realmojis/realmojis.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/common.scss" as c; 2 | 3 | .realmojis { 4 | width: 100%; 5 | display: flex; 6 | overflow-x: scroll; 7 | flex-wrap: wrap; 8 | 9 | 10 | @media screen and (max-width: 600px) { 11 | flex-direction: column; 12 | flex-wrap: nowrap; 13 | align-items: center; 14 | } 15 | } -------------------------------------------------------------------------------- /client/pages/layout.tsx: -------------------------------------------------------------------------------- 1 | import Divider from "@/components/divider/divider" 2 | import Navbar from "@/components/navbar/navbar" 3 | import useCheck from "@/utils/check" 4 | 5 | export default function Layout({ children }: any) { 6 | return ( 7 | <> 8 | 9 | 10 |
{children}
11 | 12 | ) 13 | } -------------------------------------------------------------------------------- /client/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const path = require('path') 3 | 4 | const { i18n } = require('./next-i18next.config') 5 | 6 | const nextConfig = { 7 | reactStrictMode: false, 8 | output: 'standalone', 9 | sassOptions: { 10 | includePaths: [path.join(__dirname, 'styles')], 11 | }, 12 | i18n, 13 | poweredByHeader: false, 14 | } 15 | 16 | module.exports = nextConfig 17 | -------------------------------------------------------------------------------- /client/next-i18next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-i18next').UserConfig} */ 2 | module.exports = { 3 | i18n: { 4 | defaultLocale: 'en', 5 | locales: ['en', 'de', 'fr'], 6 | }, 7 | /** To avoid issues when deploying to some paas (vercel...) */ 8 | localePath: 9 | typeof window === 'undefined' 10 | ? require('path').resolve('./public/locales') 11 | : '/locales', 12 | } -------------------------------------------------------------------------------- /client/utils/logout.ts: -------------------------------------------------------------------------------- 1 | export function logout(router: any, ls: any) { 2 | 3 | 4 | 5 | 6 | function removeStorage() { 7 | ls.removeItem("token"); 8 | ls.removeItem("firebase_refresh_token"); 9 | ls.removeItem("firebase_id_token"); 10 | ls.removeItem("expiration"); 11 | ls.removeItem("uid"); 12 | ls.removeItem("is_new_user"); 13 | ls.removeItem("token_type"); 14 | ls.removeItem("myself") 15 | } 16 | 17 | removeStorage(); 18 | router.push("/"); 19 | 20 | } -------------------------------------------------------------------------------- /client/models/user.ts: -------------------------------------------------------------------------------- 1 | class User { 2 | username: string; 3 | pfp: string; 4 | uid: string; 5 | 6 | constructor(username: string, pfp: string, uid: string) { 7 | this.username = username; 8 | this.pfp = pfp; 9 | this.uid = uid; 10 | } 11 | 12 | static create(rawuser: any) { 13 | let username = rawuser.username; 14 | let pfp = rawuser.profilePicture == undefined ? "" : rawuser.profilePicture.url; 15 | let uid = rawuser.id; 16 | 17 | return new User(username, pfp, uid); 18 | } 19 | } 20 | 21 | export default User; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: install dependencies 2 | FROM node:lts-alpine AS deps 3 | WORKDIR /app 4 | COPY client/package*.json . 5 | RUN npm i 6 | 7 | # Stage 2: build 8 | FROM node:lts-alpine AS builder 9 | WORKDIR /app 10 | COPY --from=deps /app/node_modules ./node_modules 11 | COPY client/ . 12 | ARG NODE_ENV=production 13 | RUN npm run build 14 | 15 | # Stage 3: run 16 | FROM gcr.io/distroless/nodejs18-debian11 17 | WORKDIR /app 18 | COPY --from=builder /app/.next/standalone ./ 19 | COPY --from=builder /app/public ./public 20 | COPY --from=builder /app/.next/static ./.next/static 21 | CMD ["server.js"] -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | 3 | } 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | html, body { 10 | padding: 0; 11 | margin: 0; 12 | overflow-x: hidden; 13 | 14 | height: 100%; 15 | } 16 | 17 | body { 18 | font-family: 'Roboto', sans-serif; 19 | background-color: #121212; 20 | color: #fff; 21 | height: 100%; 22 | padding: 20px 60px; 23 | overflow-y: scroll; 24 | } 25 | 26 | @media screen and (max-width: 768px) { 27 | body { 28 | padding: 20px 20px; 29 | } 30 | } 31 | 32 | main { 33 | flex: 1; 34 | display: flex; 35 | } 36 | 37 | #__next { 38 | display: flex; 39 | flex-direction: column; 40 | min-height: 100%; 41 | } -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules", "sst.config.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # sst 38 | .sst 39 | # sst-env.d.ts 40 | 41 | # open-next 42 | .open-next 43 | 44 | # firebase 45 | # firebase.json 46 | # .firebaserc 47 | .firebase 48 | -------------------------------------------------------------------------------- /client/models/friend.ts: -------------------------------------------------------------------------------- 1 | import User from "./user"; 2 | 3 | class Friend extends User { 4 | status: string; 5 | fullname: string; 6 | 7 | constructor(username: string, pfp: string, uid: string, status: string, fullname: string) { 8 | super(username, pfp, uid); 9 | this.status = status; 10 | this.fullname = fullname; 11 | } 12 | 13 | static create(rawuser: any) { 14 | let username = rawuser.username; 15 | let pfp = rawuser.profilePicture == undefined ? "" : rawuser.profilePicture.url; 16 | let uid = rawuser.id; 17 | let status = rawuser.status; 18 | let fullname = rawuser.fullname; 19 | 20 | return new Friend(username, pfp, uid, status, fullname); 21 | } 22 | } 23 | 24 | export default Friend; -------------------------------------------------------------------------------- /client/pages/memories/memories.module.scss: -------------------------------------------------------------------------------- 1 | .mem { 2 | width: 100%; 3 | } 4 | 5 | .memories { 6 | display: flex; 7 | overflow-x: scroll !important; 8 | 9 | 10 | @media screen and (max-width: 600px) { 11 | flex-direction: column; 12 | align-items: center; 13 | width: 100%; 14 | 15 | } 16 | } 17 | 18 | .download { 19 | padding-top: 40px; 20 | button { 21 | cursor: pointer; 22 | font-size: 16px; 23 | font-weight: 500; 24 | height: 34px; 25 | width: 200px; 26 | border-radius: 8px; 27 | background-color: white; 28 | border: none; 29 | margin-top: 20px; 30 | } 31 | 32 | .canvas { 33 | display: none; 34 | } 35 | 36 | .error { 37 | color: red; 38 | } 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /client/pages/allMemories/allMemories.module.scss: -------------------------------------------------------------------------------- 1 | .mem { 2 | width: 100%; 3 | } 4 | 5 | .allMemories { 6 | display: flex; 7 | overflow-x: scroll !important; 8 | 9 | 10 | @media screen and (max-width: 600px) { 11 | flex-direction: column; 12 | align-items: center; 13 | width: 100%; 14 | 15 | } 16 | } 17 | 18 | .download { 19 | padding-top: 40px; 20 | button { 21 | cursor: pointer; 22 | font-size: 16px; 23 | font-weight: 500; 24 | height: 34px; 25 | width: 200px; 26 | border-radius: 8px; 27 | background-color: white; 28 | border: none; 29 | margin-top: 20px; 30 | } 31 | 32 | .canvas { 33 | display: none; 34 | } 35 | 36 | .error { 37 | color: red; 38 | } 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /client/styles/loader.module.scss: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | 0% { transform: rotate(0deg); } 3 | 100% { transform: rotate(360deg); } 4 | } 5 | 6 | .loader { 7 | border: 6px solid #f3f3f3; /* Light grey */ 8 | border-top: 6px solid grey; /* Blue */ 9 | border-radius: 50%; 10 | width: 40px; 11 | height: 40px; 12 | animation: spin 2s linear infinite; 13 | } 14 | 15 | .loadersmall { 16 | border: 3px solid #f3f3f3; /* Light grey */ 17 | border-top: 3px solid grey; /* Blue */ 18 | border-radius: 50%; 19 | width: 20px; 20 | height: 20px; 21 | animation: spin 2s linear infinite; 22 | } 23 | 24 | .loadertiny { 25 | border: 2px solid #f3f3f3; /* Light grey */ 26 | border-top: 2px solid grey; /* Blue */ 27 | border-radius: 50%; 28 | width: 10px; 29 | height: 10px; 30 | animation: spin 2s linear infinite; 31 | } -------------------------------------------------------------------------------- /client/models/memoryV2.ts: -------------------------------------------------------------------------------- 1 | class MemoryV2 { 2 | id: string; 3 | primary: string; 4 | secondary: string; 5 | date: string; 6 | time: string; 7 | 8 | constructor( 9 | id: string, primary: string, secondary: string, date: string, time: string 10 | ) { 11 | this.id = id; 12 | this.primary = primary; 13 | this.secondary = secondary; 14 | this.date = date; 15 | this.time = time; 16 | } 17 | 18 | 19 | static create(raw: any) { 20 | let id = raw.id; 21 | let primary = raw.primary.url; 22 | let secondary = raw.secondary.url; 23 | 24 | let takenAt = raw.takenAt.split("T"); 25 | let date = takenAt[0]; 26 | let time = takenAt[1].split(".")[0]; 27 | 28 | return new MemoryV2(id, primary, secondary, date, time); 29 | } 30 | } 31 | 32 | export default MemoryV2; -------------------------------------------------------------------------------- /client/utils/myself.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export default async function myself() { 4 | 5 | console.log("setting myself"); 6 | 7 | let token = localStorage.getItem("token"); 8 | let body = JSON.stringify({ "token": token }); 9 | 10 | let options = { 11 | url: "/api/me", 12 | method: "POST", 13 | headers: { 'Content-Type': 'application/json' }, 14 | data: body, 15 | } 16 | 17 | return axios.request(options).then( 18 | (response) => { 19 | console.log("me resp", response.data); 20 | let myselfobject = response.data; 21 | localStorage.setItem("myself", JSON.stringify(myselfobject)); 22 | return true 23 | } 24 | ).catch( 25 | (error) => { 26 | console.log(error); 27 | return false 28 | } 29 | ) 30 | 31 | 32 | } -------------------------------------------------------------------------------- /client/pages/help/help.module.scss: -------------------------------------------------------------------------------- 1 | .help { 2 | 3 | .brief { 4 | max-width: 900px; 5 | a { 6 | color: #fff; 7 | } 8 | p { 9 | margin: 12px 0px; 10 | } 11 | margin-bottom: 50px; 12 | } 13 | 14 | .directory { 15 | display: flex; 16 | flex-direction: column; 17 | a { 18 | font-size: 22px; 19 | color: #fff; 20 | margin-bottom: 12px; 21 | &:last-child { 22 | margin-bottom: 0px; 23 | } 24 | } 25 | margin-bottom: 50px; 26 | } 27 | 28 | .faq, .uses, .issues { 29 | margin-bottom: 50px; 30 | h2 { 31 | text-decoration: underline; 32 | } 33 | .q, .use, .issue { 34 | h3 { 35 | margin-bottom: 8px; 36 | } 37 | p { 38 | margin-top: 0px; 39 | margin-bottom: 22px; 40 | } 41 | } 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /.github/workflows/dockerimage.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | paths: 8 | - "client/**" 9 | - "Dockerfile" 10 | workflow_dispatch: 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v3 19 | - 20 | name: Set up QEMU 21 | uses: docker/setup-qemu-action@v2 22 | - 23 | name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v2 25 | - 26 | name: Login to Docker Hub 27 | uses: docker/login-action@v2 28 | with: 29 | username: ${{ secrets.DOCKERHUB_USERNAME }} 30 | password: ${{ secrets.DOCKERHUB_TOKEN }} 31 | - 32 | name: Build and push 33 | uses: docker/build-push-action@v4 34 | with: 35 | context: . 36 | platforms: linux/amd64,linux/arm64 37 | push: true 38 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/toofake:latest 39 | -------------------------------------------------------------------------------- /client/pages/api/memoriesV1.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | import { PROXY } from '@/utils/constants'; 5 | 6 | export const config = { 7 | api: { 8 | responseLimit: false, 9 | }, 10 | maxDuration: 300, 11 | } 12 | 13 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 14 | 15 | let authorization_token = req.body.token; 16 | console.log("me"); 17 | console.log(authorization_token); 18 | 19 | return axios.request({ 20 | url: `${PROXY}https://mobile.bereal.com/api` + "/feeds/memories-v1", 21 | method: "GET", 22 | headers: getAuthHeaders(req.body.token), 23 | }).then( 24 | (response) => { 25 | res.status(200).json(response.data); 26 | } 27 | ).catch( 28 | (error) => { 29 | console.log(error); 30 | res.status(400).json({ status: "error" }); 31 | } 32 | ) 33 | } -------------------------------------------------------------------------------- /client/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | import Script from 'next/script' 3 | import { GoogleAnalytics } from 'nextjs-google-analytics' 4 | 5 | export default function Document() { 6 | return ( 7 | 8 | 9 | {/* 10 | */} 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /client/pages/api/profile.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | import { PROXY } from '@/utils/constants'; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | 8 | let authorization_token = req.body.token; 9 | let profile_id = req.body.profile_id; 10 | console.log("profile"); 11 | console.log(authorization_token, profile_id); 12 | 13 | return axios.request({ 14 | url: `${PROXY}https://mobile.bereal.com/api` + `/person/profiles/${profile_id}`, 15 | method: "GET", 16 | headers: getAuthHeaders(req.body.token), 17 | }).then( 18 | (response) => { 19 | console.log(response.data); 20 | 21 | res.status(200).json(response.data); 22 | } 23 | ).catch( 24 | (error) => { 25 | console.log(error); 26 | res.status(400).json({ status: "error" }); 27 | } 28 | ) 29 | } -------------------------------------------------------------------------------- /client/pages/api/me.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | import { PROXY } from '@/utils/constants'; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | let authorization_token = req.body.token; 8 | console.log("me"); 9 | console.log(authorization_token); 10 | 11 | return axios.request({ 12 | url: `${PROXY}https://mobile.bereal.com/api` + "/person/me", 13 | method: "GET", 14 | headers: getAuthHeaders(req.body.token), 15 | }).then( 16 | (response) => { 17 | console.log("------------------") 18 | console.log("request me success"); 19 | console.log(response.data); 20 | console.log("------------------") 21 | 22 | res.status(200).json(response.data); 23 | } 24 | ).catch( 25 | (error) => { 26 | console.log(error); 27 | res.status(400).json({ status: "error" }); 28 | } 29 | ) 30 | } -------------------------------------------------------------------------------- /client/pages/api/delete.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | import { PROXY } from '@/utils/constants'; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | 8 | let authorization_token = req.body.token; 9 | console.log("me"); 10 | console.log(authorization_token); 11 | 12 | return axios.request({ 13 | url: `${PROXY}https://mobile.bereal.com/api" + "/content/posts`, 14 | method: "DELETE", 15 | headers: getAuthHeaders(req.body.token), 16 | }).then( 17 | (response) => { 18 | console.log("------------------") 19 | console.log("delete post success"); 20 | console.log(response.data); 21 | console.log("------------------") 22 | 23 | res.status(200).json(response.data); 24 | } 25 | ).catch( 26 | (error) => { 27 | console.log(error); 28 | res.status(400).json({ status: "error" }); 29 | } 30 | ) 31 | } -------------------------------------------------------------------------------- /client/pages/api/friends.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | import { PROXY } from '@/utils/constants'; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | let authorization_token = req.body.token; 8 | console.log("friends"); 9 | console.log(authorization_token); 10 | 11 | return axios.request({ 12 | url: `${PROXY}https://mobile.bereal.com/api` + "/relationships/friends", 13 | method: "GET", 14 | headers: getAuthHeaders(req.body.token), 15 | }).then( 16 | (response) => { 17 | console.log("------------------") 18 | console.log("request friends success"); 19 | console.log(response.data); 20 | console.log("------------------") 21 | 22 | res.status(200).json(response.data); 23 | } 24 | ).catch( 25 | (error) => { 26 | console.log(error); 27 | res.status(400).json({ status: "error" }); 28 | } 29 | ) 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 toofake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/pages/api/memoriesV2.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | import { PROXY } from '@/utils/constants'; 5 | 6 | export const config = { 7 | api: { 8 | responseLimit: false, 9 | }, 10 | maxDuration: 300, 11 | } 12 | 13 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 14 | try { 15 | return axios.request({ 16 | url: `${PROXY}https://mobile.bereal.com/api` + "/feeds/memories-v2/" + req.query.momentId, 17 | method: "GET", 18 | headers: getAuthHeaders(req.body.token), 19 | }).then( 20 | (response) => { 21 | res.status(200).json(response.data); 22 | } 23 | ).catch( 24 | (error) => { 25 | console.log(error); 26 | res.status(400).json({ status: "error" }); 27 | } 28 | ) 29 | } catch (e) { 30 | console.log(`-----------retrying ${req.query.momentId}-----------`); 31 | handler(req, res); 32 | } 33 | } -------------------------------------------------------------------------------- /client/models/realmoji.ts: -------------------------------------------------------------------------------- 1 | import User from "./user"; 2 | 3 | class Realmoji { 4 | owner: User; 5 | 6 | emoji: string; 7 | emoji_id: string; 8 | type: string; 9 | uri: string; 10 | 11 | constructor(owner: User, emoji: string, emoji_id: string, type: string, uri: string) { 12 | this.owner = owner; 13 | this.emoji = emoji; 14 | this.emoji_id = emoji_id; 15 | this.type = type; 16 | this.uri = uri; 17 | } 18 | 19 | static create(raw: any) { 20 | 21 | let owner = User.create(raw.user); 22 | 23 | let emoji = raw.emoji; 24 | let emoji_id = raw.id; 25 | let type = raw.type; 26 | let uri = raw.uri; 27 | 28 | return new Realmoji(owner, emoji, emoji_id, type, uri); 29 | 30 | } 31 | 32 | static moment(raw: any) { 33 | let owner = User.create(raw.user); 34 | 35 | let emoji = raw.emoji; 36 | let emoji_id = raw.id; 37 | let type = raw.type; 38 | let uri = raw.media.url; 39 | 40 | return new Realmoji(owner, emoji, emoji_id, type, uri); 41 | } 42 | 43 | } 44 | 45 | export default Realmoji; -------------------------------------------------------------------------------- /client/pages/api/feed.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | 5 | export const config = { 6 | api: { 7 | responseLimit: false, 8 | }, 9 | maxDuration: 300, 10 | } 11 | 12 | // deprecated 13 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 14 | 15 | console.log("FETCING FEED") 16 | 17 | return axios.request({ 18 | url: "https://mobile.bereal.com/api" + "/feeds/friends", 19 | method: "GET", 20 | headers: getAuthHeaders(req.body.token), 21 | }).then( 22 | (response) => { 23 | console.log("------------------") 24 | console.log("request feed success"); 25 | console.log(response.data); 26 | console.log("------------------") 27 | 28 | res.status(200).json(response.data); 29 | } 30 | ).catch( 31 | (error) => { 32 | console.log(error.response.data); 33 | res.status(400).json({ status: "error", error: error.response.data }); 34 | } 35 | ) 36 | } -------------------------------------------------------------------------------- /client/components/memoire/memoire.module.scss: -------------------------------------------------------------------------------- 1 | .memory { 2 | border-radius: 8px; 3 | width: 300px; 4 | min-width: 300px; 5 | border: 1px solid white; 6 | box-shadow: 0 0 10px 1px #6a6565; 7 | margin-right: 30px; 8 | height: min-content; 9 | 10 | .details { 11 | padding: 12px 10px; 12 | .date { 13 | font-weight: bold; 14 | } 15 | } 16 | 17 | .content { 18 | position: relative; 19 | /* height: 400px; */ 20 | width: 100%; 21 | 22 | .primary { 23 | position: relative; 24 | width: 100%; 25 | border-radius: 8px; 26 | } 27 | 28 | .bounds { 29 | width: 100%; 30 | height: 85%; 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | padding: 10px; 35 | .secondary { 36 | border-radius: 8px; 37 | width: 100px; 38 | position: absolute; 39 | cursor: pointer; 40 | z-index: 9; 41 | } 42 | } 43 | } 44 | 45 | @media screen and (max-width: 600px) { 46 | margin-right: 0; 47 | margin-bottom: 20px; 48 | } 49 | } -------------------------------------------------------------------------------- /client/pages/api/memories.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | import { PROXY } from '@/utils/constants'; 5 | 6 | export const config = { 7 | api: { 8 | responseLimit: false, 9 | }, 10 | maxDuration: 300, 11 | } 12 | 13 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 14 | 15 | let authorization_token = req.body.token; 16 | console.log("me"); 17 | console.log(authorization_token); 18 | 19 | return axios.request({ 20 | url: `${PROXY}https://mobile.bereal.com/api` + "/feeds/memories", 21 | method: "GET", 22 | headers: getAuthHeaders(req.body.token), 23 | }).then( 24 | (response) => { 25 | console.log("------------------") 26 | console.log("request memories success"); 27 | console.log(response.data); 28 | console.log("------------------") 29 | 30 | res.status(200).json(response.data); 31 | } 32 | ).catch( 33 | (error) => { 34 | console.log(error); 35 | res.status(400).json({ status: "error" }); 36 | } 37 | ) 38 | } -------------------------------------------------------------------------------- /client/models/memory.ts: -------------------------------------------------------------------------------- 1 | class Memory { 2 | memid: string; 3 | primary: string; 4 | secondary: string; 5 | thumbnail: string; 6 | date: string; 7 | location: { latitude: number, longitude: number } | undefined; 8 | 9 | constructor( 10 | memid: string, primary: string, secondary: string, thumbnail: string, date: string, 11 | location: { latitude: number, longitude: number } | undefined 12 | ) { 13 | this.memid = memid; 14 | this.primary = primary; 15 | this.secondary = secondary; 16 | this.thumbnail = thumbnail; 17 | this.date = date; 18 | this.location = location; 19 | } 20 | 21 | 22 | static async create(raw: any) { 23 | let memid = raw.id; 24 | let primary = raw.primary.url; 25 | let secondary = raw.secondary.url; 26 | let thumbnail = raw.thumbnail.url; 27 | let date = raw.memoryDay; 28 | let location = raw.location != undefined ? { 29 | latitude: raw.location.latitude as number, 30 | longitude: raw.location.longitude as number 31 | } : undefined; 32 | 33 | return new Memory(memid, primary, secondary, thumbnail, date, location); 34 | } 35 | } 36 | 37 | export default Memory; -------------------------------------------------------------------------------- /client/pages/api/all.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | 5 | export const config = { 6 | api: { 7 | responseLimit: false, 8 | }, 9 | maxDuration: 300, 10 | } 11 | 12 | // friends feed v1 13 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 14 | 15 | console.log("FETCHING FEED") 16 | 17 | let authHeaders = getAuthHeaders(req.body.token); 18 | console.log(authHeaders); 19 | 20 | return axios.request({ 21 | url: "https://mobile.bereal.com/api" + "/feeds/friends-v1", 22 | method: "GET", 23 | headers: getAuthHeaders(req.body.token), 24 | }).then( 25 | (response) => { 26 | console.log("------------------") 27 | console.log("all request feed success"); 28 | console.log(response.data); 29 | console.log("------------------") 30 | 31 | res.status(200).json(response.data); 32 | } 33 | ).catch( 34 | (error) => { 35 | console.log(error.response.data); 36 | res.status(400).json({ status: "error", error: error.response.data }); 37 | } 38 | ) 39 | } -------------------------------------------------------------------------------- /client/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/pages/api/react.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | import { PROXY } from '@/utils/constants'; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | 8 | let authorization_token = req.body.token; 9 | let post_id = req.body.post_id; 10 | let post_user_id = req.body.post_user_id; 11 | let emoji = req.body.emoji; 12 | 13 | console.log("me"); 14 | console.log(authorization_token); 15 | 16 | let data = { 17 | "emoji": `${emoji}`, 18 | } 19 | 20 | let params = { 21 | "postId": `${post_id}`, 22 | "postUserId": `${post_user_id}`, 23 | } 24 | 25 | return axios.request({ 26 | url: `${PROXY}https://mobile.bereal.com/api` + `/content/realmojis`, 27 | method: "PUT", 28 | headers: getAuthHeaders(req.body.token), 29 | data: data, 30 | params: params, 31 | }).then( 32 | (response) => { 33 | console.log("------------------") 34 | console.log("request me success"); 35 | console.log(response.data); 36 | console.log("------------------") 37 | 38 | res.status(200).json(response.data); 39 | } 40 | ).catch( 41 | (error) => { 42 | console.log(error); 43 | res.status(400).json({ status: "error" }); 44 | } 45 | ) 46 | } -------------------------------------------------------------------------------- /client/components/memoire/memoireV2.tsx: -------------------------------------------------------------------------------- 1 | import MemoryV2 from "@/models/memoryV2" 2 | import s from "./memoire.module.scss" 3 | import Draggable from "react-draggable" 4 | import { useState } from "react"; 5 | 6 | 7 | export default function Memoire({ memory }: { memory: MemoryV2 }) { 8 | 9 | let [swap, setSwap] = useState(false); 10 | 11 | let date = new Date(memory.date); 12 | let formatOptions: any = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; 13 | 14 | let [location] = useState(""); 15 | 16 | return ( 17 |
18 |
19 |
20 | {date.toLocaleDateString(undefined, formatOptions)} 21 |
22 |
23 | {location} 24 |
25 |
26 |
27 | 28 |
setSwap(!swap)}> 29 | 30 | setSwap(!swap)} onMouseDown={(e) => { e.stopPropagation() }} /> 31 | 32 |
33 |
34 |
35 | ) 36 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 13 | "@fortawesome/free-brands-svg-icons": "^6.4.0", 14 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 15 | "@fortawesome/react-fontawesome": "^0.2.0", 16 | "@types/formidable": "^2.0.6", 17 | "@types/node": "20.3.1", 18 | "@types/react": "18.2.12", 19 | "@types/react-dom": "18.2.5", 20 | "@vercel/analytics": "^1.0.1", 21 | "axios": "^1.4.0", 22 | "cropperjs": "^1.6.2", 23 | "file-saver": "^2.0.5", 24 | "formidable": "^3.4.0", 25 | "happy-headers": "^1.5.0", 26 | "heic-convert": "^1.2.4", 27 | "i18next": "^23.2.3", 28 | "jimp": "^0.22.8", 29 | "jszip": "^3.10.1", 30 | "moment": "^2.29.4", 31 | "next": "^13.4.6", 32 | "next-i18next": "^14.0.0", 33 | "nextjs-google-analytics": "^2.3.3", 34 | "react": "18.2.0", 35 | "react-cropper": "^2.3.3", 36 | "react-dom": "18.2.0", 37 | "react-draggable": "^4.4.5", 38 | "react-easy-crop": "^5.0.8", 39 | "react-i18next": "^13.0.1", 40 | "react-multi-carousel": "^2.8.4", 41 | "react-phone-input-2": "^2.15.1", 42 | "sass": "^1.63.4", 43 | "sharp": "^0.32.1", 44 | "sst": "ion", 45 | "typescript": "5.1.3" 46 | }, 47 | "devDependencies": { 48 | "@types/file-saver": "^2.0.7" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/pages/api/comment.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { getAuthHeaders } from '@/utils/authHeaders'; 4 | import { PROXY } from '@/utils/constants'; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | 8 | let authorization_token = req.body.token; 9 | let instance_id = req.body.instance_id; 10 | let poster_user_id = req.body.poster_user_id; 11 | let comment = req.body.comment; 12 | console.log("me"); 13 | console.log(authorization_token) 14 | console.log(instance_id, comment); 15 | 16 | let body = { 17 | content: comment, 18 | } 19 | let options = { 20 | url: `${PROXY}https://mobile.bereal.com/api` + "/content/comments" + "?postId=" + instance_id + "&postUserId=" + poster_user_id, 21 | method: "POST", 22 | headers: getAuthHeaders(req.body.token), 23 | data: body, 24 | } 25 | 26 | console.log("FETCHING COMMENT") 27 | console.log(options); 28 | 29 | return axios.request(options).then( 30 | (response) => { 31 | console.log("------------------") 32 | console.log("request comment success"); 33 | console.log(response.data); 34 | console.log("------------------") 35 | 36 | res.status(200).json(response.data); 37 | } 38 | ).catch( 39 | (error) => { 40 | console.log(error.response.data); 41 | res.status(400).json({ status: "error" }); 42 | } 43 | ) 44 | } -------------------------------------------------------------------------------- /client/pages/api/otp/vonage/send.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { generateDeviceId } from '@/utils/device'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | 7 | let headers_list = { 8 | "Accept": "*/*", 9 | "User-Agent": "BeReal/8586 CFNetwork/1240.0.4 Darwin/20.6.0", 10 | "x-ios-bundle-identifier": "AlexisBarreyat.BeReal", 11 | "Content-Type": "application/json" 12 | } 13 | 14 | let phonenumber = req.body.number; 15 | let device_id: string = generateDeviceId(); 16 | 17 | console.log("------------------") 18 | console.log("request vonage otp"); 19 | console.log(phonenumber, device_id); 20 | console.log("------------------") 21 | 22 | let body_content = JSON.stringify({ "phoneNumber": phonenumber, "deviceId": device_id }); 23 | let req_options = { 24 | url: "https://auth.bereal.team/api/vonage/request-code", 25 | method: "POST", 26 | headers: headers_list, 27 | data: body_content, 28 | } 29 | 30 | return axios.request(req_options).then( 31 | (response) => { 32 | let rstatus = response.data.status; 33 | let vonage_request_id = response.data.vonageRequestId; 34 | res.status(200).json({ status: "success", vonageRequestId: vonage_request_id }); 35 | } 36 | ).catch( 37 | (error) => { 38 | console.log(error.response); 39 | res.status(400).json({ error: error.response.data }); 40 | } 41 | ) 42 | } -------------------------------------------------------------------------------- /client/pages/feed/feed.module.scss: -------------------------------------------------------------------------------- 1 | .feed { 2 | padding-top: 20px; 3 | display: flex; 4 | overflow-x: scroll; 5 | padding-bottom: 40px; 6 | 7 | .failure { 8 | .error { 9 | /* width: 100%; */ 10 | color: maroon; 11 | } 12 | .help { 13 | margin-top: 12px; 14 | width: 100%; 15 | } 16 | } 17 | 18 | 19 | 20 | //if screen is small 21 | @media (max-width: 600px) { 22 | flex-direction: column; 23 | align-items: center; 24 | width: 100%; 25 | } 26 | } 27 | 28 | .ad { 29 | height: min-content; 30 | width: 300px; 31 | 32 | border: 1px solid white; 33 | border-radius: 8px; 34 | 35 | margin-right: 30px; 36 | margin-top: 248px; 37 | 38 | @media (max-width: 600px) { 39 | margin-right: 0px; 40 | margin-bottom: 12px; 41 | margin-top: 0px; 42 | } 43 | 44 | .head { 45 | padding: 10px; 46 | padding-top: 12px; 47 | display: flex; 48 | justify-content: space-between; 49 | font-size: 12px; 50 | .close { 51 | cursor: pointer; 52 | font-size: 16px; 53 | margin-right: 6px; 54 | } 55 | } 56 | 57 | .adimage { 58 | height: min-content; 59 | border-radius: 8px; 60 | background-color: #ff7373; 61 | width: 100%; 62 | object-fit: cover; 63 | position: relative; 64 | top: 4px; 65 | } 66 | } 67 | .hide { 68 | display: none; 69 | } 70 | 71 | .feed::-webkit-scrollbar { 72 | width: 10px; 73 | } 74 | 75 | .feed::-webkit-scrollbar-track { 76 | background: #fff; 77 | } 78 | 79 | .feed::-webkit-scrollbar-thumb { 80 | background: #555; 81 | } 82 | 83 | .feed::-webkit-scrollbar-thumb:hover { 84 | background: #444; 85 | } -------------------------------------------------------------------------------- /client/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import Head from 'next/head' 4 | import Layout from './layout' 5 | import { Analytics } from '@vercel/analytics/react'; 6 | import Script from 'next/script'; 7 | import { GoogleAnalytics } from 'nextjs-google-analytics'; 8 | import "@fortawesome/fontawesome-svg-core/styles.css"; 9 | import { config } from "@fortawesome/fontawesome-svg-core"; 10 | config.autoAddCss = false; 11 | 12 | import { appWithTranslation } from 'next-i18next' 13 | 14 | function App({ Component, pageProps }: AppProps) { 15 | return ( 16 | 17 | 18 | TooFake 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | /* export default appWithTranslation(App) */ 43 | export default App; 44 | -------------------------------------------------------------------------------- /client/models/comment.ts: -------------------------------------------------------------------------------- 1 | import User from "./user"; 2 | 3 | class Comment { 4 | comment_id: number; 5 | text: string; 6 | owner: User; 7 | comment_time?: string; 8 | 9 | constructor(comment_id: number, text: string, owner: User, comment_time?: string) { 10 | this.comment_id = comment_id; 11 | this.text = text; 12 | this.owner = owner; 13 | this.comment_time = comment_time; 14 | } 15 | 16 | static create(raw: any) { 17 | let comment_id = raw.id; 18 | let text = raw.text; 19 | 20 | let owner = User.create(raw.user); 21 | 22 | return new Comment(comment_id, text, owner); 23 | } 24 | 25 | static moment(raw: any) { 26 | let comment_id = raw.id; 27 | let text = raw.content; 28 | let comment_time = this.formatTime(raw.postedAt); 29 | 30 | let owner = User.create(raw.user); 31 | 32 | return new Comment(comment_id, text, owner, comment_time); 33 | } 34 | 35 | static formatTime(postedAt: string): string { 36 | if (!postedAt) return "no date available"; 37 | 38 | let postedDate = new Date(postedAt); 39 | let now = new Date(); 40 | let diffInSeconds = Math.floor((now.getTime() - postedDate.getTime()) / 1000); 41 | 42 | if (diffInSeconds < 60) { 43 | return `${diffInSeconds} seconds ago`; 44 | } else if (diffInSeconds < 3600) { 45 | let minutes = Math.floor(diffInSeconds / 60); 46 | return `${minutes} minutes ago`; 47 | } else { 48 | return postedDate.toLocaleString(undefined, { 49 | hour: '2-digit', 50 | minute: '2-digit', 51 | second: '2-digit', 52 | month: 'short', 53 | day: 'numeric' 54 | }); 55 | } 56 | } 57 | } 58 | 59 | export default Comment; 60 | -------------------------------------------------------------------------------- /client/components/realmoji/realmoji.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/common.scss" as c; 2 | 3 | .realmoji { 4 | height: 300px; 5 | width: 240px; 6 | min-width: 240px; 7 | border-radius: 8px; 8 | box-shadow: 0px 0px 20px .1px rgba(84, 84, 84, 0.75); 9 | display: flex; 10 | flex-direction: column; 11 | margin-right: 30px; 12 | 13 | img, .nomoji { 14 | height: 240px; 15 | width: 100%; 16 | border-radius: 8px; 17 | object-fit: cover; 18 | border: 1px solid white; 19 | @include c.center(); 20 | } 21 | 22 | .details { 23 | width: 100%; 24 | height: 60px; 25 | display: flex; 26 | padding: 0px 20px; 27 | .emoji { 28 | width: 10%; 29 | font-size: 24px; 30 | @include c.center(); 31 | margin-right: 12px; 32 | padding-bottom: 2px; 33 | } 34 | 35 | .utility { 36 | width: 90%; 37 | display: flex; 38 | flex-direction: row; 39 | justify-content: space-between; 40 | align-items: center; 41 | input[type=file] { 42 | display: none; 43 | } 44 | .upload { 45 | position: relative; 46 | border: 1px solid #fff; 47 | display: inline-block; 48 | cursor: pointer; 49 | border-radius: 8px; 50 | padding: 8px 12px; 51 | height: 30px; 52 | @include c.center(); 53 | background-color: rgb(0, 0, 0, 0.5); 54 | } 55 | .send { 56 | height: 30px; 57 | width: 25px; 58 | border: none; 59 | border-radius: 8px; 60 | cursor: pointer; 61 | @include c.center(); 62 | } 63 | } 64 | 65 | } 66 | 67 | 68 | @media screen and (max-width: 600px) { 69 | margin-right: 0px; 70 | margin-bottom: 30px; 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TooFake: a bereal viewer & web client 2 | 3 | ### want to stalk your friends, family, or ex without posting your own bereal?
toofake gives the ability to view & download bereals, and post custom bereals & realmojis without ever having to click on that annoying notification 4 | 5 | ### https://toofake.lol 6 | 7 | --- 8 | 9 | # current status: IFFY - help wanted! 10 | 11 | BeReal will only continue to change and get more advanced, breaking projects like toofake & befake. The open source community has kept this project going for very long & hopefully can for much longer! Any Pull Requests or changes are always happily welcomed! 12 | 13 | --- 14 | 15 | # how to run locally 16 | 17 | **node** 18 | * clone the project `git clone https://github.com/s-alad/toofake.git` 19 | * cd into the /client directory `cd client` 20 | * run `npm install` 21 | * run `npm run dev` 22 | 23 | **docker** 24 | * dockerhub: https://hub.docker.com/repository/docker/ssalad/toofake/general 25 | * clone the project `git clone https://github.com/s-alad/toofake.git` 26 | * cd into the /client directory `cd client` 27 | * run `docker build . -t toofake` 28 | * run `docker run -d -p 3000:3000 toofake` 29 | 30 | --- 31 | 32 | 33 | # TODO 34 | 35 | - [ ] fix webp, .heic and .heif image issues !!! 36 | - [ ] delete comment ability 37 | - [ ] add instant realmoji 38 | - [ ] react-all realmoji 39 | - [ ] not all friends show for big friends list 40 | - [ ] feed not fetching for big friends list 41 | - [ ] fix occasional login 500 errors 42 | - [ ] change state & login management 43 | - [ ] cache things and use less requests 44 | - [ ] move things to proxy? 45 | - [ ] fix overused state and spaghetti code 46 | 47 | --- 48 | # Peers 49 | 50 | toofake owes a lot to the open source community & great peer projects that have been cease & desisted: 51 | ### shoutout to [rvaidun's befake](https://github.com/rvaidun) for many parts of the reverse engineering! 52 | ### shoutout to the community at [BeFake](https://github.com/notmarek/BeFake) for exposing many of the api endpoints! 53 | ### heavily inspired by [shomil](https://shomil.me/bereal/) 54 | --- 55 | -------------------------------------------------------------------------------- /client/pages/api/refresh.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { GAPIKEY, PROXY } from '@/utils/constants'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | let headers_list = {"Accept": "application/json","User-Agent": "BeReal/8586 CFNetwork/1240.0.4 Darwin/20.6.0","x-ios-bundle-identifier": "AlexisBarreyat.BeReal","Content-Type": "application/json"} 7 | let firebase_refresh_token = req.body.refresh; 8 | 9 | let firebase_refresh_data = JSON.stringify({ 10 | "grantType": "refresh_token", 11 | "refreshToken": firebase_refresh_token 12 | }); 13 | let firebase_refresh_options = { 14 | url: `https://securetoken.googleapis.com/v1/token?key=${GAPIKEY}`, 15 | method: "POST", 16 | headers: headers_list, 17 | data: firebase_refresh_data, 18 | } 19 | let firebase_refresh_response = await axios.request(firebase_refresh_options); 20 | 21 | let new_firebase_token = firebase_refresh_response.data.id_token; 22 | let new_firebase_refresh_token = firebase_refresh_response.data.refresh_token; 23 | let firebase_expiration = Date.now() + firebase_refresh_response.data.expires_in * 1000; 24 | 25 | // ============================================================================================ 26 | 27 | let access_grant = JSON.stringify({ 28 | "grant_type": "firebase", 29 | "client_id": "ios", 30 | "client_secret": "962D357B-B134-4AB6-8F53-BEA2B7255420", 31 | "token": new_firebase_token 32 | }); 33 | let access_grant_options = { 34 | url: `${PROXY}https://auth.bereal.team/token?grant_type=firebase`, 35 | method: "POST", 36 | headers: headers_list, 37 | data: access_grant, 38 | } 39 | return await axios.request(access_grant_options).then( 40 | (response) => { 41 | let bereal_access_token = response.data.access_token; 42 | res.status(200).json({ 43 | status: "success", 44 | token: bereal_access_token, 45 | firebase_id_token: new_firebase_token, 46 | firebase_refresh_token: new_firebase_refresh_token, 47 | expiration: firebase_expiration 48 | }) 49 | } 50 | ).catch( 51 | (error) => { 52 | console.log("ERROR"); 53 | console.log(error); 54 | res.status(400).json({ status: "error" }); 55 | } 56 | ); 57 | } -------------------------------------------------------------------------------- /client/pages/profile/profile.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/common.scss" as c; 2 | 3 | .me { 4 | .card { 5 | 6 | /* border: 1px solid white; */ 7 | border-radius: 8px; 8 | box-shadow: 0px 0px 20px .1px rgba(84, 84, 84, 0.75); 9 | 10 | .pfp { 11 | width: 300px; 12 | border: 1px solid white; 13 | border-radius: 8px; 14 | min-height: 200px; 15 | @include c.center(); 16 | } 17 | 18 | .details { 19 | padding: 10px 20px; 20 | display: flex; 21 | flex-direction: column; 22 | 23 | .detail { 24 | display: flex; 25 | flex-direction: column; 26 | margin-bottom: 8px; 27 | .label { 28 | font-size: 12px; 29 | color: gray; 30 | } 31 | .value { 32 | font-size: 18px; 33 | } 34 | } 35 | } 36 | } 37 | .divider { 38 | margin-top: 40px; 39 | 40 | height: 1px; 41 | background-color: white; 42 | } 43 | .friends { 44 | a { 45 | text-decoration: none; 46 | color: inherit; 47 | } 48 | display: flex; 49 | flex-direction: column; 50 | margin-top: 20px; 51 | 52 | .title { 53 | font-size: 20px; 54 | font-weight: 600; 55 | margin-bottom: 12px; 56 | } 57 | 58 | .friend { 59 | border-radius: 8px; 60 | box-shadow: 0px 0px 5px .1px rgba(84, 84, 84, 0.75); 61 | display: flex; 62 | margin-bottom: 12px; 63 | .pfp { 64 | width: 60px; 65 | height: 60px; 66 | border: 1px solid white; 67 | border-radius: 8px; 68 | @include c.center(); 69 | } 70 | .details { 71 | display: flex; 72 | flex-direction: column; 73 | padding-left: 18px; 74 | justify-content: center; 75 | 76 | .username { 77 | font-size: 16px; 78 | font-weight: 600; 79 | margin-bottom: 4px; 80 | } 81 | .fullname { 82 | font-size: 14px; 83 | color: rgb(158, 158, 158); 84 | } 85 | } 86 | 87 | } 88 | 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /client/pages/realmojis/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState } from 'react' 3 | import { useEffect } from 'react' 4 | import axios from 'axios' 5 | import useCheck from '@/utils/check'; 6 | import myself from '@/utils/myself'; 7 | 8 | import l from '@/styles/loader.module.scss'; 9 | import s from './realmojis.module.scss' 10 | 11 | import User from '@/models/user'; 12 | import Friend from '@/models/friend'; 13 | import Link from 'next/link'; 14 | import Realmoji from '@/components/realmoji/realmoji'; 15 | import Moji from '@/models/moji'; 16 | 17 | export default function RealMojis() { 18 | 19 | useCheck(); 20 | 21 | let emoji_lookup: {[key: string]: string} = { 22 | "😍": "heartEyes", 23 | "😂": "laughing", 24 | "😲": "surprised", 25 | "😃": "happy", 26 | "👍": "up" 27 | } 28 | 29 | let [myRealMojis, setMyRealMojis] = useState<{[key: string]: Moji | undefined}>( 30 | { 31 | "😍": undefined, 32 | "😂": undefined, 33 | "😲": undefined, 34 | "😃": undefined, 35 | "👍": undefined 36 | } 37 | ); 38 | 39 | useEffect(() => { 40 | myself() 41 | 42 | if (!localStorage.getItem("myself")) { 43 | return; 44 | } 45 | 46 | let my_real_mojis = JSON.parse(localStorage.getItem("myself")!).realmojis; 47 | console.log("MY MOJIS"); 48 | console.log(my_real_mojis); 49 | 50 | let my_current_realmojis = myRealMojis 51 | for (let i = 0; i < my_real_mojis.length; i++) { 52 | 53 | let emoji = my_real_mojis[i].emoji; 54 | 55 | let my_real_moji: Moji = { 56 | id: my_real_mojis[i].id, 57 | emoji: emoji, 58 | url: my_real_mojis[i].media.url, 59 | userId: my_real_mojis[i].userId, 60 | type: emoji_lookup[emoji] 61 | } 62 | 63 | my_current_realmojis[emoji] = my_real_moji; 64 | } 65 | 66 | console.log("MY CURRENT MOJIS"); 67 | console.log(my_current_realmojis); 68 | 69 | setMyRealMojis({...my_current_realmojis}); 70 | 71 | }, []) 72 | 73 | return ( 74 |
75 | { 76 | Object.keys(myRealMojis).map((emoji, index) => { 77 | return ( 78 | 79 | ) 80 | }) 81 | } 82 |
83 | ) 84 | 85 | } -------------------------------------------------------------------------------- /client/pages/me/me.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/common.scss" as c; 2 | 3 | .me { 4 | .card { 5 | 6 | /* border: 1px solid white; */ 7 | border-radius: 8px; 8 | box-shadow: 0px 0px 20px .1px rgba(84, 84, 84, 0.75); 9 | 10 | .pfp { 11 | width: 300px; 12 | min-width: 300px; 13 | border: 1px solid white; 14 | border-radius: 8px; 15 | min-height: 200px; 16 | @include c.center(); 17 | } 18 | 19 | .details { 20 | padding: 10px 20px; 21 | display: flex; 22 | flex-direction: column; 23 | 24 | .detail { 25 | display: flex; 26 | flex-direction: column; 27 | margin-bottom: 8px; 28 | .label { 29 | font-size: 12px; 30 | color: gray; 31 | } 32 | .value { 33 | font-size: 18px; 34 | } 35 | } 36 | } 37 | } 38 | .divider { 39 | margin-top: 40px; 40 | 41 | height: 1px; 42 | background-color: white; 43 | } 44 | .friends { 45 | a { 46 | text-decoration: none; 47 | color: inherit; 48 | } 49 | display: flex; 50 | flex-direction: column; 51 | margin-top: 20px; 52 | 53 | .title { 54 | font-size: 20px; 55 | font-weight: 600; 56 | margin-bottom: 12px; 57 | } 58 | 59 | .friend { 60 | border-radius: 8px; 61 | box-shadow: 0px 0px 5px .1px rgba(84, 84, 84, 0.75); 62 | display: flex; 63 | margin-bottom: 12px; 64 | .pfp { 65 | width: 60px; 66 | height: 60px; 67 | border: 1px solid white; 68 | border-radius: 8px; 69 | @include c.center(); 70 | } 71 | .details { 72 | display: flex; 73 | flex-direction: column; 74 | padding-left: 18px; 75 | justify-content: center; 76 | 77 | .username { 78 | font-size: 16px; 79 | font-weight: 600; 80 | margin-bottom: 4px; 81 | } 82 | .fullname { 83 | font-size: 14px; 84 | color: rgb(158, 158, 158); 85 | } 86 | } 87 | 88 | } 89 | 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /client/components/memoire/memoire.tsx: -------------------------------------------------------------------------------- 1 | import Memory from "@/models/memory" 2 | import s from "./memoire.module.scss" 3 | import Draggable from "react-draggable" 4 | import { useEffect, useState } from "react"; 5 | import axios from "axios"; 6 | 7 | 8 | export default function Memoire({ memory }: { memory: Memory }) { 9 | 10 | let [swap, setSwap] = useState(false); 11 | 12 | let date = new Date(memory.date); 13 | let formatOptions: any = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; 14 | 15 | let [location, setLocation] = useState(""); 16 | 17 | async function getLocation() { 18 | 19 | if (memory.location == undefined) { 20 | setLocation("No location data"); 21 | return; 22 | } 23 | 24 | let mem = memory; 25 | 26 | let lat = mem.location!.latitude; 27 | let long = mem.location!.longitude; 28 | console.log(lat, long); 29 | 30 | try { 31 | console.log("axios started"); 32 | let response = await axios.get( 33 | `https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode?location=${long},${lat}&outSR=&forStorage=false&f=pjson` 34 | ) 35 | console.log(response.data) 36 | setLocation(response.data.address.Match_addr + ", " + response.data.address.City) 37 | } catch (error) { 38 | console.log(error); 39 | setLocation("No location data"); 40 | } 41 | } 42 | 43 | /* useEffect(() => { 44 | getLocation(); 45 | }, []) */ 46 | 47 | return ( 48 |
49 |
50 |
51 | {date.toLocaleDateString(undefined, formatOptions)} 52 |
53 |
54 | {location} 55 |
56 |
57 |
58 | 59 |
setSwap(!swap)}> 60 | 61 | setSwap(!swap)} onMouseDown={(e) => { e.stopPropagation() }} /> 62 | 63 |
64 |
65 |
66 | ) 67 | } -------------------------------------------------------------------------------- /client/pages/post/post.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/common.scss" as c; 2 | 3 | .images { 4 | display: flex; 5 | flex-wrap: wrap; 6 | .img { 7 | position: relative; 8 | width: 300px; 9 | min-width: 300px; 10 | height: 400px; 11 | border: 2px solid white; 12 | margin-right: 18px; 13 | border-radius: 12px; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | margin-bottom: 14px; 18 | input[type=file] { 19 | display: none; 20 | } 21 | .upload { 22 | position: absolute; 23 | border: 2px solid #fff; 24 | display: inline-block; 25 | cursor: pointer; 26 | border-radius: 12px; 27 | padding-left: 14px; padding-right: 14px; 28 | height: 40px; 29 | @include c.center(); 30 | background-color: rgb(0, 0, 0, 0.5); 31 | } 32 | .sub { 33 | height: 100%; 34 | width: 100%; 35 | img { 36 | height: 100%; 37 | width: 100%; 38 | object-fit: cover; 39 | border-radius: 12px; 40 | } 41 | .data { 42 | position: absolute; 43 | top: 0px; 44 | } 45 | } 46 | } 47 | } 48 | .caption { 49 | padding-left: 14px; padding-right: 14px; 50 | width: 100%; 51 | min-width: 300px; 52 | max-width: 620px; 53 | height: 40px; 54 | border-radius: 12px; 55 | background-color: white; 56 | color: black; 57 | 58 | @media screen and (max-width: 675px) { 59 | margin-left: 0px !important; 60 | width: 300px !important; 61 | } 62 | } 63 | .submit { 64 | margin-top: 14px; 65 | cursor: pointer; 66 | @include c.center(); 67 | border: 2px solid white; 68 | min-width: 144px; 69 | width: 144px; 70 | height: 40px; 71 | border-radius: 12px; 72 | padding-left: 14px; padding-right: 14px; 73 | } 74 | .info { 75 | max-width: 600px; 76 | margin-top: 12px; 77 | color: white; 78 | font-size: 12px; 79 | } 80 | .failure { 81 | color: rgb(165, 0, 0); 82 | font-size: 14px; 83 | margin-top: 12px; 84 | } 85 | .success { 86 | color: cyan; 87 | font-size: 14px; 88 | margin-top: 12px; 89 | } 90 | .loading { 91 | margin-top: 12px; 92 | color: green; 93 | font-size: 14px; 94 | } 95 | 96 | .switchMode { 97 | border: 2px solid #fff; 98 | display: inline-block; 99 | cursor: pointer; 100 | border-radius: 12px; 101 | padding-left: 14px; padding-right: 14px; 102 | height: 40px; 103 | @include c.center(); 104 | background-color: rgb(0, 0, 0, 0.5); 105 | color: white; 106 | margin-bottom: 14px; 107 | 108 | } -------------------------------------------------------------------------------- /client/pages/api/otp/fire/send.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { GAPIKEY } from '@/utils/constants'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | 7 | let phone_number = req.body.number; 8 | console.log(phone_number) 9 | 10 | let receipt_headers = { 11 | "content-type": "application/json", 12 | "accept": "*/*", 13 | "x-client-version": "iOS/FirebaseSDK/9.6.0/FirebaseCore-iOS", 14 | "x-ios-bundle-identifier": "AlexisBarreyat.BeReal", 15 | "accept-language": "en", 16 | "user-agent": 17 | "FirebaseAuth.iOS/9.6.0 AlexisBarreyat.BeReal/0.31.0 iPhone/14.7.1 hw/iPhone9_1", 18 | "x-firebase-locale": "en", 19 | "x-firebase-gmpid": "1:405768487586:ios:28c4df089ca92b89", 20 | } 21 | let receipt_body = { "appToken": "54F80A258C35A916B38A3AD83CA5DDD48A44BFE2461F90831E0F97EBA4BB2EC7" } 22 | 23 | let receipt_options = { 24 | url: `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyClient?key=${GAPIKEY}`, 25 | method: "POST", 26 | headers: receipt_headers, 27 | data: receipt_body, 28 | } 29 | console.log(receipt_options) 30 | 31 | 32 | let receipt_response = await axios.request(receipt_options) 33 | let receipt = receipt_response.data.receipt; 34 | 35 | console.log("receipt response"); 36 | console.log(receipt_response.data); 37 | console.log('---------------------') 38 | 39 | 40 | let otp_request_headers = { 41 | "content-type": "application/json", 42 | "accept": "*/*", 43 | "x-client-version": "iOS/FirebaseSDK/9.6.0/FirebaseCore-iOS", 44 | "x-ios-bundle-identifier": "AlexisBarreyat.BeReal", 45 | "accept-language": "en", 46 | "user-agent": 47 | "FirebaseAuth.iOS/9.6.0 AlexisBarreyat.BeReal/0.28.2 iPhone/14.7.1 hw/iPhone9_1", 48 | "x-firebase-locale": "en", 49 | "x-firebase-gmpid": "1:405768487586:ios:28c4df089ca92b89", 50 | } 51 | let otp_request_body = { 52 | "phoneNumber": phone_number, 53 | "iosReceipt": receipt, 54 | } 55 | let otp_request_options = { 56 | url: `https://www.googleapis.com/identitytoolkit/v3/relyingparty/sendVerificationCode?key=${GAPIKEY}`, 57 | method: "POST", 58 | headers: otp_request_headers, 59 | data: otp_request_body, 60 | } 61 | 62 | return axios.request(otp_request_options).then( 63 | (response) => { 64 | console.log("otp request response"); 65 | console.log(response.data); 66 | console.log('---------------------') 67 | let session_info = response.data.sessionInfo; 68 | res.status(200).json({ status: "success", session_info: session_info }); 69 | } 70 | ).catch( 71 | (error) => { 72 | console.log("THERE IS AN ERROR") 73 | console.log(error.response); 74 | console.log(error.response.data) 75 | console.log(error.response.data.error) 76 | res.status(400).json({ status: "error", errorData: error.response.data.error }); 77 | } 78 | ) 79 | 80 | } -------------------------------------------------------------------------------- /client/pages/react/index.tsx: -------------------------------------------------------------------------------- 1 | import Instance from "@/models/instance"; 2 | import useCheck from "@/utils/check"; 3 | import axios from "axios"; 4 | import { useRouter } from "next/router"; 5 | import React, { useEffect, useState } from "react"; 6 | 7 | import s from "./react.module.scss"; 8 | import l from "@/styles/loader.module.scss"; 9 | 10 | export default function Reacter() { 11 | let router = useRouter(); 12 | if (!useCheck()) { 13 | return <> 14 | } 15 | let [instances, setInstances] = useState<{ [key: string]: Instance }>({}) 16 | let [loading, setLoading] = useState(true); 17 | 18 | async function feed() { 19 | 20 | let authorization_token = localStorage.getItem("token"); 21 | 22 | let headers = { 23 | "authorization": "Bearer " + authorization_token, 24 | } 25 | 26 | console.log("FETCING FEED") 27 | 28 | return axios.request({ 29 | url: "https://toofake-cors-proxy-4fefd1186131.herokuapp.com/" + "https://mobile.bereal.com/api" + "/feeds/friends-v1", 30 | method: "GET", 31 | headers: headers, 32 | }).then( 33 | async (response) => { 34 | console.log("------------------") 35 | console.log("all request feed success"); 36 | console.log(response.data); 37 | console.log("------------------") 38 | 39 | let newinstances: { [key: string]: Instance } = {}; 40 | async function createInstance(data: any, usr: any) { 41 | /* console.log("CURRENT INSTANCE DATA"); 42 | console.log(data); 43 | console.log("=====================================") */ 44 | let id = data.id; 45 | let newinstance = await Instance.moment(data, usr); 46 | newinstances[id] = newinstance; 47 | /* console.log("newinstances"); 48 | console.log(newinstances); */ 49 | } 50 | 51 | let friends = response.data.friendsPosts; 52 | 53 | for (let i = 0; i < friends.length; i++) { 54 | let thisuser = friends[i].user; 55 | let posts = friends[i].posts; 56 | for (let j = 0; j < posts.length; j++) { 57 | let post = posts[j]; 58 | try { 59 | await createInstance(post, thisuser); 60 | setInstances({...newinstances}); 61 | } catch (error) { 62 | console.log("COULDNT MAKE INSTANCE WITH DATA: ", post) 63 | console.log(error); 64 | } 65 | } 66 | } 67 | 68 | console.log("newinstances"); 69 | console.log(newinstances); 70 | console.log("=====================================") 71 | setLoading(false); 72 | } 73 | ).catch( 74 | (error) => { 75 | console.log(error); 76 | setLoading(false); 77 | } 78 | ) 79 | } 80 | 81 | useEffect(() => { 82 | 83 | setLoading(true); 84 | 85 | feed() 86 | }, []); 87 | 88 | 89 | return ( 90 |
91 | { 92 | loading ?
: 93 |
94 | 95 |
96 | } 97 |
98 | ); 99 | } -------------------------------------------------------------------------------- /client/utils/check.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import axios from 'axios'; 4 | 5 | export default function useCheck() { 6 | const router = useRouter(); 7 | 8 | 9 | function removeStorage() { 10 | localStorage.removeItem("token"); 11 | localStorage.removeItem("firebase_refresh_token"); 12 | localStorage.removeItem("firebase_id_token"); 13 | localStorage.removeItem("expiration"); 14 | localStorage.removeItem("uid"); 15 | localStorage.removeItem("is_new_user"); 16 | localStorage.removeItem("token_type"); 17 | localStorage.removeItem("myself") 18 | } 19 | 20 | useEffect(() => { 21 | console.log("====================================") 22 | console.log("CHECKING STATE") 23 | 24 | let token = localStorage.getItem("token"); 25 | let firebase_refresh_token = localStorage.getItem("firebase_refresh_token"); 26 | let expiration = localStorage.getItem("expiration"); 27 | let now = Date.now(); 28 | console.log(token); 29 | console.log(firebase_refresh_token); 30 | console.log(expiration); 31 | console.log(now); 32 | 33 | if (token == null || expiration == null || firebase_refresh_token == null) { 34 | console.log("no token or expiration or refresh_token"); 35 | removeStorage(); 36 | router.push("/"); 37 | return; 38 | } else { 39 | if (now > parseInt(expiration)) { 40 | console.log("token expired, attempting refresh"); 41 | 42 | axios.request( 43 | { 44 | url: "/api/refresh", 45 | method: "POST", 46 | data: { refresh: firebase_refresh_token } 47 | } 48 | ).then( 49 | (response) => { 50 | console.log(response.data); 51 | if (response.data.status == "success") { 52 | console.log("refresh success"); 53 | let new_token = response.data.token; 54 | let new_firebase_id_token = response.data.firebase_id_token; 55 | let new_firebase_refresh_token = response.data.firebase_refresh_token; 56 | let expiration = response.data.expiration; 57 | 58 | localStorage.setItem("token", new_token); 59 | localStorage.setItem("firebase_refresh_token", new_firebase_refresh_token); 60 | localStorage.setItem("firebase_id_token", new_firebase_id_token); 61 | localStorage.setItem("expiration", expiration); 62 | 63 | console.log("refreshing page"); 64 | router.reload(); // don't know if this works yet 65 | 66 | } else { 67 | console.log("refresh error"); 68 | removeStorage(); 69 | router.push("/"); 70 | return; 71 | } 72 | }).catch( 73 | (error) => { 74 | console.log("refresh fetch error"); 75 | console.log(error); 76 | removeStorage(); 77 | router.push("/"); 78 | return; 79 | } 80 | ) 81 | } 82 | } 83 | 84 | console.log("token is valid"); 85 | 86 | 87 | 88 | if (router.pathname == "/") { 89 | router.push("/feed"); 90 | } 91 | 92 | console.log("====================================") 93 | }, []) 94 | 95 | return true; 96 | } -------------------------------------------------------------------------------- /client/pages/post/post-with-camera/postcamera.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../../styles/common.scss" as c; 2 | 3 | .images { 4 | display: flex; 5 | flex-wrap: wrap; 6 | .img { 7 | position: relative; 8 | width: 300px; 9 | min-width: 300px; 10 | height: 400px; 11 | border: 2px solid white; 12 | margin-right: 18px; 13 | border-radius: 12px; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | margin-bottom: 14px; 18 | input[type=file] { 19 | display: none; 20 | } 21 | .upload { 22 | position: absolute; 23 | border: 2px solid #fff; 24 | display: inline-block; 25 | cursor: pointer; 26 | border-radius: 12px; 27 | padding-left: 14px; padding-right: 14px; 28 | height: 40px; 29 | @include c.center(); 30 | background-color: rgb(0, 0, 0, 0.5); 31 | color: white; 32 | } 33 | .sub { 34 | height: 100%; 35 | width: 100%; 36 | img { 37 | height: 100%; 38 | width: 100%; 39 | object-fit: cover; 40 | border-radius: 12px; 41 | } 42 | .data { 43 | position: absolute; 44 | top: 0px; 45 | } 46 | } 47 | } 48 | } 49 | .caption { 50 | padding-left: 14px; padding-right: 14px; 51 | width: 100%; 52 | min-width: 300px; 53 | max-width: 620px; 54 | height: 40px; 55 | border-radius: 12px; 56 | background-color: white; 57 | color: black; 58 | 59 | @media screen and (max-width: 675px) { 60 | margin-left: 0px !important; 61 | width: 300px !important; 62 | } 63 | } 64 | 65 | .switchMode { 66 | border: 2px solid #fff; 67 | display: inline-block; 68 | cursor: pointer; 69 | border-radius: 12px; 70 | padding-left: 14px; padding-right: 14px; 71 | height: 40px; 72 | @include c.center(); 73 | background-color: rgb(0, 0, 0, 0.5); 74 | color: white; 75 | margin-bottom: 14px; 76 | } 77 | 78 | .submit { 79 | margin-top: 14px; 80 | cursor: pointer; 81 | @include c.center(); 82 | border: 2px solid white; 83 | min-width: 144px; 84 | width: 144px; 85 | height: 40px; 86 | border-radius: 12px; 87 | padding-left: 14px; padding-right: 14px; 88 | } 89 | .info { 90 | max-width: 600px; 91 | margin-top: 12px; 92 | color: white; 93 | font-size: 12px; 94 | } 95 | .failure { 96 | color: rgb(165, 0, 0); 97 | font-size: 14px; 98 | margin-top: 12px; 99 | } 100 | .success { 101 | color: cyan; 102 | font-size: 14px; 103 | margin-top: 12px; 104 | } 105 | .loading { 106 | margin-top: 12px; 107 | color: green; 108 | font-size: 14px; 109 | } 110 | .video { 111 | width: 100%; 112 | height: auto; 113 | display: block; 114 | } 115 | 116 | .canvas { 117 | display: none; 118 | } 119 | 120 | .captureButton { 121 | background-color: #007bff; 122 | color: #fff; 123 | padding: 10px 20px; 124 | text-align: center; 125 | cursor: pointer; 126 | margin-top: 10px; 127 | margin-right: 10px; 128 | border: none; 129 | border-radius: 5px; 130 | } 131 | 132 | .swapButton { 133 | background-color: grey; 134 | color: #fff; 135 | padding: 10px 20px; 136 | text-align: center; 137 | cursor: pointer; 138 | margin-top: 10px; 139 | margin-right: 10px; 140 | border: none; 141 | border-radius: 5px; 142 | } 143 | 144 | .closeButton { 145 | background-color: maroon; 146 | color: #fff; 147 | padding: 10px 20px; 148 | text-align: center; 149 | cursor: pointer; 150 | margin-top: 10px; 151 | margin-right: 10px; 152 | border: none; 153 | border-radius: 5px; 154 | } 155 | 156 | 157 | .cameraModal { 158 | position: fixed; 159 | top: 50%; 160 | left: 50%; 161 | transform: translate(-50%, -50%); 162 | background: rgba(0, 0, 0, 0.8); 163 | padding: 20px; 164 | border-radius: 10px; 165 | z-index: 1000; 166 | display: flex; 167 | flex-direction: column; 168 | align-items: center; 169 | } 170 | 171 | .cameraControls { 172 | display: flex; 173 | justify-content: center; 174 | margin-top: 20px; 175 | } 176 | 177 | .preview { 178 | margin-top: 10px; 179 | 180 | img { 181 | max-width: 100%; 182 | height: auto; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /client/models/instance.ts: -------------------------------------------------------------------------------- 1 | 2 | import User from './user'; 3 | import Realmoji from './realmoji'; 4 | import Comment from './comment'; 5 | import axios from 'axios'; 6 | 7 | 8 | class Instance { 9 | user: User; 10 | 11 | realmojis: Realmoji[]; 12 | comments: Comment[]; 13 | location: { latitude: number, longitude: number } | undefined; 14 | creationdate: string; 15 | caption: string; 16 | instanceid: string; 17 | primary: string; 18 | secondary: string; 19 | btsMedia: string | undefined; 20 | music: any; 21 | 22 | 23 | constructor( 24 | user: User, 25 | realmojis: Realmoji[], 26 | comments: Comment[], 27 | location: { latitude: number, longitude: number } | undefined, 28 | creationdate: string, 29 | caption: string, 30 | instanceid: string, 31 | primary: string, 32 | secondary: string, 33 | btsMedia: string | undefined, 34 | music: any 35 | ) { 36 | this.user = user; 37 | this.realmojis = realmojis; 38 | this.comments = comments; 39 | this.location = location; 40 | this.creationdate = creationdate; 41 | this.caption = caption; 42 | this.instanceid = instanceid; 43 | this.primary = primary; 44 | this.secondary = secondary; 45 | this.btsMedia = btsMedia; 46 | this.music = music; 47 | } 48 | 49 | // static method to create instances (old api) 50 | static async create(raw: any) { 51 | let user = User.create(raw.user); 52 | 53 | let caption = raw.caption; 54 | let instanceid = raw.id; 55 | let primary = raw.primary.url; 56 | let secondary = raw.secondary.url; 57 | 58 | let creationdate = Instance.formatTime(raw.takenAt); 59 | 60 | let raw_realmojis = raw.realMojis; 61 | let realmojis: Realmoji[] = []; 62 | for (let raw_moji of raw_realmojis) { 63 | realmojis.push(Realmoji.create(raw_moji)); 64 | } 65 | 66 | let initial_location = undefined; 67 | if (raw.location) { 68 | let lat = raw.location._latitude; 69 | let long = raw.location._longitude; 70 | initial_location = { latitude: lat, longitude: long }; 71 | } 72 | let location = initial_location; 73 | 74 | let comments: Comment[] = []; 75 | for (let raw_comment of raw.comment) { 76 | comments.push(Comment.create(raw_comment)); 77 | } 78 | 79 | 80 | let music: any = raw.music ? raw.music : undefined; 81 | 82 | return new Instance(user, realmojis, comments, location, creationdate, caption, instanceid, primary, secondary, "", music); 83 | } 84 | 85 | // same but new api 86 | static async moment(raw: any, rawuser: any) { 87 | let user = User.create(rawuser); 88 | let caption = raw.caption; 89 | let instanceid = raw.id; 90 | let primary = raw.primary.url; 91 | let secondary = raw.secondary.url; 92 | 93 | let creationdate = Instance.formatTime(raw.takenAt); 94 | 95 | let raw_realmojis = raw.realMojis; 96 | let realmojis: Realmoji[] = []; 97 | for (let raw_moji of raw_realmojis) { 98 | realmojis.push(Realmoji.moment(raw_moji)); 99 | } 100 | 101 | let initial_location = undefined; 102 | if (raw.location) { 103 | let lat = raw.location.latitude; 104 | let long = raw.location.longitude; 105 | initial_location = { latitude: lat, longitude: long }; 106 | } 107 | let location = initial_location; 108 | 109 | let comments: Comment[] = []; 110 | for (let raw_comment of raw.comments) { 111 | comments.push(Comment.moment(raw_comment)); 112 | } 113 | 114 | let bts: string | undefined = undefined; 115 | if (raw.btsMedia) { 116 | bts = raw.btsMedia.url; 117 | } 118 | 119 | let music: any = raw.music ? raw.music : undefined; 120 | 121 | return new Instance(user, realmojis, comments, location, creationdate, caption, instanceid, primary, secondary, bts, music); 122 | } 123 | 124 | // static method to format time 125 | static formatTime(takenAt: string): string { 126 | if (!takenAt) return "no date available"; 127 | 128 | let postedDate = new Date(takenAt); 129 | let now = new Date(); 130 | let diffInSeconds = Math.floor((now.getTime() - postedDate.getTime()) / 1000); 131 | 132 | if (diffInSeconds < 60) { 133 | return `${diffInSeconds} seconds ago`; 134 | } else if (diffInSeconds < 3600) { 135 | let minutes = Math.floor(diffInSeconds / 60); 136 | return `${minutes} minutes ago`; 137 | } else { 138 | return postedDate.toLocaleString(undefined, { 139 | hour: '2-digit', 140 | minute: '2-digit', 141 | second: '2-digit', 142 | month: 'short', 143 | day: 'numeric' 144 | }); 145 | } 146 | } 147 | } 148 | 149 | export default Instance; -------------------------------------------------------------------------------- /client/pages/index.module.scss: -------------------------------------------------------------------------------- 1 | @use "../styles/common.scss" as c; 2 | 3 | .wrapper { 4 | display: flex; 5 | flex-direction: column; 6 | overflow-x: hidden; 7 | 8 | } 9 | 10 | .log { 11 | display: flex; 12 | flex-direction: column; 13 | width: 100%; 14 | height: 375px; 15 | 16 | .login, .verify { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: start; 20 | width: 100%; 21 | padding-top: 70px; 22 | height: 70%; 23 | 24 | .text { 25 | font-size: 22px 26 | } 27 | 28 | //check if screen is small 29 | .number { 30 | display: flex; 31 | margin-top: 12px; 32 | 33 | /* @include common.center(); */ 34 | @media (max-width: 600px) { 35 | flex-direction: column !important; 36 | align-items: start !important; 37 | 38 | .send { 39 | margin-top: 16px; 40 | } 41 | } 42 | 43 | .space { 44 | padding-left: 10px; 45 | } 46 | 47 | .digits { 48 | text-decoration: none; 49 | background-color: transparent !important; 50 | color: white; 51 | font-size: 18px; 52 | border-radius: 6px; 53 | border: 2px solid white; 54 | margin-right: 10px; 55 | height: 40px; 56 | } 57 | 58 | .digits:hover { 59 | background-color: transparent !important; 60 | } 61 | 62 | .digits:focus { 63 | outline: 0; 64 | } 65 | 66 | .dropdown { 67 | background-color: transparent; 68 | border: 2px solid white; 69 | } 70 | 71 | .dropdown:hover { 72 | background-color: transparent; 73 | } 74 | 75 | .search { 76 | outline: 0; 77 | text-decoration: none; 78 | background-color: #121212 !important; 79 | } 80 | 81 | .search:focus { 82 | outline: 0; 83 | } 84 | 85 | .search:hover { 86 | background-color: #121212; 87 | } 88 | 89 | .button { 90 | 91 | outline: 0; 92 | text-decoration: none; 93 | } 94 | 95 | .button:hover { 96 | outline: 0; 97 | text-decoration: none; 98 | } 99 | 100 | .button:focus { 101 | outline: 0; 102 | text-decoration: none; 103 | } 104 | 105 | .cont { 106 | background-color: transparent; 107 | } 108 | 109 | .cont:hover { 110 | background-color: transparent; 111 | } 112 | 113 | .country:hover { 114 | background-color: #7a7a7a39 !important; 115 | } 116 | 117 | .country.highlight { 118 | background-color: #7a7a7a39 !important; 119 | } 120 | 121 | .send { 122 | height: 40px; 123 | width: 70px; 124 | min-width: 70px; 125 | background-color: white; 126 | color: black; 127 | font-size: 18px; 128 | border-radius: 6px; 129 | border: 2px solid white; 130 | cursor: pointer; 131 | @include c.center(); 132 | } 133 | } 134 | 135 | .error { 136 | margin-top: 12px; 137 | color: #bf616a; 138 | font-size: 14px; 139 | } 140 | } 141 | 142 | .messages { 143 | display: flex; 144 | flex-direction: column; 145 | align-items: start; 146 | width: 100%; 147 | padding-top: 0px; 148 | padding-bottom: 20px; 149 | height: 30%; 150 | font-size: 12px; 151 | 152 | .failed { 153 | color: rgb(165, 0, 0); 154 | margin-bottom: 12px; 155 | word-wrap: normal; 156 | } 157 | 158 | .help { 159 | color: aqua; 160 | word-wrap: normal; 161 | } 162 | } 163 | } 164 | 165 | .info { 166 | flex: 1; 167 | display: flex; 168 | flex-direction: column; 169 | max-width: 600px; 170 | font-size: 14px; 171 | justify-content: center; 172 | span { 173 | color: yellow; 174 | /* color: red; */ 175 | } 176 | 177 | a { 178 | color: white; 179 | } 180 | 181 | p { 182 | margin: 6px 0px; 183 | } 184 | } 185 | 186 | 187 | 188 | 189 | //overides 190 | .country:hover { 191 | background-color: #7a7a7a39 !important; 192 | } 193 | 194 | .country.highlight { 195 | background-color: #7a7a7a39 !important; 196 | } 197 | 198 | .arrow.up { 199 | border-top: none !important; 200 | border-bottom: 4px solid white !important; 201 | } 202 | 203 | .arrow { 204 | border-top: 4px solid white !important; 205 | } 206 | 207 | .react-tel-input .country-list .country.highlight { 208 | background-color: transparent !important; 209 | } -------------------------------------------------------------------------------- /client/pages/api/otp/fire/verify.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { GAPIKEY, PROXY } from '@/utils/constants'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | 7 | console.log("PROXY", PROXY) 8 | 9 | try { 10 | 11 | let headers_list = {"Accept": "application/json","User-Agent": "BeReal/8586 CFNetwork/1240.0.4 Darwin/20.6.0","x-ios-bundle-identifier": "AlexisBarreyat.BeReal","Content-Type": "application/json"} 12 | 13 | let otp = req.body.code; 14 | let session_info = req.body.session_info; 15 | 16 | let fire_otp_headers = { 17 | "content-type": "application/json", 18 | "x-firebase-client": 19 | "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", 20 | "accept": "*/*", 21 | "x-client-version": "iOS/FirebaseSDK/8.15.0/FirebaseCore-iOS", 22 | "x-firebase-client-log-type": "0", 23 | "x-ios-bundle-identifier": "AlexisBarreyat.BeReal", 24 | "accept-language": "en", 25 | "user-agent": 26 | "FirebaseAuth.iOS/8.15.0 AlexisBarreyat.BeReal/0.22.4 iPhone/14.7.1 hw/iPhone9_1", 27 | "x-firebase-locale": "en", 28 | } 29 | 30 | let fire_otp_body = { 31 | "code": otp, 32 | "sessionInfo": session_info, 33 | "operation": "SIGN_UP_OR_IN" 34 | } 35 | 36 | let fire_otp_options = { 37 | url: `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhoneNumber?key=${GAPIKEY}`, 38 | method: "POST", 39 | headers: fire_otp_headers, 40 | data: fire_otp_body, 41 | } 42 | 43 | let fire_otp_response = await axios.request(fire_otp_options); 44 | 45 | let fire_refresh_token = fire_otp_response.data.refreshToken; 46 | let is_new_user = fire_otp_response.data.isNewUser; 47 | let uid = fire_otp_response.data.localId; 48 | 49 | console.log("otp response"); 50 | console.log(fire_otp_response.data); 51 | console.log('---------------------') 52 | 53 | // ============================================================================================ 54 | 55 | let firebase_refresh_data = JSON.stringify({ 56 | "grantType": "refresh_token", 57 | "refreshToken": fire_refresh_token 58 | }); 59 | let firebase_refresh_options = { 60 | url: `https://securetoken.googleapis.com/v1/token?key=${GAPIKEY}`, 61 | method: "POST", 62 | headers: headers_list, 63 | data: firebase_refresh_data, 64 | } 65 | let firebase_refresh_response = await axios.request(firebase_refresh_options); 66 | 67 | /* if (check_response(firebase_refresh_response)) {return;} */ 68 | 69 | console.log("firebase refresh"); 70 | console.log(firebase_refresh_response.status); 71 | console.log(firebase_refresh_response.data); 72 | console.log('---------------------') 73 | 74 | let firebase_token = firebase_refresh_response.data.id_token; 75 | let firebase_refresh_token = firebase_refresh_response.data.refresh_token; 76 | let user_id = firebase_refresh_response.data.user_id; 77 | let firebase_expiration = Date.now() + firebase_refresh_response.data.expires_in * 1000; 78 | 79 | // ============================================================================================ 80 | 81 | let access_grant = JSON.stringify({ 82 | "grant_type": "firebase", 83 | "client_id": "ios", 84 | "client_secret": "962D357B-B134-4AB6-8F53-BEA2B7255420", 85 | "token": firebase_token 86 | }); 87 | let access_grant_options = { 88 | url: `${PROXY}https://auth.bereal.team/token?grant_type=firebase`, 89 | method: "POST", 90 | headers: headers_list, 91 | data: access_grant, 92 | } 93 | let access_grant_response = await axios.request(access_grant_options); 94 | 95 | /* if (check_response(access_grant_response)) {return;} */ 96 | 97 | let access_token = access_grant_response.data.access_token; 98 | let access_refresh_token = access_grant_response.data.refresh_token; 99 | let access_token_type = access_grant_response.data.token_type; 100 | let access_expiration = Date.now() + access_grant_response.data.expires_in * 1000; 101 | 102 | console.log("access grant"); 103 | console.log(access_grant_response.status); 104 | console.log(access_grant_response.data); 105 | console.log('---------------------') 106 | 107 | res.status(200).json({ 108 | bereal_access_token: access_token, 109 | firebase_refresh_token: firebase_refresh_token, 110 | firebase_id_token: firebase_token, 111 | token_type: access_token_type, 112 | expiration: firebase_expiration, 113 | uid: uid, 114 | is_new_user: is_new_user 115 | }); 116 | 117 | 118 | } 119 | catch (error: any) { 120 | console.log("FAILURE") 121 | console.log(error); 122 | console.log('---------------------') 123 | 124 | let error_message; 125 | 126 | if (error.response) { 127 | error_message = JSON.stringify(error.response.data); 128 | } else { 129 | error_message = error.toString(); 130 | } 131 | console.log(error_message); 132 | res.status(400).json({ error: error_message, full_error: error }); 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /client/pages/api/otp/vonage/verify.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios'; 3 | import { GAPIKEY } from '@/utils/constants'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | 7 | function check_response(response: { status: number; data: any; }) { 8 | if (response.status > 350 || response.status == 16) { 9 | console.log("error | ", response); 10 | res.status(400).json({ status: response }); 11 | return true; 12 | } 13 | return false; 14 | } 15 | 16 | try { 17 | 18 | let otp = req.body.code; 19 | let vonage_request_id = req.body.vonageRequestId; 20 | 21 | console.log('=====================') 22 | console.log("login process"); 23 | console.log(req.body); 24 | console.log(otp); 25 | console.log(vonage_request_id); 26 | console.log('---------------------') 27 | 28 | let headers_list = {"Accept": "application/json","User-Agent": "BeReal/8586 CFNetwork/1240.0.4 Darwin/20.6.0","x-ios-bundle-identifier": "AlexisBarreyat.BeReal","Content-Type": "application/json"} 29 | 30 | let vonage_body_content = JSON.stringify({ "code": otp, "vonageRequestId": vonage_request_id }); 31 | let vonage_options = { 32 | url: "https://auth.bereal.team/api/vonage/check-code", 33 | method: "POST", 34 | headers: headers_list, 35 | data: vonage_body_content, 36 | } 37 | let response = await axios.request(vonage_options); 38 | 39 | if (check_response(response)) {return;} 40 | 41 | let rstatus = response.data.status; 42 | let token = response.data.token; 43 | let uid = response.data.uid; 44 | console.log("validated"); 45 | console.log(response.data); 46 | console.log('---------------------') 47 | 48 | // ============================================================================================ 49 | 50 | let refresh_body = JSON.stringify({ "token": token, "returnSecureToken": "True" }); 51 | let refresh_options = { 52 | url: `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=${GAPIKEY}`, 53 | method: "POST", 54 | headers: headers_list, 55 | data: refresh_body, 56 | } 57 | let refresh_response = await axios.request(refresh_options) 58 | 59 | let id_token = refresh_response.data.idToken; 60 | let refresh_token = refresh_response.data.refreshToken; 61 | let expires_in = refresh_response.data.expiresIn; 62 | let is_new_user = refresh_response.data.isNewUser; 63 | 64 | console.log("first refresh"); 65 | console.log(refresh_response.status); 66 | console.log(refresh_response.data); 67 | console.log('---------------------') 68 | 69 | // ============================================================================================ 70 | 71 | let firebase_refresh_data = JSON.stringify({ 72 | "grantType": "refresh_token", 73 | "refreshToken": refresh_token 74 | }); 75 | let firebase_refresh_options = { 76 | url: `https://securetoken.googleapis.com/v1/token?key=${GAPIKEY}`, 77 | method: "POST", 78 | headers: headers_list, 79 | data: firebase_refresh_data, 80 | } 81 | let firebase_refresh_response = await axios.request(firebase_refresh_options); 82 | 83 | if (check_response(firebase_refresh_response)) {return;} 84 | 85 | console.log("firebase refresh"); 86 | console.log(firebase_refresh_response.status); 87 | console.log(firebase_refresh_response.data); 88 | console.log('---------------------') 89 | 90 | let firebase_token = firebase_refresh_response.data.id_token; 91 | let firebase_refresh_token = firebase_refresh_response.data.refresh_token; 92 | let user_id = firebase_refresh_response.data.user_id; 93 | let firebase_expiration = Date.now() + firebase_refresh_response.data.expires_in * 1000; 94 | 95 | // ============================================================================================ 96 | 97 | let access_grant = JSON.stringify({ 98 | "grant_type": "firebase", 99 | "client_id": "ios", 100 | "client_secret": "962D357B-B134-4AB6-8F53-BEA2B7255420", 101 | "token": firebase_token 102 | }); 103 | let access_grant_options = { 104 | url: "https://auth.bereal.team/token?grant_type=firebase", 105 | method: "POST", 106 | headers: headers_list, 107 | data: access_grant, 108 | } 109 | let access_grant_response = await axios.request(access_grant_options); 110 | 111 | if (check_response(access_grant_response)) {return;} 112 | 113 | let access_token = access_grant_response.data.access_token; 114 | let access_refresh_token = access_grant_response.data.refresh_token; 115 | let access_token_type = access_grant_response.data.token_type; 116 | let access_expiration = Date.now() + access_grant_response.data.expires_in * 1000; 117 | 118 | console.log("access grant"); 119 | console.log(access_grant_response.status); 120 | console.log(access_grant_response.data); 121 | console.log('---------------------') 122 | 123 | res.status(200).json({ 124 | bereal_access_token: access_token, 125 | firebase_refresh_token: firebase_refresh_token, 126 | firebase_id_token: firebase_token, 127 | token_type: access_token_type, 128 | expiration: firebase_expiration, 129 | uid: uid, 130 | is_new_user: is_new_user 131 | }); 132 | 133 | 134 | } catch (error: any) { 135 | console.log("FAILURE") 136 | console.log(error.response.data); 137 | res.status(400).json({ error: error.response.data }); 138 | } 139 | } -------------------------------------------------------------------------------- /client/components/realmoji/realmoji.tsx: -------------------------------------------------------------------------------- 1 | 2 | import s from "./realmoji.module.scss" 3 | import l from "@/styles/loader.module.scss"; 4 | import { useEffect, useState } from "react"; 5 | import axios from "axios"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faCheck, faUpload } from "@fortawesome/free-solid-svg-icons"; 8 | import { useRouter } from "next/router"; 9 | 10 | interface RealmojiProperties { 11 | emoji: string; 12 | realmoji: any | undefined; 13 | } 14 | 15 | export default function Realmoji({ emoji, realmoji }: RealmojiProperties) { 16 | 17 | let router = useRouter(); 18 | 19 | let [loading, setLoading] = useState(false); 20 | let [success, setSuccess] = useState(false); 21 | let [failure, setFailure] = useState(false); 22 | const [selectedFile, setSelectedFile]: any = useState(); 23 | const [isFilePicked, setIsFilePicked] = useState(false); 24 | const [fileBase64, setFileBase64] = useState(''); 25 | 26 | function getBase64(file: any) { 27 | return new Promise(resolve => { 28 | let fileInfo; 29 | let baseURL: any = ""; 30 | let reader = new FileReader(); 31 | reader.readAsDataURL(file); 32 | 33 | reader.onload = () => { 34 | baseURL = reader.result; 35 | resolve(baseURL); 36 | }; 37 | }); 38 | }; 39 | 40 | function fileHandler(event: any) { 41 | setIsFilePicked(true); 42 | setSelectedFile(event.target.files[0]); 43 | 44 | getBase64(event.target.files[0]).then(result => { 45 | setFileBase64(result!.toString()); 46 | }).catch(err => { 47 | console.log(err); 48 | }); 49 | }; 50 | 51 | function handleSubmission() { 52 | 53 | setLoading(true); 54 | 55 | console.log("BASE64"); 56 | console.log(fileBase64); 57 | 58 | let authorization_token = localStorage.getItem("token"); 59 | 60 | fetch("/api/add/realmoji", { 61 | method: "POST", 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | }, 65 | body: JSON.stringify({ 66 | token: authorization_token, 67 | fileBase64: fileBase64, 68 | emoji: emoji 69 | }) 70 | }).then((response) => { 71 | console.log(response); 72 | if (response.ok) { 73 | setLoading(false); 74 | setSuccess(true); 75 | setTimeout(() => { setSuccess(false); router.reload()}, 5000); 76 | } else { throw new Error("Error: " + response.statusText); } 77 | }).catch((error) => { 78 | console.log(error); 79 | setLoading(false); 80 | setFailure(true) 81 | setTimeout(() => { setFailure(false) }, 5000); 82 | }) 83 | 84 | /* const formData = new FormData(); 85 | formData.append('fileBase64', fileBase64); 86 | formData.append('token', authorization_token!); 87 | formData.append('emoji', emoji); 88 | console.log(formData); 89 | 90 | let options = { 91 | url: "/api/add/realmoji", 92 | method: "POST", 93 | headers: { 'Content-Type': "multipart/form-data" }, 94 | data: formData, 95 | } 96 | 97 | axios.request(options).then( 98 | (response) => { 99 | console.log(response.data); 100 | setLoading(false); 101 | setSuccess(true); 102 | setTimeout(() => { setSuccess(false); router.reload()}, 5000); 103 | } 104 | ).catch( 105 | (error) => { 106 | console.log(error); 107 | setLoading(false); 108 | setFailure(true) 109 | setTimeout(() => { setFailure(false) }, 5000); 110 | } 111 | ) */ 112 | } 113 | 114 | return ( 115 | 116 |
117 | 118 | { 119 | (realmoji[emoji] != undefined) 120 | ? 121 | ( 122 | isFilePicked ? 123 | 124 | : 125 | 126 | ) 127 | : 128 | ( 129 | isFilePicked ? 130 | 131 | : 132 |
no realmoji
133 | ) 134 | } 135 | 136 |
137 |
{emoji}
138 |
139 | 140 | 141 | { 142 | isFilePicked ? 143 | 154 | : "" 155 | } 156 |
157 |
158 |
159 | 160 | ) 161 | } -------------------------------------------------------------------------------- /client/pages/api/add/realmoji.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios' 3 | /* import { File } from "formidable"; 4 | import formidable, { IncomingForm } from "formidable"; */ 5 | import Jimp from "jimp"; 6 | import fs from "fs"; 7 | import sharp from 'sharp'; 8 | import moment from 'moment'; 9 | import { getAuthHeaders } from '@/utils/authHeaders'; 10 | 11 | export const config = { 12 | api: { 13 | bodyParser: { sizeLimit: '12mb', }, 14 | } 15 | }; 16 | 17 | /* export type FormidableParseReturn = { 18 | fields: formidable.Fields; 19 | files: formidable.Files; 20 | }; 21 | 22 | export async function parseFormAsync(req: NextApiRequest, formidableOptions?: formidable.Options): Promise { 23 | const form = formidable(formidableOptions); 24 | 25 | return await new Promise((resolve, reject) => { 26 | form.parse(req, async (err, fields, files) => { 27 | if (err) { 28 | reject(err); 29 | } 30 | 31 | resolve({ fields, files }); 32 | }); 33 | }); 34 | } */ 35 | 36 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 37 | 38 | try { 39 | 40 | /* 41 | const { fields, files } = await parseFormAsync(req); 42 | console.log(fields, files) 43 | 44 | let authorization_token: string = fields["token"] as string; 45 | let filebase64: string = fields["fileBase64"][0] as string; 46 | let emoji: string = fields["emoji"] as string; 47 | */ 48 | 49 | // using fetch 50 | let authorization_token: string = req.body.token; 51 | let filebase64: string = req.body.fileBase64; 52 | let emoji: string = req.body.emoji; 53 | 54 | console.log("emoji"); 55 | console.log(emoji); 56 | 57 | // log the first 20 chars of the base64 string 58 | console.log("BASE64 STRINGS 20chars"); 59 | console.log(filebase64.substring(0, 40)); 60 | console.log('---------------------') 61 | 62 | // drop prefix of base64 string 63 | filebase64 = filebase64.replace(/^data:(image|application)\/(png|webp|jpeg|jpg|octet-stream);base64,/, ""); 64 | 65 | // ============================================================================================ 66 | 67 | //convert base64 to buffer 68 | let file_image_buffer = Buffer.from(filebase64, 'base64'); 69 | console.log("IMAGE BUFFER"); 70 | console.log(file_image_buffer); 71 | console.log('---------------------') 72 | 73 | let sharp_file = await sharp(file_image_buffer).toBuffer(); 74 | const primary_mime_type = (await sharp(sharp_file).metadata()).format; 75 | 76 | 77 | console.log("SHARP IMAGES"); 78 | console.log(sharp_file); 79 | console.log(primary_mime_type); 80 | console.log('---------------------') 81 | 82 | 83 | if (primary_mime_type != 'webp') { 84 | sharp_file = await sharp(sharp_file).toFormat('webp').toBuffer(); 85 | } 86 | 87 | // ============================================================================================ 88 | // upload url 89 | 90 | let upload_options = { 91 | url: "https://mobile.bereal.com/api/content/realmojis/upload-url?mimeType=image/webp", 92 | method: "GET", 93 | headers: getAuthHeaders(authorization_token), 94 | } 95 | 96 | let upload_res = await axios.request(upload_options) 97 | 98 | console.log("upload result"); 99 | console.log(upload_res.data); 100 | console.log('---------------------') 101 | 102 | let primary_res = upload_res.data.data 103 | 104 | let primary_headers = primary_res.headers; 105 | let primary_url = primary_res.url; 106 | let primary_path = primary_res.path; 107 | let primary_bucket = primary_res.bucket; 108 | Object.assign(primary_headers, getAuthHeaders(authorization_token)) 109 | 110 | // ============================================================================================ 111 | 112 | let put_file_options = { 113 | url: primary_url, 114 | method: "PUT", 115 | headers: primary_headers, 116 | /* data: secondary, */ 117 | data: sharp_file, 118 | } 119 | let put_file_res = await axios.request(put_file_options) 120 | console.log("put secondary result"); 121 | console.log(put_file_res.status); 122 | console.log('---------------------') 123 | 124 | // ============================================================================================ 125 | 126 | let post_data: any = { 127 | "media": { 128 | "bucket": primary_bucket, 129 | "path": primary_path, 130 | "width": 500, 131 | "height": 500, 132 | }, 133 | "emoji": `${emoji}` 134 | }; 135 | let post_headers = { 136 | "content-type": "application/json", 137 | "bereal-platform": "iOS", 138 | "bereal-os-version": "14.7.1", 139 | "accept-language": "en-US;q=1.0", 140 | "bereal-app-language": "en-US", 141 | "user-agent": "BeReal/0.28.2 (AlexisBarreyat.BeReal; build:8425; iOS 14.7.1) 1.0.0/BRApiKit", 142 | "bereal-device-language": "en", 143 | ...getAuthHeaders(authorization_token) 144 | } 145 | console.log("post data"); 146 | console.log(post_data); 147 | console.log(post_headers) 148 | console.log('---------------------') 149 | 150 | let post_response = await axios.request({ 151 | method: 'PUT', 152 | url: "https://mobile.bereal.com/api" + "/person/me/realmojis", 153 | data: JSON.stringify(post_data), 154 | headers: post_headers, 155 | }) 156 | 157 | console.log("post response"); 158 | console.log(post_response); 159 | console.log('---------------------') 160 | 161 | res.status(200).json(upload_res.data.data); 162 | 163 | 164 | } catch (error: any) { 165 | console.log("FAILURE") 166 | console.log(error); 167 | console.log('---------------------') 168 | 169 | let error_message; 170 | 171 | if (error.response) { 172 | error_message = JSON.stringify(error.response.data); 173 | } else { 174 | error_message = error.toString(); 175 | } 176 | console.log(error_message); 177 | res.status(400).json({ error: error_message }); 178 | } 179 | } -------------------------------------------------------------------------------- /client/pages/help/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import s from "./help.module.scss"; 4 | 5 | /* import { useTranslation } from 'next-i18next' */ 6 | 7 | export default function Help() { 8 | 9 | 10 | /* const { t, i18n } = useTranslation() */ 11 | 12 | return ( 13 |
14 | 15 |
16 | Hello there! 😄
17 |

If you're here you're probably interested in not using BeReal in the way it was intended to be used, and to be honest thats why I mainly started this project too! Don't worry, there's no judgment ;)

18 |

With TooFake, you can view friends' BeReals without posting your own. You can post custom images. You can add custom reactions and react to posts. You can also comment & react to posts without posting. You can also screenshot and download BeReals without being detected.

19 |

This project is completely open source and still a work in progress. There are many issues that I know of and others that I don't know of; feel free to reach out with bugs or if you'd like to contribute!

20 |

It's based on a lot of the great work done at Notmarek's BeFake and inspired by shomil. Go show them some support!

21 |

BeReal continuously changes their code which sometimes breaks this project. I'll try to keep a status up on the homepage.

22 |

- 🥗

23 |
24 | 25 |
26 | FAQ 27 | How To Use 28 | Common Issues 29 |
30 | 31 |
32 |

FAQ

33 |
34 |
35 |

Will I get banned from BeReal

36 |

TooFake, the BeFake project, and the old BeFake website have been running for over 10 months without anybody getting banned. Trends show you are safe! But as with everything there's always a small risk.

37 |
38 |
39 |

Is TooFake safe?

40 |

TooFake is completely open source; you can check out the code here. It doesn't save any of your credentials. If you are uncomfortable using this client, you can run a local instance aswell!

41 |
42 |
43 |

Why can't I login?

44 |

Logging in should currently be working. TooFake tries to log you in two times with two different BeReal providers; if both fail then there might be an issue I don't know of. Try refreshing the website and trying again.

45 |
46 |
47 |

Can I screenshot or download friends' BeReals?

48 |

Yes, you can take screenshots without notifying your friends. You can also press the download button on the bottom right of a BeReal to download the primary image

49 |
50 |
51 |

Why does the website crash when I try to post images?

52 |

If the images you are posting are .heic, .heif, (iphone images) or .webp images, the website will crash as it currently does not support those. Please try converting them or taking a screenshot of the photos and posting those.

53 |
54 |
55 |

Why does the page I'm at go entirely black or have a client side exception?

56 |

This might happen if there is some error that arises that we haven't handled. Please refresh the page or re-login

57 |
58 |
59 |
60 | 61 |
62 |

How To Use

63 |
64 |
65 |

Logging in

66 |

Login by navigating to toofake.lol and entering your phone number. After that it will try to send a code using two providers, if one fails you'll see red and blue text notifying you it is trying the second. You should recieve a code. Enter the code and press enter once. You'll hopefully be redirected to the homepage where you can view BeReals.

67 |
68 |
69 |

Viewing BeReals

70 |

After logging in, you should see all your friends BeReals. On a computer, you can click any of the images to swap them, and drag the images around. On mobile, click the big image to swap it to the small one.

71 |
72 |
73 |

Posting BeReals

74 |

You can post BeReals by clicking post on the navigation menu. Select your primary and secondary image and add a caption. Submit the image and hopefully it'll get posted. Posting does not support iphone images (.heic & .heif) or .webp images as of yet.

75 |
76 |
77 |

Reacting to BeReals

78 |

You can react to BeReals by clicking the smiley reaction face at the top right of a BeReal next to the username. This will show you your current reactions that you have. Click one to submit it, you'll see a loading sign and a check if it works or an X if it fails.

79 |
80 |
81 |

Adding custom Reactions

82 |
You can add custom reactions by navigating to the realmojis menu on the navigation menu. You'll see your current reactions alongside the emoji theyre associated with. Click on select a new reaction to submit an image, then click the send button that pops up. Hopefully your reaction will get added. You'll see a loading symbol and a check if it works.
NOTE: It may take a minuite or two for your reactions to start showing
83 |
84 |
85 |
86 | 87 |
88 |

Common Issues

89 |
90 | 91 | 92 | 93 |
94 | ) 95 | } -------------------------------------------------------------------------------- /client/pages/profile/[id].tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useRouter } from 'next/router' 3 | import { useEffect, useState } from 'react' 4 | 5 | import s from './profile.module.scss' 6 | 7 | interface Friend { 8 | id: string; 9 | username: string; 10 | fullname: string; 11 | profilePicture: { 12 | url: string | null; // Allow null values 13 | width: number; 14 | height: number; 15 | } | null; // Allow null values 16 | } 17 | 18 | export default function Profile() { 19 | const router = useRouter() 20 | 21 | const [username, setUsername] = useState(""); 22 | const [name, setName] = useState(""); 23 | const [bio, setBio] = useState(""); 24 | const [pfp, setPfp] = useState(""); 25 | const [joinDate, setJoined] = useState(""); 26 | const [location, setLocation] = useState(""); 27 | const [status, setStatus] = useState(""); 28 | const [streak, setStreak] = useState(""); 29 | const [friendedDate, setFriendedDate] = useState(""); 30 | const [mutualFriends, setMutualFriends] = useState([]); 31 | const [mutualFriendsLoading, setMutualFriendsLoading] = useState(true); 32 | 33 | useEffect(() => { 34 | 35 | if (!router.isReady) return; 36 | 37 | const fetchProfileData = async () => { 38 | try { 39 | const rid = router.query.id; 40 | const token = localStorage.getItem("token"); 41 | const body = JSON.stringify({ "token": token, "profile_id": rid }); 42 | const options = { 43 | url: "/api/profile", 44 | method: "POST", 45 | headers: { 'Content-Type': 'application/json' }, 46 | data: body, 47 | }; 48 | 49 | const response = await axios.request(options); 50 | const data = response.data; 51 | 52 | console.log(data); 53 | 54 | setUsername(data.username); 55 | setName(data.fullname); 56 | setBio(data.biography ?? ""); 57 | setLocation(data.location ?? ""); 58 | setStreak(data.streakLength ?? ""); 59 | setPfp(data.profilePicture?.url ?? ""); 60 | setJoined(new Date(data.createdAt).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) + ', ' + new Date(data.createdAt).toLocaleTimeString()); 61 | setStatus(data.relationship.status); 62 | setFriendedDate(data.relationship.friendedAt ? new Date(data.relationship.friendedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) + ', ' + new Date(data.relationship.friendedAt).toLocaleTimeString() : ""); 63 | 64 | // Populate mutual friends 65 | setMutualFriends(data.relationship.commonFriends.sample); 66 | setMutualFriendsLoading(false); 67 | 68 | } catch (error) { 69 | console.error(error); 70 | } 71 | }; 72 | 73 | fetchProfileData(); 74 | }, [router.isReady]); 75 | 76 | const handleFriendClick = (friendId: string) => { 77 | window.location.replace(`/profile/${friendId}`); 78 | }; 79 | 80 | return ( 81 |
82 |
83 | {pfp ? :
no profile picture
} 84 |
85 |
86 |
username
87 |
{username}
88 |
89 |
90 |
name
91 |
{name}
92 |
93 | {bio.length > 0 && ( 94 |
95 |
biography
96 |
{bio}
97 |
98 | )} 99 | {location.length > 0 && ( 100 |
101 |
location
102 |
{location}
103 |
104 | )} 105 |
106 |
date Joined
107 |
{joinDate}
108 |
109 |
110 |
relation
111 |
{status === "accepted" ? "friends" : "stranger"}
112 |
113 | 114 | 115 | {friendedDate.length > 0 && ( 116 |
117 |
date Friended
118 |
{friendedDate}
119 |
120 | )} 121 |
122 |
current streak
123 |
🔥 {streak} 🔥
124 |
125 |
126 | 127 |
128 |
129 |
130 |
Mutual Friends ({mutualFriends.length})
131 | {mutualFriendsLoading ? ( 132 |
133 | ) : ( 134 | mutualFriends.map((friend) => ( 135 |
handleFriendClick(friend.id)} 139 | role="button" 140 | tabIndex={0} 141 | > 142 | {friend.profilePicture && friend.profilePicture.url ? ( 143 | 144 | ) : ( 145 |
no profile picture
146 | )} 147 |
148 |
@{friend.username}
149 |
{friend.fullname}
150 |
151 |
152 | )) 153 | )} 154 |
155 |
156 | ); 157 | } 158 | -------------------------------------------------------------------------------- /client/components/navbar/navbar.module.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/common.scss" as c; 2 | 3 | .toofake { 4 | font-size: 44px; 5 | font-weight: 700; 6 | display: flex; 7 | 8 | .sep { 9 | background-color: white; 10 | height: 100%; 11 | width: 4px; 12 | margin: 0 16px; 13 | } 14 | 15 | .navigation { 16 | flex: 1; 17 | display: flex; 18 | align-items: center; 19 | .fake { 20 | text-decoration: none; 21 | color: white; 22 | @include c.center(); 23 | } 24 | .pagename { 25 | font-size: 28px; 26 | } 27 | 28 | .actions { 29 | display: flex; 30 | flex: 1; 31 | justify-content: flex-end; 32 | align-items: center; 33 | height: 100%; 34 | a { 35 | color: white; 36 | text-decoration: none; 37 | } 38 | .item { 39 | margin-right: 12px; 40 | @include c.center(); 41 | button { 42 | background-color: transparent; 43 | border: none; 44 | color: white; 45 | font-size: 16px; 46 | font-weight: 500; 47 | cursor: pointer; 48 | text-decoration: underline; 49 | } 50 | } 51 | .post { 52 | @include c.center(); 53 | button { 54 | cursor: pointer; 55 | font-size: 16px; 56 | font-weight: 500; 57 | height: 34px; 58 | width: 80px; 59 | border-radius: 8px; 60 | background-color: white; 61 | border: none; 62 | } 63 | } 64 | .letter { 65 | box-shadow: 0px 0px 6px 1px #fff; 66 | height: 45px; 67 | width: 45px; 68 | border-radius: 100%; 69 | object-fit: cover; 70 | background-color: white; 71 | color: black; 72 | @include c.center(); 73 | font-size: 20px; 74 | } 75 | img { 76 | box-shadow: 0px 0px 6px 1px #fff; 77 | height: 45px; 78 | width: 45px; 79 | border-radius: 100%; 80 | object-fit: cover; 81 | background-color: white; 82 | } 83 | } 84 | 85 | .mobile { 86 | display: none; 87 | flex: 1; 88 | justify-content: flex-end; 89 | align-items: center; 90 | height: 100%; 91 | 92 | .helpmobile { 93 | display: flex; 94 | a { 95 | font-size: 14px; 96 | font-weight: 500; 97 | text-decoration: underline; 98 | margin-left: 14px; 99 | } 100 | } 101 | 102 | img { 103 | box-shadow: 0px 0px 6px 1px #fff; 104 | height: 30px; 105 | width: 30px; 106 | border-radius: 100%; 107 | object-fit: cover; 108 | background-color: white; 109 | } 110 | a { 111 | @include c.center(); 112 | color: white; 113 | text-decoration: none; 114 | } 115 | button { 116 | cursor: pointer; 117 | font-size: 12px; 118 | font-weight: 500; 119 | height: 26px; 120 | width: 50px; 121 | border-radius: 6px; 122 | background-color: white; 123 | border: none; 124 | } 125 | .menu { 126 | cursor: pointer; 127 | display: flex; 128 | flex-direction: column; 129 | justify-content: space-between; 130 | width: 26px; 131 | height: 20px; 132 | .line { 133 | height: 2px; 134 | width: 100%; 135 | background-color: white; 136 | /* margin: 4px 0px; */ 137 | } 138 | } 139 | .menuopen { 140 | position: relative; 141 | .line { 142 | &:first-child { 143 | width: 100%; 144 | transform: rotate(45deg); 145 | top: 10px; 146 | position: relative; 147 | } 148 | &:nth-child(2) { 149 | width: 0%; 150 | display: none; 151 | } 152 | &:last-child { 153 | width: 100%; 154 | transform: rotate(-45deg); 155 | top: -8px; 156 | position: relative; 157 | } 158 | } 159 | } 160 | } 161 | 162 | .extra { 163 | display: flex; 164 | height: 100%; 165 | a, span { 166 | color: white; 167 | text-decoration: none; 168 | } 169 | .logout { 170 | margin-right: 1.3em; 171 | font-size: 12px; 172 | button { 173 | background-color: transparent; 174 | border: none; 175 | color: white; 176 | font-size: 1.2em; 177 | font-weight: 500; 178 | cursor: pointer; 179 | text-decoration: underline; 180 | margin: 0px; 181 | padding: 0px; 182 | } 183 | } 184 | button { 185 | cursor: pointer; 186 | font-size: 12px; 187 | font-weight: 500; 188 | height: 26px; 189 | width: min-content; 190 | border-radius: 6px; 191 | background-color: white; 192 | border: none; 193 | } 194 | } 195 | } 196 | 197 | @media screen and (max-width: 830px) { 198 | font-size: 32px; 199 | } 200 | 201 | @media screen and (min-width: 700px) { 202 | .navigation { 203 | .extra { 204 | display: none; 205 | } 206 | } 207 | } 208 | 209 | 210 | @media screen and (max-width: 700px) { 211 | font-size: 28px; 212 | .sep { width: 2px; margin: 0 12px;} 213 | .navigation { 214 | .pagename { font-size: 20px; } 215 | 216 | .actions { 217 | display: none; 218 | } 219 | 220 | .mobile { 221 | display: flex; 222 | } 223 | } 224 | } 225 | 226 | @media screen and (max-width: 420px) { 227 | font-size: 22px; 228 | .navigation { 229 | .pagename { font-size: 16px; } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /client/pages/me/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { useEffect } from 'react' 4 | import axios from 'axios' 5 | import useCheck from '@/utils/check'; 6 | import myself from '@/utils/myself'; 7 | 8 | import s from './me.module.scss' 9 | import l from '@/styles/loader.module.scss'; 10 | import User from '@/models/user'; 11 | import Friend from '@/models/friend'; 12 | import Link from 'next/link'; 13 | 14 | export default function Me() { 15 | 16 | useCheck(); 17 | 18 | let [username, setUsername] = React.useState(""); 19 | let [name, setName] = React.useState(""); 20 | let [bio, setBio] = React.useState(""); 21 | let [pfp, setPfp] = React.useState(""); 22 | let [joinDate, setJoined] = React.useState(""); 23 | let [location, setLocation] = React.useState(""); 24 | let [streak, setStreak] = React.useState(""); 25 | let [friends, setFriends] = React.useState([]); 26 | let [friendsLoading, setFriendsLoading] = React.useState(true); 27 | 28 | 29 | 30 | useEffect(() => { 31 | 32 | if (localStorage && JSON.parse(localStorage.getItem("myself")!)) { 33 | setUsername(JSON.parse(localStorage.getItem("myself")!).username); 34 | setName(JSON.parse(localStorage.getItem("myself")!).fullname); 35 | setBio(JSON.parse(localStorage.getItem("myself")!).biography); 36 | setPfp(JSON.parse(localStorage.getItem("myself")!).profilePicture != undefined ? JSON.parse(localStorage.getItem("myself")!).profilePicture.url : ""); 37 | } 38 | 39 | let token = localStorage.getItem("token"); 40 | let body = JSON.stringify({ "token": token }); 41 | let options = { 42 | url: "/api/me", 43 | method: "POST", 44 | headers: { 'Content-Type': 'application/json' }, 45 | data: body, 46 | } 47 | 48 | axios.request(options).then( 49 | (response) => { 50 | console.log("resp me", response.data); 51 | setUsername(response.data.username); 52 | setName(response.data.fullname); 53 | setBio(response.data.biography); 54 | setPfp(response.data.profilePicture != undefined ? response.data.profilePicture.url : ""); 55 | setLocation(response.data.location ?? ""); 56 | setStreak(response.data.streakLength ?? "") 57 | setJoined(new Date(response.data.createdAt).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) + ', ' + new Date(response.data.createdAt).toLocaleTimeString()); 58 | 59 | } 60 | ).catch( 61 | (error) => { 62 | console.log(error); 63 | } 64 | ) 65 | 66 | let friend_options = { 67 | url: "/api/friends", 68 | method: "POST", 69 | headers: { 'Content-Type': 'application/json' }, 70 | data: body, 71 | } 72 | 73 | axios.request(friend_options).then( 74 | async (response) => { 75 | console.log("resp friend", response.data); 76 | 77 | let raw_friends = response.data.data; 78 | let new_friends: Friend[] = []; 79 | 80 | async function createFriend(data: any) { 81 | let newfriend = await Friend.create(data); 82 | new_friends.push(newfriend); 83 | return newfriend; 84 | } 85 | 86 | for (let i = 0; i < raw_friends.length; i++) { 87 | try { 88 | await createFriend(raw_friends[i]); 89 | setFriendsLoading(false); 90 | setFriends([...new_friends]); 91 | } catch (error) { 92 | console.log("COULDNT MAKE FRIEND WITH DATA: ", raw_friends[i]) 93 | console.log(error); 94 | } 95 | } 96 | 97 | console.log("new friends"); 98 | console.log(new_friends); 99 | } 100 | ).catch( 101 | (error) => { 102 | console.log(error); 103 | } 104 | ) 105 | 106 | 107 | }, []) 108 | 109 | return ( 110 |
111 |
112 | {pfp ? :
no profile picture
} 113 |
114 |
115 |
username
116 |
{username}
117 |
118 |
119 |
name
120 |
{name}
121 |
122 | { 123 | bio && bio.length > 0 ? 124 |
125 |
biography
126 |
{bio}
127 |
: null 128 | } 129 | { 130 | location && location.length > 0 ? 131 |
132 |
location
133 |
{location}
134 |
: null 135 | } 136 |
137 |
date joined
138 |
{joinDate}
139 |
140 |
141 |
current streak
142 |
🔥 {streak} 🔥
143 |
144 |
145 |
146 |
147 |
148 |
Friends ({friends.length})
149 | { 150 | friendsLoading ?
: 151 | friends.map((friend) => { 152 | return ( 153 | 154 |
155 | 156 |
157 |
@{friend.username}
158 |
{friend.fullname}
159 |
160 |
161 | 162 | ) 163 | }) 164 | } 165 |
166 |
167 | ) 168 | } -------------------------------------------------------------------------------- /client/pages/post/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import s from './post.module.scss' 3 | import axios from "axios"; 4 | import useCheck from "@/utils/check"; 5 | import { useRouter } from "next/router"; 6 | 7 | export default function Post() { 8 | 9 | useCheck(); 10 | 11 | let router = useRouter(); 12 | 13 | let [loading, setLoading] = useState(false); 14 | let [failure, setFailure] = useState(""); 15 | let [success, setSuccess] = useState(""); 16 | 17 | const [caption, setCaption] = useState(''); 18 | const [selectedFileOne, setSelectedFileOne]: any = useState(); 19 | const [selectedFileTwo, setSelectedFileTwo]: any = useState(); 20 | const [isFirstFilePicked, setIsFirstFilePicked] = useState(false); 21 | const [isSecondFilePicked, setIsSecondFilePicked] = useState(false); 22 | 23 | const [primarybase64, setPrimaryBase64] = useState(''); 24 | const [secondarybase64, setSecondaryBase64] = useState(''); 25 | 26 | function getBase64(file: any) { 27 | return new Promise(resolve => { 28 | let fileInfo; 29 | let baseURL: any = ""; 30 | let reader = new FileReader(); 31 | reader.readAsDataURL(file); 32 | 33 | reader.onload = () => { 34 | baseURL = reader.result; 35 | resolve(baseURL); 36 | }; 37 | }); 38 | }; 39 | 40 | function fileOneHandler(event: any) { 41 | setIsFirstFilePicked(true); 42 | setSelectedFileOne(event.target.files[0]); 43 | 44 | getBase64(event.target.files[0]).then(result => { 45 | setPrimaryBase64(result!.toString()); 46 | }).catch(err => { 47 | console.log(err); 48 | }); 49 | }; 50 | 51 | function fileTwoHandler(event: any) { 52 | setIsSecondFilePicked(true); 53 | setSelectedFileTwo(event.target.files[0]); 54 | 55 | getBase64(event.target.files[0]).then(result => { 56 | setSecondaryBase64(result!.toString()); 57 | }).catch(err => { 58 | console.log(err); 59 | }); 60 | }; 61 | 62 | function handleSubmission() { 63 | setLoading(true); 64 | 65 | let authorization_token = localStorage.getItem("token"); 66 | 67 | fetch("/api/add/post", { 68 | method: "POST", 69 | headers: { 70 | 'Content-Type': 'application/json' 71 | }, 72 | body: JSON.stringify({ 73 | primaryb64: primarybase64, 74 | secondaryb64: secondarybase64, 75 | caption: caption, 76 | token: authorization_token 77 | }) 78 | }).then( 79 | (response) => { 80 | console.log(response); 81 | if (response.ok) { 82 | setLoading(false); 83 | setSuccess("Successfully posted!"); 84 | setTimeout(() => { setSuccess(""); router.push("/feed")}, 3000); 85 | } else { throw new Error("Error: " + response.statusText); } 86 | } 87 | ).catch((error) => { 88 | console.log(error); 89 | setLoading(false); 90 | setFailure(error.message) 91 | setTimeout(() => { setFailure("") }, 5000); 92 | } 93 | ) 94 | 95 | /* 96 | const formData = new FormData(); 97 | formData.append('primaryb64', primarybase64); 98 | formData.append('secondaryb64', secondarybase64); 99 | formData.append('caption', caption ? caption : ""); 100 | formData.append('token', authorization_token!); 101 | console.log(formData); 102 | 103 | let options = { 104 | url: "/api/add/post", 105 | method: "POST", 106 | headers: { 'Content-Type': "multipart/form-data" }, 107 | data: { 108 | primaryb64: primarybase64, 109 | secondaryb64: secondarybase64, 110 | caption: caption, 111 | token: authorization_token 112 | } 113 | } 114 | 115 | axios.request(options).then( 116 | (response) => { 117 | console.log(response.data); 118 | setLoading(false); 119 | setSuccess("Successfully posted!"); 120 | setTimeout(() => { setSuccess(""); router.push("/feed")}, 3000); 121 | } 122 | ).catch( 123 | (error) => { 124 | console.log(error); 125 | setLoading(false); 126 | setFailure(error.response.data.error) 127 | setTimeout(() => { setFailure("") }, 5000); 128 | } 129 | ) */ 130 | } 131 | 132 | return ( 133 |
134 | 137 |
138 |
139 | 140 | 141 | {isFirstFilePicked ? ( 142 |
143 | 144 |
145 | ) : (<>)} 146 |
147 |
148 | 149 | 150 | {isSecondFilePicked ? ( 151 | <> 152 |
153 | 154 |
155 | 156 | ) : (<>) 157 | } 158 |
159 |
160 | setCaption(txt.target.value)} 161 | disabled 162 | > 163 |
{ handleSubmission() }}> 164 | Post 165 |
166 |
167 | *some photos taken on an iphone (.heic) may not work. if there is an error try taking a screenshot of the image and uploading that instead.
168 | *you might get a client-side exception if the image is too large. The maximum limit currently is 12mb for both images combined. 169 |
170 | {/* fix this nesting */} 171 | { 172 | failure != "" ? ( 173 |
174 | {failure} 175 |
176 | ) : ( 177 | loading ? ( 178 |
179 | loading... 180 |
181 | ) : ( 182 | success != "" ? ( 183 |
184 | {success} 185 |
186 | ) : (<>) 187 | ) 188 | ) 189 | } 190 |
191 | ) 192 | } 193 | -------------------------------------------------------------------------------- /client/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { Inter } from 'next/font/google' 3 | import Image from 'next/image' 4 | import s from './index.module.scss' 5 | import axios from "axios"; 6 | import { useState } from 'react'; 7 | import PhoneInput from 'react-phone-input-2'; 8 | import "react-phone-input-2/lib/bootstrap.css"; 9 | import { useRouter } from 'next/router' 10 | import useCheck from '@/utils/check' 11 | import myself from '@/utils/myself' 12 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 13 | import { faGithub } from '@fortawesome/free-brands-svg-icons' 14 | 15 | const inter = Inter({ subsets: ['latin'] }) 16 | 17 | export default function Home() { 18 | useCheck(); 19 | 20 | let router = useRouter(); 21 | 22 | let [vonageid, setVonageid] = useState(""); 23 | let [firebase_session, set_firebase_session] = useState(""); 24 | let [inputNumber, setInputNumber] = useState(""); 25 | let [inputOTP, setInputOTP] = useState(""); 26 | let [requestedOtp, setRequestedOtp] = useState(false); 27 | 28 | let [failed, setFailed] = useState(""); 29 | let [help, setHelp] = useState(""); 30 | 31 | function failure(text: string) { 32 | console.log(text); 33 | setFailed(`ERROR: ${text}`); 34 | setTimeout(() => {setFailed("");}, 4000); 35 | } 36 | 37 | function helpme() { 38 | setHelp("Failed with Firebase login provider, re-trying to login with Vonage..."); 39 | setTimeout(() => {setHelp("");}, 4000); 40 | } 41 | 42 | 43 | async function verifyOTPVonage(otp: string) { 44 | console.log("client vonage verify otp: ", otp, " vonageid: ", vonageid); 45 | 46 | let body = JSON.stringify({ "code": otp, "vonageRequestId": vonageid }); 47 | let options = { 48 | url: "/api/otp/vonage/verify", 49 | method: "POST", 50 | headers: { 'Content-Type': 'application/json' }, 51 | data: body, 52 | } 53 | 54 | let response = axios.request(options).then( 55 | async (response) => { 56 | console.log(response.data); 57 | localStorage.setItem("token", response.data.bereal_access_token); 58 | localStorage.setItem("firebase_refresh_token", response.data.firebase_refresh_token); 59 | localStorage.setItem("firebase_id_token", response.data.firebase_id_token) 60 | localStorage.setItem("expiration", response.data.expiration) 61 | localStorage.setItem("uid", response.data.uid); 62 | localStorage.setItem("is_new_user", response.data.is_new_user); 63 | localStorage.setItem("token_type", response.data.token_type); 64 | await myself(); 65 | router.push("/feed"); 66 | } 67 | ).catch((error) => {failure((error.response.data.error.error.code + " | "+ error.response.data.error.error.message).toString())}) 68 | } 69 | 70 | async function requestOTPVonage(number: string) { 71 | console.log("client vonage request otp"); 72 | console.log(number); 73 | console.log("------------------") 74 | 75 | let body = JSON.stringify({ "number": number }) 76 | let options = { 77 | url: "/api/otp/vonage/send", 78 | method: "POST", 79 | headers: { 'Content-Type': 'application/json' }, 80 | data: body, 81 | } 82 | 83 | axios.request(options).then( 84 | (response) => { 85 | let rvonageid = response.data.vonageRequestId; 86 | console.log(response.data); 87 | setVonageid(rvonageid); 88 | setRequestedOtp(true); 89 | } 90 | ).catch((error) => {failure("VONAGE REQUEST ERROR: " + JSON.stringify(error.response.data.error));}) 91 | } 92 | 93 | async function verifyOTPFirebase(otp: string) { 94 | console.log("client firebase verify otp: ", otp, " firebase_session: ", firebase_session); 95 | 96 | let body = JSON.stringify({ "code": otp, "session_info": firebase_session }); 97 | let options = { 98 | url: "/api/otp/fire/verify", 99 | method: "POST", 100 | headers: { 'Content-Type': 'application/json' }, 101 | data: body, 102 | } 103 | 104 | let response = axios.request(options).then( 105 | async (response) => { 106 | console.log(response.data); 107 | localStorage.setItem("token", response.data.bereal_access_token); 108 | localStorage.setItem("firebase_refresh_token", response.data.firebase_refresh_token); 109 | localStorage.setItem("firebase_id_token", response.data.firebase_id_token) 110 | localStorage.setItem("expiration", response.data.expiration) 111 | localStorage.setItem("uid", response.data.uid); 112 | localStorage.setItem("is_new_user", response.data.is_new_user); 113 | localStorage.setItem("token_type", response.data.token_type); 114 | await myself(); 115 | router.push("/feed"); 116 | } 117 | ).catch((error) => { 118 | if (error.response) { 119 | failure("FIREBASE VERIFY ERROR: " + error.response.data.error) 120 | }else { 121 | failure("FIREBASE VERIFY ERROR: " + "unknown error, please try re-logging in") 122 | } 123 | }) 124 | } 125 | 126 | async function requestOTPFirebase(number: string) { 127 | console.log("client firebase request otp"); 128 | console.log(number); 129 | console.log("------------------") 130 | 131 | let body = JSON.stringify({ "number": number }) 132 | let options = { 133 | url: "/api/otp/fire/send", 134 | method: "POST", 135 | headers: { 'Content-Type': 'application/json' }, 136 | data: body, 137 | } 138 | 139 | let response = axios.request(options).then( 140 | (response) => { 141 | console.log(response.data); 142 | let firebase_session = response.data.session_info; 143 | set_firebase_session(firebase_session); 144 | setRequestedOtp(true); 145 | } 146 | ).catch( 147 | (error) => { 148 | failure("FIREBASE OTP REQUEST ERROR:" + JSON.stringify(error)); 149 | helpme(); 150 | requestOTPVonage(number); 151 | } 152 | ) 153 | } 154 | 155 | 156 | return ( 157 |
158 |
159 | { 160 | !requestedOtp ? 161 |
162 |
163 | login using your phone number 164 |
165 |
166 | setInputNumber('+' + phone)} 172 | inputClass={s.digits} 173 | dropdownClass={s.dropdown} 174 | searchClass={s.search} 175 | buttonClass={s.button} 176 | containerClass={s.cont} 177 | /> 178 | 181 |
182 |
183 | : 184 |
185 |
186 | enter the one time passcode 187 |
188 |
189 | {setInputOTP(event.target.value);}} placeholder={'000111'}> 190 | 195 |
196 |
197 | } 198 |
199 | { 200 | failed != "" ? 201 | 202 | {failed} 203 | : 204 | } 205 | { 206 | help != "" ? 207 |
208 | {help} 209 |
: 210 | } 211 |
212 |
213 |
214 |

TooFake is working for american numbers, but is unlikely to work for others. TooFake needs your help maintaining the project!

215 |

TooFake has taken a considerable amount of my time (& money) to keep alive. Any help is greatly appreciated especially as BeReal continues to beef up its security making it much harder to reverse engineer. If you are well versed in reverse engineering, please check out the github and help us keep the project working!

216 | {/*

You can login using your phone number, view bereals and post custom images.

217 |

Please report any bugs or issues on the github theres probably a bunch!

218 |

More features coming soon!

*/} 219 | {/*

*/} 220 | {/*

- There has been increased reports of login not working in the UK & other countries

*/} 221 |
222 |
223 | ) 224 | } 225 | -------------------------------------------------------------------------------- /client/pages/post/post-with-camera/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from "react"; 2 | import s from './postcamera.module.scss'; 3 | import useCheck from "@/utils/check"; 4 | import { useRouter } from "next/router"; 5 | 6 | export default function Post() { 7 | useCheck(); 8 | const router = useRouter(); 9 | 10 | const [loading, setLoading] = useState(false); 11 | const [failure, setFailure] = useState(""); 12 | const [success, setSuccess] = useState(""); 13 | 14 | const [caption, setCaption] = useState(''); 15 | const [primaryBase64, setPrimaryBase64] = useState(''); 16 | const [secondaryBase64, setSecondaryBase64] = useState(''); 17 | const [cameraActive, setCameraActive] = useState(false); 18 | const [isPrimaryCaptured, setIsPrimaryCaptured] = useState(false); 19 | const [isSecondaryCaptured, setIsSecondaryCaptured] = useState(false); 20 | const [selectedCameraId, setSelectedCameraId] = useState(null); 21 | const [currentCapture, setCurrentCapture] = useState<'primary' | 'secondary' | null>(null); 22 | 23 | const videoRef = useRef(null); 24 | const canvasRef = useRef(null); 25 | 26 | useEffect(() => { 27 | let stream: MediaStream | null = null; 28 | 29 | const getMediaStream = async () => { 30 | try { 31 | const constraints: MediaStreamConstraints = { 32 | video: { deviceId: selectedCameraId ? { exact: selectedCameraId } : undefined } 33 | }; 34 | stream = await navigator.mediaDevices.getUserMedia(constraints); 35 | if (videoRef.current) { 36 | videoRef.current.srcObject = stream; 37 | videoRef.current.play(); 38 | } 39 | } catch (err) { 40 | console.error("Error accessing the camera: ", err); 41 | } 42 | }; 43 | 44 | if (cameraActive) { 45 | getMediaStream(); 46 | } 47 | 48 | return () => { 49 | if (stream) { 50 | stream.getTracks().forEach(track => track.stop()); 51 | } 52 | }; 53 | }, [cameraActive, selectedCameraId]); 54 | 55 | useEffect(() => { 56 | const getCameraList = async () => { 57 | const devices = await navigator.mediaDevices.enumerateDevices(); 58 | const videoDevices = devices.filter(device => device.kind === 'videoinput'); 59 | if (videoDevices.length > 0) { 60 | setSelectedCameraId(videoDevices[0].deviceId); // Default to the first camera 61 | } 62 | }; 63 | 64 | getCameraList(); 65 | }, []); 66 | 67 | const captureImage = () => { 68 | if (canvasRef.current && videoRef.current) { 69 | const context = canvasRef.current.getContext('2d'); 70 | if (context) { 71 | canvasRef.current.width = 1500; 72 | canvasRef.current.height = 2000; 73 | context.drawImage(videoRef.current, 0, 0, 1500, 2000); 74 | const imageBase64 = canvasRef.current.toDataURL('image/png'); 75 | 76 | if (currentCapture === 'primary') { 77 | setPrimaryBase64(imageBase64); 78 | setIsPrimaryCaptured(true); 79 | } else if (currentCapture === 'secondary') { 80 | setSecondaryBase64(imageBase64); 81 | setIsSecondaryCaptured(true); 82 | } 83 | 84 | stopCamera(); 85 | } 86 | } 87 | }; 88 | 89 | const stopCamera = () => { 90 | const stream = videoRef.current?.srcObject as MediaStream; 91 | if (stream) { 92 | stream.getTracks().forEach(track => track.stop()); 93 | } 94 | setCameraActive(false); 95 | }; 96 | 97 | const handleCameraToggle = async () => { 98 | const devices = await navigator.mediaDevices.enumerateDevices(); 99 | const videoDevices = devices.filter(device => device.kind === 'videoinput'); 100 | if (videoDevices.length > 0) { 101 | const currentIndex = videoDevices.findIndex(device => device.deviceId === selectedCameraId); 102 | const nextIndex = (currentIndex + 1) % videoDevices.length; 103 | setSelectedCameraId(videoDevices[nextIndex].deviceId); 104 | } 105 | }; 106 | 107 | const handleCameraModalOpen = (type: 'primary' | 'secondary') => { 108 | setCurrentCapture(type); 109 | setIsPrimaryCaptured(type === 'primary' ? false : isPrimaryCaptured); 110 | setIsSecondaryCaptured(type === 'secondary' ? false : isSecondaryCaptured); 111 | setCameraActive(true); 112 | }; 113 | 114 | const handleSubmission = () => { 115 | setLoading(true); 116 | 117 | const authorization_token = localStorage.getItem("token"); 118 | 119 | fetch("/api/add/post", { 120 | method: "POST", 121 | headers: { 122 | 'Content-Type': 'application/json' 123 | }, 124 | body: JSON.stringify({ 125 | primaryb64: primaryBase64, 126 | secondaryb64: secondaryBase64, 127 | caption: caption, 128 | token: authorization_token 129 | }) 130 | }) 131 | .then(response => { 132 | if (response.ok) { 133 | setLoading(false); 134 | setSuccess("Successfully posted!"); 135 | setTimeout(() => { setSuccess(""); router.push("/feed") }, 3000); 136 | } else { 137 | throw new Error("Error: " + response.statusText); 138 | } 139 | }) 140 | .catch(error => { 141 | console.log(error); 142 | setLoading(false); 143 | setFailure(error.message); 144 | setTimeout(() => { setFailure("") }, 5000); 145 | }); 146 | }; 147 | 148 | return ( 149 |
150 | 153 |
154 |
155 | 158 | {isPrimaryCaptured && ( 159 |
160 | Back Image 161 |
162 | )} 163 |
164 |
165 | 168 | {isSecondaryCaptured && ( 169 |
170 | Front Image 171 |
172 | )} 173 |
174 |
175 | setCaption(txt.target.value)} 179 | disabled 180 | /> 181 |
Post
182 |
183 | *The photos taken here won't look perfect in the app for everyone else, but it's close.
184 |
185 | {failure &&
{failure}
} 186 | {loading &&
loading...
} 187 | {success &&
{success}
} 188 | 189 | {cameraActive && ( 190 |
191 | 192 |
193 | 194 | 195 | 196 |
197 | 198 |
199 | )} 200 |
201 | ); 202 | } 203 | -------------------------------------------------------------------------------- /client/pages/feed/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { useEffect } from 'react' 4 | import axios from 'axios' 5 | import Instant from '@/components/instant/instant'; 6 | import Instance from '@/models/instance'; 7 | import { useState } from 'react'; 8 | 9 | import { useRouter } from 'next/router' 10 | 11 | import useCheck from '@/utils/check'; 12 | 13 | import s from './feed.module.scss'; 14 | import l from '@/styles/loader.module.scss'; 15 | import Moji from '@/models/moji'; 16 | 17 | import Link from 'next/link' 18 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 19 | import { faAdd, faClose } from "@fortawesome/free-solid-svg-icons"; 20 | 21 | 22 | 23 | export default function Feed() { 24 | 25 | let router = useRouter(); 26 | if (!useCheck()) { 27 | return <> 28 | } 29 | 30 | let [instances, setInstances] = useState<{ [key: string]: Instance }>({}) 31 | let [loading, setLoading] = useState(true); 32 | let [failure, setFailure] = useState(""); 33 | 34 | useEffect(() => { 35 | 36 | setLoading(true); 37 | let token = localStorage.getItem("token"); 38 | let body = JSON.stringify({ "token": token }); 39 | 40 | /* 41 | old feed api 42 | 43 | let options = { 44 | url: "/api/feed", 45 | method: "POST", 46 | headers: { 'Content-Type': 'application/json' }, 47 | data: body, 48 | } 49 | 50 | axios.request(options).then( 51 | async (response) => { 52 | 53 | console.log("response.data") 54 | console.log(response.data); 55 | let newinstances: { [key: string]: Instance } = {}; 56 | 57 | async function createInstance(data: any) { 58 | let id = data.id; 59 | let newinstance = await Instance.create(data); 60 | newinstances[id] = newinstance; 61 | setLoading(false); 62 | } 63 | 64 | for (let i = 0; i < response.data.length; i++) { 65 | try { 66 | await createInstance(response.data[i]); 67 | setInstances({...newinstances}); 68 | setLoading(false); 69 | } catch (error) { 70 | console.log("CULDNT MAKE INSTANCE WITH DATA: ", response.data[i]) 71 | console.log(error); 72 | } 73 | 74 | } 75 | console.log("newinstances"); 76 | console.log(newinstances); 77 | setLoading(false); 78 | } 79 | ).catch( 80 | (error) => { 81 | console.log("FETCHING ERROR") 82 | console.log(error); 83 | setLoading(false); 84 | setFailure("SOMETHING WENT WRONG: " + JSON.stringify(error.response.data.error)); 85 | setTimeout(() => {setFailure("")}, 5000); 86 | } 87 | ) 88 | */ 89 | 90 | let testoptions = { 91 | url: "/api/all", 92 | method: "POST", 93 | headers: { 'Content-Type': 'application/json' }, 94 | data: body, 95 | } 96 | 97 | axios.request(testoptions).then( 98 | async (response) => { 99 | console.log("=====================================") 100 | console.log("all feed data") 101 | console.log(response.data); 102 | console.log("=====================================") 103 | 104 | let newinstances: { [key: string]: Instance } = {}; 105 | async function createInstance(data: any, usr: any) { 106 | let id = data.id; 107 | let newinstance = await Instance.moment(data, usr); 108 | newinstances[id] = newinstance; 109 | setLoading(false); 110 | } 111 | 112 | let mine = response.data.userPosts; 113 | 114 | //check if mine is undefined 115 | if (mine != undefined) { 116 | let myposts = mine.posts; 117 | for (let i = 0; i < myposts.length; i++) { 118 | let post = myposts[i]; 119 | try { 120 | await createInstance(post, mine.user); 121 | setInstances({...newinstances}); 122 | setLoading(false); 123 | } catch (error) { 124 | console.log("COULDNT MAKE INSTANCE WITH DATA: ", post) 125 | console.log(error); 126 | } 127 | } 128 | } else { 129 | console.log("I have no posts") 130 | } 131 | 132 | let friends = response.data.friendsPosts; 133 | 134 | for (let i = 0; i < friends.length; i++) { 135 | let thisuser = friends[i].user; 136 | let posts = friends[i].posts; 137 | for (let j = 0; j < posts.length; j++) { 138 | let post = posts[j]; 139 | try { 140 | await createInstance(post, thisuser); 141 | setInstances({...newinstances}); 142 | setLoading(false); 143 | } catch (error) { 144 | console.log("COULDNT MAKE INSTANCE WITH DATA: ", post) 145 | console.log(error); 146 | } 147 | } 148 | } 149 | console.log("newfriendinstances"); 150 | console.log(newinstances); 151 | setLoading(false); 152 | }).catch( 153 | (error) => { 154 | console.log("FETCHING ERROR") 155 | console.log(error); 156 | setLoading(false); 157 | setFailure("SOMETHING WENT WRONG: " + JSON.stringify(error.response.data.error)); 158 | setTimeout(() => {setFailure("")}, 5000); 159 | } 160 | ) 161 | }, []) 162 | 163 | 164 | let emoji_lookup: {[key: string]: string} = { 165 | "😍": "heartEyes", 166 | "😂": "laughing", 167 | "😲": "surprised", 168 | "😃": "happy", 169 | "👍": "up" 170 | } 171 | let [mymojis, setMymojis] = useState([]); 172 | useEffect(() => { 173 | 174 | if (localStorage.getItem("myself") == undefined) return; 175 | 176 | let my_real_mojis = JSON.parse(localStorage.getItem("myself")!).realmojis; 177 | 178 | let my_current_realmojis: Moji[] = [] 179 | for (let i = 0; i < my_real_mojis.length; i++) { 180 | 181 | let emoji = my_real_mojis[i].emoji; 182 | 183 | let my_real_moji: Moji = { 184 | id: my_real_mojis[i].id, 185 | emoji: emoji, 186 | url: my_real_mojis[i].media.url, 187 | userId: my_real_mojis[i].userId, 188 | type: emoji_lookup[emoji] 189 | } 190 | 191 | my_current_realmojis.push(my_real_moji); 192 | } 193 | 194 | setMymojis([...my_current_realmojis]); 195 | 196 | }, [loading]) 197 | 198 | 199 | let [ad, setAd] = useState(true); 200 | function closeAds() { 201 | sessionStorage.setItem("ads", "false"); 202 | setAd(false); 203 | } 204 | useEffect(() => { 205 | let ads = sessionStorage.getItem("ads"); 206 | if (ads == "false") { setAd(false);} 207 | }, []) 208 | 209 | return ( 210 |
211 | { 212 | failure ? 213 |
214 |
{failure}
215 |
something went wrong, please try refreshing the page or re-login
216 |
217 | : '' 218 | } 219 | { 220 | /* 221 | Object.keys(instances).map((key, idx) => { 222 | const elements = []; 223 | elements.push(); 224 | 225 | if ((idx + 1) % 3 === 2) { 226 | elements.push( 227 |
228 |
229 | advertisment 230 | 233 |
234 |
235 | ); 236 | } 237 | 238 | return elements; 239 | }) 240 | */ 241 | 242 | loading ?
: 243 | ( 244 | Object.keys(instances).length > 0 ? 245 | Object.keys(instances).map((key, idx) => { 246 | return ( 247 | 248 | ) 249 | }) : 250 |
251 | It's quiet here, nobody has posted anything yet. 252 |
253 | ) 254 | } 255 |
256 | ) 257 | } -------------------------------------------------------------------------------- /client/components/navbar/navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import s from './navbar.module.scss' 3 | 4 | import { useRouter } from 'next/router' 5 | import Link from 'next/link'; 6 | import { logout } from '@/utils/logout'; 7 | 8 | export default function Navbar() { 9 | 10 | let router = useRouter(); 11 | 12 | let [pfp, setPfp] = React.useState(""); 13 | let [username, setUsername] = React.useState(""); 14 | 15 | useEffect(() => { 16 | if (localStorage && JSON.parse(localStorage.getItem("myself")!)) { 17 | setUsername(JSON.parse(localStorage.getItem("myself")!).username); 18 | if (JSON.parse(localStorage.getItem("myself")!).profilePicture) { 19 | setPfp(JSON.parse(localStorage.getItem("myself")!).profilePicture.url); 20 | } 21 | } 22 | }, [router.pathname]) 23 | 24 | function getPageName() { 25 | if (router.pathname == "/feed") { 26 | return "feed"; 27 | } else if (router.pathname == "/me") { 28 | return "me"; 29 | } else if (router.pathname == "/post") { 30 | return "post"; 31 | } else if (router.pathname.startsWith("/profile")) { 32 | return "profile"; 33 | } else if (router.pathname == "/memories") { 34 | return "memories"; 35 | } else if (router.pathname == "/allMemories") { 36 | return "allMemories"; 37 | } else if (router.pathname == "/realmojis") { 38 | return "realmojis"; 39 | } else if (router.pathname == "/") { 40 | return "login"; 41 | } else if (router.pathname.startsWith("/help")) { 42 | return "help"; 43 | } 44 | } 45 | 46 | let [menu, setMenu] = React.useState(false); 47 | 48 | // if width greater than 800px, show desktop navbar 49 | // if width less than 800px, show mobile navbar 50 | 51 | // super hacky navbar but works for now 52 | return ( 53 | 197 | ) 198 | } -------------------------------------------------------------------------------- /client/pages/api/add/post.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios' 3 | /* import { File } from "formidable"; 4 | import formidable, { IncomingForm } from "formidable"; */ 5 | import Jimp from "jimp"; 6 | import fs from "fs"; 7 | import sharp from 'sharp'; 8 | import moment from 'moment'; 9 | // @ts-ignore 10 | import * as convert from 'heic-convert'; 11 | import { getAuthHeaders } from '@/utils/authHeaders'; 12 | import { PROXY } from '@/utils/constants'; 13 | 14 | export const config = { 15 | api: { 16 | bodyParser: { sizeLimit: '12mb', }, 17 | } 18 | }; 19 | 20 | /* 21 | export type FormidableParseReturn = { 22 | fields: formidable.Fields; 23 | files: formidable.Files; 24 | }; 25 | 26 | export async function parseFormAsync(req: NextApiRequest, formidableOptions?: formidable.Options): Promise { 27 | const form = formidable(formidableOptions); 28 | 29 | return await new Promise((resolve, reject) => { 30 | form.parse(req, async (err, fields, files) => { 31 | if (err) { 32 | reject(err); 33 | } 34 | 35 | resolve({ fields, files }); 36 | }); 37 | }); 38 | } 39 | */ 40 | 41 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 42 | console.log("POST REQUEST"); 43 | try { 44 | 45 | /* 46 | const { fields, files } = await parseFormAsync(req); 47 | console.log(fields, files) 48 | 49 | let caption: string = fields["caption"] as string; 50 | let authorization_token: string = fields["token"] as string; 51 | let primaryb64: string = fields["primaryb64"][0] as string; 52 | let secondaryb64: string = fields["secondaryb64"][0] as string; 53 | */ 54 | 55 | // using fetch 56 | let caption: string = req.body.caption; 57 | let authorization_token: string = req.body.token; 58 | let primaryb64: string = req.body.primaryb64; 59 | let secondaryb64: string = req.body.secondaryb64; 60 | 61 | // log the first 20 chars of the base64 string 62 | console.log("BASE64 STRINGS 40 chars"); 63 | console.log(primaryb64.substring(0, 40)); 64 | console.log(secondaryb64.substring(0, 40)); 65 | console.log('---------------------') 66 | 67 | // drop prefix of base64 string 68 | // the possible formats are png, jpeg, jpg, octet-stream 69 | // the possible data formats are image and application 70 | 71 | let isPrimaryHeic = false; 72 | if (primaryb64.startsWith("data:application/octet-stream;base64,")) { 73 | isPrimaryHeic = true; 74 | } 75 | let isSecondaryHeic = false; 76 | if (secondaryb64.startsWith("data:application/octet-stream;base64,")) { 77 | isSecondaryHeic = true; 78 | } 79 | 80 | primaryb64 = primaryb64.replace(/^data:(image|application)\/(png|webp|jpeg|jpg|octet-stream);base64,/, ""); 81 | secondaryb64 = secondaryb64.replace(/^data:(image|application)\/(png|webp|jpeg|jpg|octet-stream);base64,/, ""); 82 | /* primaryb64 = primaryb64.replace(/^data:image\/(png|jpeg|jpg|octet-stream);base64,/, ""); 83 | secondaryb64 = secondaryb64.replace(/^data:image\/(png|jpeg|jpg|octet-stream);base64,/, ""); */ 84 | 85 | // ============================================================================================ 86 | 87 | //convert base64 to buffer 88 | let primary_image_buffer = Buffer.from(primaryb64, 'base64'); 89 | let secondary_image_buffer = Buffer.from(secondaryb64, 'base64'); 90 | console.log("IMAGE BUFFERS"); 91 | console.log(primary_image_buffer); 92 | console.log('---------------------') 93 | console.log(secondary_image_buffer); 94 | console.log('=====================') 95 | 96 | // ============================================================================================ 97 | 98 | /* if (isPrimaryHeic) { 99 | console.log("CONVERTING HEIC TO JPG"); 100 | primary_image_buffer = await convert({ 101 | buffer: primary_image_buffer, // the HEIC file buffer 102 | format: 'JPEG', // output format 103 | quality: 1 // the jpeg compression quality, between 0 and 1 104 | }); 105 | } 106 | 107 | if (isSecondaryHeic) { 108 | console.log("CONVERTING HEIC TO JPG"); 109 | secondary_image_buffer = await convert({ 110 | buffer: secondary_image_buffer, // the HEIC file buffer 111 | format: 'JPEG', // output format 112 | quality: 1 // the jpeg compression quality, between 0 and 1 113 | }); 114 | } */ 115 | 116 | // ============================================================================================ 117 | 118 | let sharp_primary = await sharp(primary_image_buffer).toBuffer(); 119 | let sharp_secondary = await sharp(secondary_image_buffer).toBuffer(); 120 | 121 | const primary_mime_type = (await sharp(sharp_primary).metadata()).format; 122 | const secondary_mime_type = (await sharp(sharp_secondary).metadata()).format; 123 | 124 | console.log("SHARP IMAGES"); 125 | console.log(sharp_primary); 126 | console.log(primary_mime_type); 127 | console.log('---------------------') 128 | console.log(sharp_secondary); 129 | console.log(secondary_mime_type); 130 | console.log('=====================') 131 | 132 | if (primary_mime_type != 'webp') { 133 | sharp_primary = await sharp(sharp_primary).toFormat('webp').toBuffer(); 134 | } 135 | if (secondary_mime_type != 'webp') { 136 | sharp_secondary = await sharp(sharp_secondary).toFormat('webp').toBuffer(); 137 | } 138 | 139 | /* console.log("SHARP IMAGES AFTER CONVERSION"); 140 | console.log(sharp_primary); 141 | console.log('---------------------') 142 | console.log(sharp_secondary); 143 | console.log('=====================') */ 144 | 145 | // ============================================================================================ 146 | // upload url 147 | 148 | let upload_options = { 149 | url: `${PROXY}https://mobile.bereal.com/api/content/posts/upload-url?mimeType=image/webp`, 150 | method: "GET", 151 | headers: getAuthHeaders(authorization_token), 152 | } 153 | 154 | let upload_res = await axios.request(upload_options) 155 | 156 | console.log("upload result"); 157 | console.log(upload_res.data); 158 | console.log('---------------------') 159 | 160 | let primary_res = upload_res.data.data[0] 161 | let secondary_res = upload_res.data.data[1] 162 | 163 | let primary_headers = primary_res.headers; 164 | let primary_url = primary_res.url; 165 | let primary_path = primary_res.path; 166 | let primary_bucket = primary_res.bucket; 167 | Object.assign(primary_headers, getAuthHeaders(authorization_token)) 168 | 169 | let secondary_headers = secondary_res.headers; 170 | let secondary_url = secondary_res.url; 171 | let secondary_path = secondary_res.path; 172 | let secondary_bucket = secondary_res.bucket; 173 | Object.assign(secondary_headers, getAuthHeaders(authorization_token)) 174 | 175 | // ============================================================================================ 176 | 177 | let put_primary_options = { 178 | url: primary_url, 179 | method: "PUT", 180 | headers: primary_headers, 181 | /* data: primary, */ 182 | data: sharp_primary, 183 | } 184 | let put_primary_res = await axios.request(put_primary_options) 185 | console.log("put primary result"); 186 | console.log(put_primary_res.status); 187 | console.log('---------------------') 188 | 189 | let put_secondary_options = { 190 | url: secondary_url, 191 | method: "PUT", 192 | headers: secondary_headers, 193 | /* data: secondary, */ 194 | data: sharp_secondary, 195 | } 196 | let put_secondary_res = await axios.request(put_secondary_options) 197 | console.log("put secondary result"); 198 | console.log(put_secondary_res.status); 199 | console.log('---------------------') 200 | 201 | // ============================================================================================ 202 | 203 | let taken_at = moment().utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); 204 | let post_data: any = { 205 | "isLate": false, 206 | "retakeCounter": 0, 207 | takenAt: taken_at, 208 | /* content: caption.toString(), */ // might not be working 209 | visibility: ["friends"], 210 | backCamera: { 211 | bucket: primary_bucket, 212 | height: 1500, 213 | width: 2000, 214 | path: primary_path, 215 | }, 216 | frontCamera: { 217 | bucket: secondary_bucket, 218 | height: 1500, 219 | width: 2000, 220 | path: secondary_path, 221 | }, 222 | }; 223 | let post_headers = { 224 | "content-type": "application/json", 225 | 'bereal-app-version-code': '14549', 226 | "bereal-os-version": "14.7.1", 227 | "accept-language": "en-US;q=1.0", 228 | "bereal-app-language": "en-US", 229 | "user-agent": "BeReal/0.28.2 (AlexisBarreyat.BeReal; build:8425; iOS 14.7.1) 1.0.0/BRApiKit", 230 | "bereal-device-language": "en", 231 | ...getAuthHeaders(authorization_token) 232 | } 233 | console.log("post data"); 234 | console.log(post_data); 235 | console.log(post_headers) 236 | console.log('---------------------') 237 | 238 | let post_response = await axios.request({ 239 | method: 'POST', 240 | url: 'https://mobile.bereal.com/api/content/posts', 241 | data: JSON.stringify(post_data), 242 | headers: post_headers, 243 | }) 244 | 245 | console.log("post response"); 246 | console.log(post_response); 247 | console.log('---------------------') 248 | 249 | res.status(200).json(upload_res.data); 250 | 251 | } catch (error: any) { 252 | console.log("FAILURE") 253 | console.log(error); 254 | console.log('---------------------') 255 | 256 | let error_message; 257 | 258 | if (error.response) { 259 | error_message = JSON.stringify(error.response.data); 260 | console.log(error.response.data); 261 | } else { 262 | error_message = error.toString(); 263 | } 264 | console.log(error_message); 265 | res.status(400).json({ error: error_message }); 266 | } 267 | } -------------------------------------------------------------------------------- /client/components/instant/instant.module.scss: -------------------------------------------------------------------------------- 1 | /* @use "@/styles/palette.scss" as p; */ 2 | @use "../../styles/common.scss" as c; 3 | 4 | .instant { 5 | border-radius: 8px; 6 | width: 300px; 7 | min-width: 300px; 8 | border: 1px solid white; 9 | box-shadow: 0 0 10px 1px #6a6565; 10 | margin-right: 30px; 11 | height: min-content; 12 | 13 | a { 14 | text-decoration: none; 15 | color: inherit; 16 | } 17 | 18 | .top { 19 | /* height: 50px; */ 20 | width: 100%; 21 | display: flex; 22 | padding: 12px 10px; 23 | 24 | .pfp { 25 | width: 50px; 26 | @include c.center(); 27 | 28 | img { 29 | height: 30px; 30 | width: 30px; 31 | border-radius: 100%; 32 | object-fit: cover; 33 | background-color: white; 34 | } 35 | 36 | .letter { 37 | height: 30px; 38 | width: 30px; 39 | border-radius: 100%; 40 | object-fit: cover; 41 | background-color: white; 42 | @include c.center(); 43 | font-size: 20px; 44 | color: black; 45 | } 46 | } 47 | 48 | .details { 49 | flex: 1; 50 | display: flex; 51 | flex-direction: column; 52 | justify-content: center; 53 | 54 | .username { 55 | font-size: 15px; 56 | font-weight: 600; 57 | margin-bottom: 4px; 58 | } 59 | 60 | .location { 61 | font-size: 10px; 62 | text-decoration: underline; 63 | color: rgb(158, 158, 158); 64 | } 65 | 66 | .timeposted { 67 | font-size: 10px; 68 | color: rgb(158, 158, 158); 69 | } 70 | } 71 | 72 | .trash { 73 | width: 30px; 74 | @include c.center(); 75 | cursor: pointer; 76 | } 77 | 78 | .btsView { 79 | width: 30px; 80 | @include c.center(); 81 | cursor: pointer; 82 | } 83 | } 84 | 85 | .content { 86 | position: relative; 87 | /* height: 400px; */ 88 | width: 100%; 89 | 90 | .primary { 91 | position: relative; 92 | width: 100%; 93 | border-radius: 8px; 94 | z-index: 100; 95 | } 96 | 97 | .bounds { 98 | width: 100%; 99 | position: absolute; 100 | top: 0; 101 | height: calc(100% - 60px); 102 | left: 0; 103 | padding: 10px; 104 | 105 | .secondary { 106 | border-radius: 8px; 107 | width: 100px; 108 | position: absolute; 109 | cursor: pointer; 110 | z-index: 101; 111 | } 112 | } 113 | 114 | .realmojis { 115 | position: absolute; 116 | left: 0; 117 | bottom: 0; 118 | width: 100%; 119 | /* height: 15%; */ 120 | height: 60px; 121 | padding: 5px 10px; 122 | padding-bottom: 4px; 123 | display: flex; 124 | z-index: 200; 125 | 126 | .carousel { 127 | width: 100%; 128 | height: 100%; 129 | 130 | .realmoji { 131 | position: relative; 132 | width: 48px; 133 | height: 50px; 134 | 135 | /* margin-right: 10px; */ 136 | .moji { 137 | position: absolute; 138 | left: 0px; 139 | top: -2px; 140 | z-index: 10; 141 | font-size: 18px; 142 | } 143 | 144 | img { 145 | border: 1px solid white; 146 | position: absolute; 147 | right: 0; 148 | top: 4px; 149 | width: 40px; 150 | height: 40px; 151 | border-radius: 100%; 152 | object-fit: cover; 153 | background-color: white; 154 | } 155 | } 156 | } 157 | 158 | .nextlast { 159 | cursor: pointer; 160 | width: 21px; 161 | height: 100%; 162 | color: white; 163 | @include c.center(); 164 | 165 | .add { 166 | background-color: rgba(0, 0, 0, 0.155); 167 | height: 100%; 168 | width: 100%; 169 | @include c.center(); 170 | } 171 | } 172 | 173 | .addmojis { 174 | position: relative; 175 | width: 48px; 176 | height: 50px; 177 | margin-right: 4px; 178 | cursor: pointer; 179 | 180 | .moji { 181 | position: absolute; 182 | left: 2px; 183 | font-size: 18px; 184 | z-index: 10; 185 | } 186 | 187 | img { 188 | border: 1px solid white; 189 | position: absolute; 190 | right: 0; 191 | top: 4px; 192 | width: 40px; 193 | height: 40px; 194 | border-radius: 100%; 195 | object-fit: cover; 196 | background-color: white; 197 | } 198 | 199 | .add { 200 | cursor: pointer; 201 | position: absolute; 202 | right: 0; 203 | top: 4px; 204 | width: 40px; 205 | height: 40px; 206 | border-radius: 100%; 207 | object-fit: cover; 208 | background-color: rgba(0, 0, 0, 0.375); 209 | border: 2px solid white; 210 | color: #fff; 211 | @include c.center(); 212 | font-size: 20px; 213 | } 214 | 215 | } 216 | } 217 | } 218 | 219 | .caption { 220 | width: 100%; 221 | padding: 7px 10px; 222 | font-size: 12px; 223 | 224 | span { 225 | color: rgb(158, 158, 158); 226 | } 227 | } 228 | 229 | .addcomment { 230 | width: 100%; 231 | padding: 10px; 232 | display: flex; 233 | justify-content: space-between; 234 | 235 | input { 236 | height: 30px; 237 | width: 77%; 238 | border-radius: 6px; 239 | border: 2px solid white; 240 | background-color: transparent; 241 | padding: 0px 10px; 242 | font-size: 14px; 243 | color: white; 244 | outline: none; 245 | } 246 | 247 | button { 248 | height: 30px; 249 | width: 20%; 250 | border-radius: 6px; 251 | border: 2px solid white; 252 | background-color: white; 253 | padding: 0px 10px; 254 | font-size: 14px; 255 | color: black; 256 | cursor: pointer; 257 | } 258 | 259 | .addloading { 260 | height: 30px; 261 | width: 20%; 262 | border-radius: 6px; 263 | border: 2px solid white; 264 | background-color: white; 265 | padding: 0px 10px; 266 | font-size: 14px; 267 | color: black; 268 | @include c.center(); 269 | } 270 | 271 | } 272 | 273 | .comments { 274 | padding: 0px 10px; 275 | padding-bottom: 4px; 276 | position: relative; 277 | 278 | .download { 279 | position: absolute; 280 | top: 0; 281 | right: 10px; 282 | width: 30px; 283 | @include c.center(); 284 | cursor: pointer; 285 | } 286 | 287 | .expand { 288 | font-size: 14px; 289 | height: 25px; 290 | 291 | .click { 292 | cursor: pointer; 293 | } 294 | } 295 | 296 | .holder { 297 | height: 25px; 298 | font-size: 14px; 299 | color: rgb(158, 158, 158); 300 | } 301 | 302 | .comment { 303 | display: flex; 304 | align-items: center; 305 | margin-top: 12px; 306 | 307 | &:last-child { 308 | margin-bottom: 12px; 309 | } 310 | 311 | img { 312 | height: 30px; 313 | width: 30px; 314 | border-radius: 100%; 315 | object-fit: cover; 316 | background-color: white; 317 | } 318 | 319 | .discourse { 320 | display: flex; 321 | flex-direction: column; 322 | margin-left: 10px; 323 | 324 | .username { 325 | font-size: 10px; 326 | font-weight: 500; 327 | margin-bottom: 2px; 328 | color: rgb(158, 158, 158); 329 | } 330 | 331 | .convo { 332 | font-size: 14px; 333 | } 334 | } 335 | } 336 | } 337 | 338 | 339 | @media (max-width: 600px) { 340 | margin-right: 0px; 341 | margin-bottom: 12px; 342 | } 343 | 344 | .musicContainer { 345 | display: flex; 346 | align-items: center; 347 | gap: 15px; 348 | padding: 4px; 349 | border-radius: 5px; 350 | background-color: #0e1b29; 351 | cursor: pointer; 352 | position: relative; 353 | 354 | &:hover::after { 355 | content: attr(title); 356 | /* Use the title attribute for the tooltip */ 357 | position: absolute; 358 | bottom: 100%; 359 | left: 50%; 360 | transform: translateX(-50%); 361 | background-color: #333; 362 | color: #fff; 363 | padding: 5px; 364 | border-radius: 5px; 365 | white-space: nowrap; 366 | /* Prevent text wrapping */ 367 | font-size: 12px; 368 | z-index: 10; 369 | opacity: 0.9; 370 | } 371 | } 372 | 373 | 374 | .musicCoverArt { 375 | width: 40px; 376 | height: 40px; 377 | border-radius: 10%; 378 | object-fit: cover; 379 | } 380 | 381 | .musicDetails { 382 | display: flex; 383 | flex-direction: column; 384 | } 385 | 386 | .musicTitle { 387 | margin: 0; 388 | font-size: 10px; 389 | font-weight: bold; 390 | } 391 | 392 | .musicArtist { 393 | margin: 0; 394 | font-size: 9px; 395 | color: #555; 396 | } 397 | 398 | 399 | .noMusicTitle { 400 | font-size: 10px; 401 | color: rgb(158, 158, 158); 402 | } 403 | } -------------------------------------------------------------------------------- /client/pages/allMemories/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState } from 'react' 3 | import { useEffect } from 'react' 4 | import axios from 'axios' 5 | import useCheck from '@/utils/check'; 6 | import s from './allMemories.module.scss' 7 | import l from '@/styles/loader.module.scss'; 8 | import MemoireV2 from '@/components/memoire/memoireV2'; 9 | import JSZip from 'jszip'; 10 | import MemoryV2 from '../../models/memoryV2'; 11 | import FileSaver from 'file-saver'; 12 | 13 | // Made memories global for downloading (kinda ugly) 14 | let memories: MemoryV2[] = []; 15 | 16 | export default function MemoriesV2() { 17 | 18 | useCheck(); 19 | 20 | let [_memories, setMemories] = useState([]); 21 | let [loading, setLoading] = useState(true); 22 | 23 | useEffect(() => { 24 | 25 | let token = localStorage.getItem("token"); 26 | let body = JSON.stringify({ "token": token }); 27 | let options1 = { 28 | url: "/api/memoriesV1", 29 | method: "POST", 30 | headers: { 'Content-Type': 'application/json' }, 31 | data: body, 32 | } 33 | 34 | axios.request(options1).then( 35 | async (response) => { 36 | let data = response.data.data; 37 | 38 | function createMemory(data: any) { 39 | let memory = MemoryV2.create(data); 40 | memories.push(memory); 41 | return memory; 42 | } 43 | 44 | let counter = 0; 45 | for (let i = 0; i < data.length; i++) { 46 | try { 47 | axios.request({ 48 | url: "/api/memoriesV2?momentId=" + data[i].momentId, 49 | method: "POST", 50 | headers: { 'Content-Type': 'application/json' }, 51 | data: body, 52 | }).then( 53 | async (res) => { 54 | const posts: any[] = res.data.posts 55 | posts.forEach((post: any) => { 56 | createMemory(post) 57 | }) 58 | 59 | counter++ 60 | if (counter >= data.length) { 61 | memories.sort((a, b) => { return a.date > b.date ? -1 : 1 }); 62 | setMemories(memories); 63 | setLoading(false); 64 | } 65 | } 66 | ).catch((error) => { console.log(error); }) 67 | } catch (error) { 68 | console.log("COULDN'T MAKE MEMORY WITH DATA: ", data[i]) 69 | console.log(error); 70 | } 71 | } 72 | } 73 | ).catch((error) => { console.log(error); }) 74 | }, []); 75 | 76 | 77 | 78 | return ( 79 |
80 |
81 | { 82 | loading ?
: 83 | _memories.map((memory, index) => { 84 | return ( 85 | 86 | ) 87 | }) 88 | } 89 |
90 | 91 | 92 |
93 | 94 | 95 |
96 |

97 |
98 | 99 |
100 |

101 |
102 | 103 |
104 |
105 | 106 | 107 |
108 |
109 | 110 | 111 |
112 |
113 | 114 |
115 | 116 |
117 |
118 |
119 | 120 | 121 | ) 122 | 123 | } 124 | 125 | async function downloadMemories() { 126 | 127 | // Note: this is JS code not TS which is why it's throwing an error but runs fine 128 | 129 | // @ts-ignore: Object is possibly 'null'. 130 | let separateImages = document.getElementById("separate").checked; 131 | // @ts-ignore: Object is possibly 'null'. 132 | let mergedImage = document.getElementById("merged").checked; 133 | // @ts-ignore: Object is possibly 'null'. 134 | let status = document.getElementById("downloadStatus"); 135 | // @ts-ignore: Object is possibly 'null'. 136 | let error = document.getElementById("error"); 137 | // @ts-ignore: Object is possibly 'null'. 138 | let downloadButton = document.getElementById("download"); 139 | 140 | // Reset text 141 | // @ts-ignore: Object is possibly 'null'. 142 | status.textContent = ""; 143 | // @ts-ignore: Object is possibly 'null'. 144 | error.textContent = ""; 145 | 146 | // Don't do anything if no boxes are checked 147 | if (!(separateImages || mergedImage)) { 148 | 149 | // @ts-ignore: Object is possibly 'null'. 150 | status.textContent = "No export option selected."; 151 | return; 152 | } 153 | 154 | 155 | // Disable download button 156 | // @ts-ignore: Object is possibly 'null'. 157 | downloadButton.disabled = true; 158 | 159 | const batchSize = 100; 160 | const batches: MemoryV2[][] = []; 161 | for (let i = 0; i < memories.length; i += batchSize) { 162 | batches.push(memories.slice(i, i + batchSize)); 163 | } 164 | 165 | let superZip = new JSZip(); 166 | let superCounter = 0; 167 | for (let j = 0; j < batches.length; j++) { 168 | let batch = batches[j]; 169 | let zip = new JSZip(); 170 | console.log("Batch ", j); 171 | 172 | // Loop through each memory 173 | let counter = 0; 174 | for (let i = 0; i < batch.length; i++) { 175 | let memory = batch[i]; 176 | 177 | // Update memory status 178 | // @ts-ignore: Object is possibly 'null'. 179 | status.textContent = `Zipping, ${(((j * batchSize + i + 1) / (memories.length)) * 100).toFixed(1)}% (Memory ${j * batchSize + i + 1}/${(memories.length)})` 180 | 181 | 182 | // Date strings for folder/file names 183 | let memoryDate = `${memory.date.replaceAll("-", "")}-${memory.time.replaceAll(":", "")}` 184 | 185 | // An error can happen here, InvalidStateException 186 | // Caused by the primary/secondary image fetch being corrupt, 187 | // but only happens rarely on specific memories 188 | try { 189 | // REPLACE WITH PROPER PROXY SETUP! 190 | // Fetch image data 191 | let primary = await fetch("https://toofake-cors-proxy-4fefd1186131.herokuapp.com/" + memory.primary) 192 | .then((result) => result.blob()) 193 | 194 | let secondary = await fetch("https://toofake-cors-proxy-4fefd1186131.herokuapp.com/" + memory.secondary) 195 | .then((result) => result.blob()) 196 | 197 | 198 | // Create zip w/ image, adapted from https://stackoverflow.com/a/49836948/21809626 199 | // Zip (primary + secondary separate) 200 | if (separateImages) { 201 | zip.file(`${memoryDate} - primary.png`, primary) 202 | zip.file(`${memoryDate} - secondary.png`, secondary) 203 | } 204 | 205 | // Merging images for combined view 206 | // (Must have canvas declaration here to be accessed by toBlob()) 207 | if (mergedImage) { 208 | var canvas = document.getElementById("myCanvas") as HTMLCanvasElement; 209 | 210 | let primaryImage = await createImageBitmap(primary); 211 | let secondaryImage = await createImageBitmap(secondary); 212 | 213 | canvas.width = primaryImage.width; 214 | canvas.height = primaryImage.height; 215 | 216 | var ctx = canvas.getContext("2d"); 217 | 218 | // Check if ctx is null for dealing with TS error (not necessary) 219 | // Bereal-style combined image 220 | // NOTE: secondary image is bugged for custom-uploaded images through the site, 221 | // that aren't phone-sized 222 | if (ctx) { 223 | ctx.drawImage(primaryImage, 0, 0) 224 | 225 | // Rounded secondary image, adapted from https://stackoverflow.com/a/19593950/21809626 226 | 227 | // Values relative to image size 228 | let width = secondaryImage.width * 0.3; 229 | let height = secondaryImage.height * 0.3; 230 | let x = primaryImage.width * 0.03; 231 | let y = primaryImage.height * 0.03; 232 | let radius = 70; 233 | 234 | 235 | ctx.beginPath(); 236 | ctx.moveTo(x + radius, y); 237 | ctx.lineTo(x + width - radius, y); 238 | ctx.quadraticCurveTo(x + width, y, x + width, y + radius); 239 | ctx.lineTo(x + width, y + height - radius); 240 | ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); 241 | ctx.lineTo(x + radius, y + height); 242 | ctx.quadraticCurveTo(x, y + height, x, y + height - radius); 243 | ctx.lineTo(x, y + radius); 244 | ctx.quadraticCurveTo(x, y, x + radius, y); 245 | 246 | ctx.closePath(); 247 | 248 | ctx.lineWidth = 20; 249 | ctx.stroke(); 250 | ctx.clip() 251 | 252 | ctx.drawImage(secondaryImage, x, y, width, height) 253 | } 254 | 255 | canvas.toBlob(async blob => { 256 | if (blob) { 257 | zip.file(`${memoryDate}.png`, blob) 258 | console.log(`Zipped ${j}.${i}`) 259 | } 260 | 261 | counter++ 262 | if (counter >= batch.length) { 263 | zip.generateAsync({ type: 'blob' }).then(function (content: any) { 264 | console.log(`Generated zip ${j}`) 265 | superZip.file(`batch-${j}.zip`, content) 266 | 267 | superCounter++ 268 | if (superCounter >= 3) { 269 | superZip.generateAsync({ type: 'blob' }).then(function (x: any) { 270 | console.log(`Super zipping`) 271 | FileSaver.saveAs(x, `bereal-export-${new Date().toLocaleString("en-us", { 272 | year: "2-digit", month: "2-digit", day: "2-digit" 273 | }).replace(/\//g, '-')}.zip`); 274 | 275 | // Reset status 276 | // @ts-ignore: Object is possibly 'null'. 277 | status.textContent = "Zip will download shortly..."; 278 | 279 | // Enable download button 280 | // @ts-ignore: Object is possibly 'null'. 281 | downloadButton.disabled = false; 282 | }) 283 | } 284 | }) 285 | } 286 | }) 287 | } 288 | } catch (e) { 289 | // @ts-ignore: Object is possibly 'null'. 290 | error.textContent = "Errors found, check console." 291 | console.log(`ERROR: Memory #${i} on ${memoryDate} could not be zipped:\n${e}`); 292 | } 293 | } 294 | } 295 | } 296 | --------------------------------------------------------------------------------