├── .envrc ├── .npmrc ├── .env.example ├── src ├── app.css ├── lib │ └── index.js ├── routes │ ├── +layout.svelte │ ├── +page.js │ ├── +page.svelte │ └── llama │ │ └── +server.js └── app.html ├── .prettierignore ├── static └── favicon.png ├── postcss.config.js ├── shell.nix ├── tailwind.config.js ├── .prettierrc ├── vite.config.js ├── .gitignore ├── database.sql ├── README.md ├── svelte.config.js ├── package.json └── wrangler.toml /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY="" 2 | GITHUB_API_KEY="" 3 | TELEGRAM_API_KEY="" -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codenoid/github-roast/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: pkgs.mkShell { 2 | nativeBuildInputs = [ 3 | pkgs.nodejs 4 | pkgs.gh 5 | ]; 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/+page.js: -------------------------------------------------------------------------------- 1 | // since there's no dynamic data here, we can prerender 2 | // it so that it gets served as a static asset in production 3 | export const prerender = true; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [] 8 | }; 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | import lokalTunnel from 'lokal-vite-plugin'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | sveltekit(), 8 | lokalTunnel({ 9 | tunnelName: 'GitHub Roast', 10 | lanAddress: 'github-roast.local' 11 | }) 12 | ] 13 | }); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | 23 | # wrangler files 24 | .wrangler 25 | .dev.vars 26 | -------------------------------------------------------------------------------- /database.sql: -------------------------------------------------------------------------------- 1 | PRAGMA defer_foreign_keys=TRUE; 2 | CREATE TABLE roasts ( 3 | id integer PRIMARY KEY AUTOINCREMENT, 4 | gh_username text NOT NULL, 5 | response text NOT NULL, 6 | created_at text NOT NULL, 7 | country text NOT NULL, 8 | language text NULL 9 | ); 10 | DELETE FROM sqlite_sequence; 11 | CREATE INDEX gh_username_idx ON roasts (gh_username); 12 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 | %sveltekit.body% 11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Roast 2 | 3 | SvelteKit, Cloudflare Pages, Cloudflare D1, OpenAI gpt-4o-mini 4 | 5 | ## Setup 6 | 7 | 1. Create a new Cloudflare D1 database 8 | 2. `npx wrangler d1 import --remote ./database.sql` 9 | 3. `npx wrangler d1 import --local ./database.sql` 10 | 4. npm run dev 11 | 12 | ## Deployment 13 | 14 | 1. on Cloudflare Pages, deploy new app 15 | 2. Framework Preset -> SvelteKit 16 | 3. Use Connect Git instead of manual upload 17 | 4. the first deployment will fail, you need to update production ENV with encrypted value (this is CF Pages bug) -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-cloudflare'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | kit: { 7 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 8 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 9 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 10 | adapter: adapter() 11 | }, 12 | preprocess: vitePreprocess() 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spicy-github-roast", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "npm run build && wrangler pages dev", 9 | "lint": "prettier --check . && eslint .", 10 | "format": "prettier --write .", 11 | "deploy": "npm run build && wrangler pages deploy" 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/adapter-auto": "^3.0.0", 15 | "@sveltejs/adapter-cloudflare": "^4.7.0", 16 | "@sveltejs/kit": "^2.0.0", 17 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 18 | "@types/eslint": "^9.6.0", 19 | "autoprefixer": "^10.4.20", 20 | "globals": "^15.0.0", 21 | "postcss": "^8.4.40", 22 | "prettier": "^3.1.1", 23 | "prettier-plugin-svelte": "^3.1.2", 24 | "svelte": "^4.2.7", 25 | "tailwindcss": "^3.4.7", 26 | "vite": "^5.0.3", 27 | "wrangler": "^3.68.0" 28 | }, 29 | "type": "module", 30 | "dependencies": { 31 | "lokal-vite-plugin": "^0.0.1", 32 | "openai": "^4.54.0", 33 | "svelte-markdown": "^0.4.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | #:schema node_modules/wrangler/config-schema.json 2 | name = "githubroast" 3 | compatibility_date = "2024-07-29" 4 | pages_build_output_dir = ".svelte-kit/cloudflare" 5 | 6 | # Automatically place your workloads in an optimal location to minimize latency. 7 | # If you are running back-end logic in a Pages Function, running it closer to your back-end infrastructure 8 | # rather than the end user may result in better performance. 9 | # Docs: https://developers.cloudflare.com/pages/functions/smart-placement/#smart-placement 10 | # [placement] 11 | # mode = "smart" 12 | 13 | # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) 14 | # Docs: 15 | # - https://developers.cloudflare.com/pages/functions/bindings/#environment-variables 16 | # Note: Use secrets to store sensitive data. 17 | # - https://developers.cloudflare.com/pages/functions/bindings/#secrets 18 | # [vars] 19 | # MY_VARIABLE = "production_value" 20 | 21 | # Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network 22 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#workers-ai 23 | # [ai] 24 | # binding = "AI" 25 | 26 | # Bind a D1 database. D1 is Cloudflare’s native serverless SQL database. 27 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#d1-databases 28 | [[d1_databases]] 29 | binding = "DB" 30 | database_name = "githubroast" 31 | database_id = "e43f5850-f45d-4d01-a3fb-1c1f0b3da4bb" 32 | 33 | # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. 34 | # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. 35 | # Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects 36 | # [[durable_objects.bindings]] 37 | # name = "MY_DURABLE_OBJECT" 38 | # class_name = "MyDurableObject" 39 | # script_name = 'my-durable-object' 40 | 41 | # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. 42 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#kv-namespaces 43 | # [[kv_namespaces]] 44 | # binding = "MY_KV_NAMESPACE" 45 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 46 | 47 | # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. 48 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#queue-producers 49 | # [[queues.producers]] 50 | # binding = "MY_QUEUE" 51 | # queue = "my-queue" 52 | 53 | # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. 54 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#r2-buckets 55 | # [[r2_buckets]] 56 | # binding = "MY_BUCKET" 57 | # bucket_name = "my-bucket" 58 | 59 | # Bind another Worker service. Use this binding to call another Worker without network overhead. 60 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#service-bindings 61 | # [[services]] 62 | # binding = "MY_SERVICE" 63 | # service = "my-service" 64 | 65 | # To use different bindings for preview and production environments, follow the examples below. 66 | # When using environment-specific overrides for bindings, ALL bindings must be specified on a per-environment basis. 67 | # Docs: https://developers.cloudflare.com/pages/functions/wrangler-configuration#environment-specific-overrides 68 | 69 | ######## PREVIEW environment config ######## 70 | 71 | # [env.preview.vars] 72 | # API_KEY = "xyz789" 73 | 74 | # [[env.preview.kv_namespaces]] 75 | # binding = "MY_KV_NAMESPACE" 76 | # id = "" 77 | 78 | ######## PRODUCTION environment config ######## 79 | 80 | # [env.production.vars] 81 | # API_KEY = "abc123" 82 | 83 | # [[env.production.kv_namespaces]] 84 | # binding = "MY_KV_NAMESPACE" 85 | # id = "" 86 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 65 | 66 | 67 | GitHub Profile Roast 🔥🔥🔥 68 | 69 | 70 | 71 | GitHub Roaster 72 | 73 | 74 | 82 | 83 | 84 | 85 | 90 | {#each languages as language} 91 | {language.label} 92 | {/each} 93 | 94 | 95 | 96 | 101 | {loading ? 'Roasting...' : 'Roast This GitHub!'} 102 | 103 | 104 | {#if roast && mounted} 105 | 106 | 109 | 110 | 111 | {/if} 112 | 113 | 114 | © 2024 github-roast.pages.dev 115 | 116 | Poke Admin if something 117 | goes wrong 118 | 119 | 120 | Source code on GitHub 121 | 122 | 123 | Permanently opt out and delete all roasts related to your GitHub account. 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/routes/llama/+server.js: -------------------------------------------------------------------------------- 1 | import { OPENAI_API_KEY, GITHUB_API_KEY } from '$env/static/private'; 2 | import OpenAI from 'openai'; 3 | import { json } from '@sveltejs/kit'; 4 | 5 | const client = new OpenAI({ 6 | apiKey: OPENAI_API_KEY 7 | }); 8 | 9 | let headers = { 10 | Accept: 'application/json', 11 | 'Content-Type': 'application/json', 12 | 'User-Agent': 'github-roast.pages.dev' 13 | }; 14 | 15 | const validLanguages = [ 16 | 'english', 17 | 'indonesian', 18 | 'indian', 19 | 'chinese', 20 | 'japanese', 21 | 'korean', 22 | 'french', 23 | 'polish', 24 | 'vietnamese', 25 | 'arabic', 26 | 'traditionalChinese' 27 | ]; 28 | 29 | export async function POST({ request, platform }) { 30 | let answerdebug = ''; 31 | const { username, language } = await request.json(); 32 | 33 | if (!validLanguages.includes(language)) { 34 | return json( 35 | { error: 'invalid language specified, please pass a valid language.' }, 36 | { status: 400 } 37 | ); 38 | } 39 | 40 | const history = await platform.env.DB.prepare( 41 | 'SELECT * FROM roasts WHERE gh_username = ? AND language = ?' 42 | ) 43 | .bind(username, language) 44 | .first(); 45 | if (history) { 46 | return json({ roast: history.response, username, language }); 47 | } 48 | 49 | if (GITHUB_API_KEY) { 50 | headers['Authorization'] = `token ${GITHUB_API_KEY}`; 51 | } 52 | 53 | var profileResponse = { status: 403 }; 54 | var useToken = false; 55 | 56 | // Check if the token is not rate-limited 57 | try { 58 | let response = await fetch(`https://api.github.com/users/${username}`, { 59 | headers: headers 60 | }); 61 | profileResponse = await response.json(); 62 | if (response.ok) { 63 | useToken = true; 64 | } else { 65 | return json({ error: 'Our roast machine is in trouble' }, { status: 500 }); 66 | } 67 | } catch (err) { 68 | console.log(err); 69 | let response = await fetch(`https://api.github.com/users/${username}`, { headers }); 70 | profileResponse = await response.json(); 71 | } 72 | 73 | // If token is rate-limited, fall back to no token 74 | if (!useToken) { 75 | delete headers['Authorization']; 76 | } 77 | 78 | let response = await fetch(`https://api.github.com/users/${username}/repos?sort=updated`, { 79 | headers: headers 80 | }); 81 | const repoResponse = await response.json(); 82 | 83 | let readmeResponse; 84 | try { 85 | response = await fetch( 86 | `https://raw.githubusercontent.com/${username}/${username}/main/README.md`, 87 | { headers: headers } 88 | ); 89 | if (response.ok) { 90 | readmeResponse = await response.text(); 91 | } 92 | } catch (error) { 93 | console.log(error); 94 | try { 95 | response = await fetch( 96 | `https://raw.githubusercontent.com/${username}/${username}/master/README.md`, 97 | { headers: headers } 98 | ); 99 | // answerdebug += (await response.text()) + ' 4\n'; 100 | if (response.ok) { 101 | readmeResponse = await response.text(); 102 | } 103 | } catch (error) { 104 | console.log(error); 105 | console.log('failed to get readme'); 106 | readmeResponse = ''; 107 | } 108 | } 109 | 110 | // https://github.com/bagusindrayana/roastgithub-api/blob/master/index.js 111 | const datas = { 112 | name: profileResponse.name, 113 | bio: profileResponse.bio, 114 | company: profileResponse.company, 115 | location: profileResponse.location, 116 | followers: profileResponse.followers, 117 | following: profileResponse.following, 118 | public_repos: profileResponse.public_repos, 119 | profile_readme: readmeResponse, 120 | last_15_repositories: repoResponse 121 | .map((repo) => ({ 122 | name: repo.name, 123 | description: repo.description, 124 | language: repo.language, 125 | stargazers_count: repo.stargazers_count, 126 | open_issues_count: repo.open_issues_count, 127 | license: repo.license, 128 | fork: repo.fork 129 | })) 130 | .slice(0, 15) 131 | }; 132 | 133 | let prompt = `give a short and harsh roasting for the following github profile: ${username}. Here are the details: "${JSON.stringify(datas)}"`; 134 | switch (language) { 135 | case 'indonesian': 136 | prompt = `gunakan bahasa indonesia yang normal seperti manusia gaul, berikan roasting singkat dengan kejam dan menyindir dalam bahasa gaul untuk profile github berikut : ${username}. Berikut detailnya: "${JSON.stringify(datas)}"`; 137 | break; 138 | case 'indian': 139 | prompt = `इस गिटहब प्रोफाइल के लिए एक क्रूर और व्यंग्यात्मक रोस्टिंग गली भाषा में दें: ${username}। विवरण इस प्रकार है: "${JSON.stringify(datas)}"`; 140 | break; 141 | case 'chinese': 142 | prompt = `用中文俚语对以下GitHub个人资料进行短暂而残酷的讽刺:${username}。以下是详细信息: "${JSON.stringify(datas)}"`; 143 | break; 144 | case 'japanese': 145 | prompt = `以下のGitHubプロフィールに対して残酷で皮肉な短いローストをギャル語でしてください: ${username}。詳細は次の通りです: "${JSON.stringify(datas)}"`; 146 | break; 147 | case 'korean': 148 | prompt = `다음 GitHub 프로필에 대해 잔인하고 비꼬는 짧은 로스팅을 속어로 해주세요: ${username}. 자세한 내용은 다음과 같습니다: "${JSON.stringify(datas)}"`; 149 | break; 150 | case 'french': 151 | prompt = `fais une courte et cruelle critique sarcastique en argot pour le profil GitHub suivant : ${username}. Voici les détails : "${JSON.stringify(datas)}"`; 152 | break; 153 | case 'german': 154 | prompt = `machen sie eine grausame, kurze, harte und sarkastische Röstung auf Deutsch und verwenden Sie Wortspiele und Slang, um Humor in das folgende Github-Profil zu bringen : ${username}. Hier sind die Details : "${JSON.stringify(datas)}"`; 155 | break; 156 | case 'arabic': 157 | prompt = `.${JSON.stringify(datas)}: اليك هذه التفصيل .${username} :(GitHub) قدم سخرية قصيرة و قاصية على الملف الشخصي في`; 158 | case 'italian': 159 | prompt = `Criticami in modo sarcastico il seguente profilo GitHub: ${username}. Ecco alcuni dettagli: "${JSON.stringify(datas)}"`; 160 | break; 161 | case 'polish': 162 | prompt = `krótko i ostro skrytykuj poniższy profil GitHub: ${username}. Oto szczegóły: "${JSON.stringify(datas)}"`; 163 | break; 164 | case 'vietnamese': 165 | prompt = `Hãy đưa ra một lời châm chọc ngắn gọn và tàn nhẫn bằng tiếng lóng cho hồ sơ GitHub sau: ${username}. Đây là chi tiết: "${JSON.stringify(datas)}"`; 166 | break; 167 | case 'finnish': 168 | prompt = `Kirjoita lyhyt, julma ja sarkastinen arvostelu slangilla seuraavalle Github-profiilille: ${username}. Tässä on profiilin yksityiskohdat: "${JSON.stringify(datas)}"`; 169 | break; 170 | case 'portuguese': 171 | prompt = `faça uma crítica curta e dura para o seguinte perfil do github: ${username}. Aqui estão os detalhes: "${JSON.stringify(datas)}"`; 172 | break; 173 | case 'traditionalChinese': 174 | prompt = `請用正體中文及道地台灣俗語,對這個 GitHub 帳號殘酷地吐槽一番:${username}。以下是這個帳號的詳細資訊: "${JSON.stringify(datas)}"`; 175 | break; 176 | } 177 | 178 | // answerdebug += prompt + '\n'; 179 | try { 180 | const completion = await client.chat.completions.create({ 181 | model: 'gpt-4o-mini', 182 | stream: false, 183 | messages: [ 184 | { 185 | role: 'system', 186 | content: 187 | 'You roast people github account based on their bio, name, readme, and repos as harsh and spicy as possible, and keep it short.' 188 | }, 189 | { role: 'user', content: prompt } 190 | ] 191 | }); 192 | 193 | const roast = completion.choices[0].message.content; 194 | try { 195 | await platform.env.DB.prepare( 196 | 'INSERT INTO roasts (gh_username, response, created_at, country, ip_address, language) VALUES (?, ?, ?, ?, ?, ?)' 197 | ) 198 | .bind( 199 | username, 200 | roast, 201 | Math.floor(new Date().getTime() / 1000), 202 | request?.cf?.country || '', 203 | sha256(request.headers.get('cf-connecting-ip')) || '', 204 | language 205 | ) 206 | .run(); 207 | } catch {} 208 | return json({ roast }); 209 | } catch (error) { 210 | console.error('Error:', error); 211 | return json({ error: 'Failed to generate roast' }, { status: 500 }); 212 | } 213 | } 214 | 215 | function sha256(str) { 216 | // Get the string as arraybuffer. 217 | var buffer = new TextEncoder('utf-8').encode(str); 218 | return crypto.subtle.digest('SHA-256', buffer).then(function (hash) { 219 | return hex(hash); 220 | }); 221 | } 222 | --------------------------------------------------------------------------------
© 2024 github-roast.pages.dev
116 | Poke Admin if something 117 | goes wrong 118 |
120 | Source code on GitHub 121 |
123 | Permanently opt out and delete all roasts related to your GitHub account. 124 |