├── .gitignore ├── assets ├── profile.png ├── GitHub Profilator.fig ├── Profilator Social Preview.jpg └── template.svg ├── public ├── favicon.png ├── [404].html ├── index.html ├── styles.css └── main.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── udd.yml ├── .vscode └── settings.json ├── deps.ts ├── deno.jsonc ├── CONTRIBUTING.md ├── LICENSE ├── index.ts ├── README.md └── utils.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .github_token -------------------------------------------------------------------------------- /assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bellisario/profilator/HEAD/assets/profile.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bellisario/profilator/HEAD/public/favicon.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Bellisario 4 | -------------------------------------------------------------------------------- /assets/GitHub Profilator.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bellisario/profilator/HEAD/assets/GitHub Profilator.fig -------------------------------------------------------------------------------- /assets/Profilator Social Preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bellisario/profilator/HEAD/assets/Profilator Social Preview.jpg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "editor.formatOnSave": true, 4 | "deno.lint": true, 5 | "deno.config": "./deno.jsonc", 6 | "editor.defaultFormatter": "denoland.vscode-deno" 7 | } 8 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Application, 3 | Context, 4 | Router, 5 | } from 'https://deno.land/x/oak@v17.1.4/mod.ts'; 6 | import { encodeBase64 } from 'https://deno.land/std@0.224.0/encoding/base64.ts'; 7 | import ky from 'https://cdn.skypack.dev/ky@0.31.0?dts'; 8 | 9 | const app = new Application({ 10 | logErrors: false, 11 | }); 12 | const router = new Router(); 13 | const decoder = new TextDecoder('utf-8'); 14 | 15 | // App Version 16 | const VERSION = '1.0.0.alpha.4'; 17 | 18 | export { app, Context, decoder, encodeBase64 as encode, ky, router, VERSION }; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request] Your feature request title" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug to help us solving the problem 4 | title: "[BUG] Your bug title" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Your device info** 27 | - Device: [e.g. iPhone13Pro] 28 | - OS: [e.g. iOS16] 29 | - Browser [e.g. Safari] 30 | - Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/workflows/udd.yml: -------------------------------------------------------------------------------- 1 | name: Update Dependencies (Udd) 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "00 17 * * *" 7 | 8 | jobs: 9 | update-dependencies: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: denoland/setup-deno@v1 14 | with: 15 | deno-version: v1.x 16 | - name: Update dependencies 17 | run: | 18 | deno run -A https://deno.land/x/udd/main.ts deps.ts 19 | - name: Create Pull Request 20 | uses: peter-evans/create-pull-request@v3 21 | id: pr 22 | with: 23 | commit-message: "Update dependencies" 24 | title: Update dependencies 25 | body: > 26 | Dependencies updated by [udd](https://github.com/hayd/deno-udd). 27 | branch: deno-dependency-updates 28 | author: GitHub 29 | delete-branch: true 30 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "strict": true 5 | }, 6 | "lint": { 7 | "rules": { 8 | "tags": [ 9 | "recommended" 10 | ] 11 | } 12 | }, 13 | "fmt": { 14 | "options": { 15 | "useTabs": false, 16 | "lineWidth": 80, 17 | "indentWidth": 4, 18 | "singleQuote": true, 19 | "proseWrap": "never" 20 | }, 21 | "files": { 22 | "exclude": [ 23 | ".github/" 24 | ] 25 | } 26 | }, 27 | "tasks": { 28 | "start": "deno run --allow-net --allow-read --allow-env index.ts", 29 | "dev": "export DEV=true && deno run --allow-net --allow-read --allow-env --watch index.ts", 30 | "test": "deno task test:lint", 31 | "test:lint": "deno fmt --check", 32 | "lint:fix": "deno fmt && deno lint" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | Make sure to follow these guidelines before opening an [issue](https://github.com/Bellisario/profilator/issues/new/choose) or a [pull request](https://github.com/Bellisario/profilator/pulls): 4 | 5 | - An issue is for a bug or a feature request, if you have any question or something similar, please use [Discussions](https://github.com/Bellisario/profilator/discussions). 6 | - Before opening an issue of a pull request, please check if the issue or the pull request already exists. 7 | - Before opening a pull request, make sure to run `deno fmt` and `deno lint` on the whole project to keep code style and linting rules. 8 | - Pull requests for packages updates are not allowed since there is [Udd](https://github.com/hayd/deno-udd) that checks them automatically. 9 | - If you don't know how to contribute, also because you don't know TypeScript, JavaScript, or Deno, you can always help others on [Discussions](https://github.com/Bellisario/profilator/discussions), debug the application, share your awesome ideas with a new issue (feature request) and check the whole project for misspellings, too. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Giorgio Bellisario 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 | -------------------------------------------------------------------------------- /assets/template.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{username}} 13 | {{name}} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/[404].html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | GitHub Profilator - Not Found 12 | 13 | 14 | 15 |

GitHub ProfilatorAlpha

16 |

Not Found

17 |

The given username is not on GitHub.

18 | GitHub_Profilator 24 |
25 | 26 | 29 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | GitHub Profilator 12 | 13 | 14 | 15 | 16 | 17 |

GitHub ProfilatorAlpha

18 |

Add GitHub profiles to Markdown in a snap

19 |

Try with your or embed a new one!

20 | 25 | GitHub_Profilator 31 | 32 |
33 | 34 | 35 |
36 |
37 | 42 | 43 | 44 | 54 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #0d1117; 3 | color: #f5f4f4; 4 | font-family: sans-serif; 5 | color-scheme: dark; 6 | } 7 | 8 | body { 9 | text-align: center; 10 | margin-top: 10vh; 11 | } 12 | 13 | h2, 14 | h3 { 15 | font-weight: unset; 16 | margin-top: -2vh; 17 | } 18 | 19 | #sponsor { 20 | border: 0; 21 | /* use invert to "set" iframe dark mode like */ 22 | filter: invert(80%); 23 | border-radius: 6px; 24 | } 25 | 26 | #version { 27 | font-size: small; 28 | vertical-align: top; 29 | margin: 0.25vw; 30 | font-style: italic; 31 | color: #ff0000; 32 | } 33 | 34 | #the-result { 35 | transition: opacity 0.5s ease-in-out; 36 | width: 150px; 37 | height: 200px; 38 | opacity: 0; 39 | } 40 | 41 | #the-username, 42 | #the-markdown, 43 | #the-repo { 44 | all: unset; 45 | font-size: revert; 46 | color: #b6b3b0; 47 | background-color: #363535; 48 | padding: 1vh !important; 49 | border: 1px #4a4949 solid; 50 | border-radius: 6px; 51 | margin: 0.5vh; 52 | margin-top: 2.5vh; 53 | height: 3.5vh; 54 | text-align: revert; 55 | transition: all 0.3s ease-in-out; 56 | } 57 | 58 | #the-username:focus, 59 | #the-markdown:focus, 60 | #the-repo:focus { 61 | box-shadow: inset 0px 0px 0px 2px #646262; 62 | } 63 | 64 | #the-result:focus { 65 | all: unset; 66 | box-shadow: inset 0px 0px 0px 2px #646262; 67 | } 68 | 69 | #the-markdown, 70 | #the-repo { 71 | width: 7.5vw; 72 | cursor: pointer; 73 | } 74 | 75 | #the-repo-link { 76 | all: unset; 77 | } 78 | 79 | /* mobile only */ 80 | @media only screen and (max-width: 600px) { 81 | 82 | #the-markdown, 83 | #the-repo { 84 | width: 27.5vw; 85 | } 86 | } 87 | 88 | #the-repo { 89 | margin-top: 0; 90 | vertical-align: top; 91 | } 92 | 93 | #the-markdown:hover, 94 | #the-repo:hover { 95 | background-color: #3a3a38; 96 | transition-duration: .1s; 97 | } 98 | 99 | #profilator-container { 100 | width: 150px; 101 | height: 200px; 102 | display: inline-block; 103 | background-image: url('/@blank'); 104 | } 105 | 106 | footer { 107 | position: fixed; 108 | width: 100%; 109 | text-align: center; 110 | bottom: 2.5vh; 111 | font-size: small; 112 | } -------------------------------------------------------------------------------- /public/main.js: -------------------------------------------------------------------------------- 1 | const result = document.getElementById('the-result'); 2 | const resultLink = document.getElementById('profilator-container'); 3 | const username = document.getElementById('the-username'); 4 | const markdown = document.getElementById('the-markdown'); 5 | 6 | let keystrokeTimeout; 7 | let before = 0; 8 | let toBeCopied = getMarkdown('Bellisario'); 9 | 10 | // get the old value to easily change the markdown value just with HTML 11 | const markdownOriginalValue = markdown.value; 12 | 13 | function copyToClipboard(text) { 14 | const textarea = document.createElement('textarea'); 15 | textarea.value = text; 16 | document.body.appendChild(textarea); 17 | textarea.select(); 18 | document.execCommand('copy'); 19 | document.body.removeChild(textarea); 20 | } 21 | 22 | function getImage(username) { 23 | const ver = window.VERSION === 'DEV' ? Date.now() : window.VERSION; 24 | return `${location.protocol}//${location.host}/${username}?v=${ver}`; 25 | } 26 | 27 | function getMarkdown(username) { 28 | return `[![${username}'s Profilator](${ 29 | getImage(username) 30 | })](https://github.com/${username})`; 31 | } 32 | 33 | function getLink(username) { 34 | if (username.startsWith('@')) { 35 | return `${location.protocol}//${location.host}/`; 36 | } 37 | return `https://github.com/${username}`; 38 | } 39 | 40 | document.addEventListener('DOMContentLoaded', () => { 41 | // focus the username input 42 | username.focus(); 43 | 44 | username.addEventListener('keydown', function (e) { 45 | if (e.keyCode === 32) return e.preventDefault(); 46 | before = username.value.length; 47 | }); 48 | 49 | username.addEventListener('keyup', (_e) => { 50 | clearInterval(keystrokeTimeout); 51 | 52 | // prevent opacity change on (for example) ctrl+a 53 | if (username.value.length === before) return; 54 | 55 | result.style.opacity = 0; 56 | // set Bellisario's Profilator as default profile if there is no username 57 | if (username.value.length === 0) { 58 | toBeCopied = getMarkdown('Bellisario'); 59 | resultLink.href = getLink('Bellisario'); 60 | setTimeout(() => { 61 | result.src = getImage('Bellisario'); 62 | }, 150); 63 | return; 64 | } 65 | 66 | toBeCopied = getMarkdown(username.value); 67 | keystrokeTimeout = setTimeout(() => { 68 | result.src = getImage(username.value); 69 | resultLink.href = getLink(username.value); 70 | }, 500); 71 | }); 72 | 73 | markdown.addEventListener('click', () => { 74 | copyToClipboard(toBeCopied); 75 | markdown.value = 'Copied!'; 76 | // https://stackoverflow.com/a/4067488/14997578 77 | username.setSelectionRange(0, username.value.length); 78 | username.focus(); 79 | setTimeout(() => { 80 | markdown.value = markdownOriginalValue; 81 | }, 1500); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | // cspell:word maxage 2 | 3 | import { app, router, VERSION } from './deps.ts'; 4 | import { 5 | acceptsHTML, 6 | defaultProfiles, 7 | DEV, 8 | getImageBase64, 9 | getUserName, 10 | isGitHubCamoBot, 11 | oneDaySeconds, 12 | Profile, 13 | scaler, 14 | } from './utils.ts'; 15 | 16 | // Static files under the "public" folder 17 | app.use(async (ctx, next) => { 18 | try { 19 | if (ctx.request.url.pathname === '/[404].html') { 20 | throw new Error('Not found'); 21 | } 22 | await ctx.send({ 23 | root: `${Deno.cwd()}/public`, 24 | index: 'index.html', 25 | }); 26 | } catch { 27 | await next(); 28 | } 29 | }); 30 | 31 | // Set cache headers 32 | app.use(async (ctx, next) => { 33 | ctx.response.headers.set( 34 | 'cache-control', 35 | 's-maxage=' + oneDaySeconds, 36 | ); 37 | await next(); 38 | }); 39 | 40 | // Set Bellisario profilator as default profile (for the favicon) 41 | router.get('/favicon.svg', (ctx) => { 42 | ctx.response.redirect('/Bellisario'); 43 | }); 44 | router.get('/favicon.ico', (ctx) => { 45 | ctx.response.redirect('/Bellisario'); 46 | }); 47 | 48 | // Using "@" because GitHub usernames cannot start with this character 49 | // use @profilator as Profilator example image 50 | router.get('/@profilator', (ctx) => { 51 | ctx.response.type = 'image/svg+xml'; 52 | ctx.response.body = defaultProfiles['@profilator']; 53 | }); 54 | // use @blank as blank profile 55 | router.get('/@blank', (ctx) => { 56 | ctx.response.type = 'image/svg+xml'; 57 | ctx.response.body = defaultProfiles['@blank']; 58 | }); 59 | // get App Version 60 | router.get('/@version', (ctx) => { 61 | ctx.response.type = 'text/javascript'; 62 | ctx.response.body = `window.VERSION = '${DEV ? 'DEV' : VERSION}';`; 63 | }); 64 | 65 | router.get('/:username', async (ctx, next) => { 66 | const { username } = ctx.params; 67 | const v = ctx.request.url.searchParams.get('v'); 68 | // Number(null) (when param not set) returns 0 , so set falsy values to '1' 69 | const scaleString = ctx.request.url.searchParams.get('scale') || '1'; 70 | // check if is NaN because 0 is a valid scale and will be set to the min scale, not to 1 71 | const scaleRaw = !Number.isNaN(Number(scaleString)) 72 | ? Number(scaleString) 73 | : 1; 74 | // make a valid scale 75 | const scale = scaler(scaleRaw); 76 | 77 | if (DEV === true && v === null) { 78 | return ctx.response.redirect(`/${username}?v=${Date.now()}`); 79 | } 80 | 81 | if (v === null) { 82 | return ctx.response.redirect(`/${username}?v=${VERSION}`); 83 | } 84 | 85 | const { name, err } = await getUserName(username); 86 | 87 | if (err) { 88 | return await next(); 89 | } 90 | 91 | const image = await getImageBase64( 92 | // prevent scale image of more than 3x (additional condition) 93 | `https://github.com/${username}.png?size=${ 94 | 101 * (scale > 3 ? 3 : scale) 95 | }`, 96 | ); 97 | 98 | const profile = Profile({ username, name, image, scale }); 99 | ctx.response.type = 'image/svg+xml'; 100 | ctx.response.body = profile; 101 | }); 102 | 103 | router.use(async (ctx) => { 104 | // prevent GitHub's "Camo" and images load from 404 error (or content fails to load) 105 | if (isGitHubCamoBot(ctx) || !acceptsHTML(ctx)) { 106 | ctx.response.type = 'image/svg+xml'; 107 | ctx.response.body = defaultProfiles['404']; 108 | return; 109 | } 110 | ctx.response.status = 404; 111 | ctx.response.type = 'text/html'; 112 | ctx.response.body = await Deno.readFile(`${Deno.cwd()}/public/[404].html`); 113 | }); 114 | 115 | app.use(router.routes()); 116 | app.use(router.allowedMethods()); 117 | 118 | app.addEventListener('error', (evt) => { 119 | // ignore this error 120 | if (evt.error.message === 'connection closed before message completed') { 121 | return; 122 | } 123 | console.log(evt); 124 | }); 125 | 126 | console.log('App listening on port', 3000); 127 | await app.listen({ port: 3000 }); 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 |

5 |

GitHub Profilator

6 | 7 | > Add GitHub profiles to Markdown in a snap 8 | 9 | > **Warning**\ 10 | > This is an _Alpha_ version of the project, and it is not stable yet. 11 | 12 | **Official website:** [https://profilator.deno.dev/](https://profilator.deno.dev/) 13 | 14 | > Are you using GitHub Profilator? Let us know [on this discussion](https://github.com/Bellisario/profilator/discussions/1)! :rocket: 15 | 16 | ## Example 17 | 18 | 19 | 20 | | Profile | Smooth Error | 21 | | ---- | ---- | 22 | | [![Bellisario's Profilator](https://profilator.deno.dev/Bellisario?v=1.0.0.alpha.4)](https://github.com/Bellisario) | [![@404's Profilator](https://profilator.deno.dev/@404?v=1.0.0.alpha.4)](https://github.com/@404) | 23 | 24 | You can also customize the scale of the Profilator (there are no step scaling limits, _ex. you can also scale 0.93534...x_) 25 | | 1x scale | 0.75x scale | 0.5x scale | 26 | | ---- | ---- | ---- | 27 | | [![Bellisario's Profilator](https://profilator.deno.dev/Bellisario?v=1.0.0.alpha.4)](https://github.com/Bellisario) | [![Bellisario's Profilator](https://profilator.deno.dev/Bellisario?v=1.0.0.alpha.4&scale=0.75)](https://github.com/Bellisario) | [![Bellisario's Profilator](https://profilator.deno.dev/Bellisario?v=1.0.0.alpha.4&scale=0.5)](https://github.com/Bellisario) 28 | 29 | > **Warning:** Currently scale cannot be below 0.5x (because Profilator will become invisible :ghost:) 30 | 31 | 32 | 33 | ### How to use the scale option 34 | 35 | Generate a new Profilator from [Profilator website](https://profilator.deno.dev/) and then when adding to markdown change it like this: 36 | 37 | ```diff 38 | + [![Bellisario's Profilator](https://profilator.deno.dev/Bellisario?v=1.0.0.alpha.4&scale=0.75)](https://github.com/Bellisario) 39 | - [![Bellisario's Profilator](https://profilator.deno.dev/Bellisario?v=1.0.0.alpha.4)](https://github.com/Bellisario) 40 | ``` 41 | 42 | > **Tip:** you can also scale more than 1x, for example 2x, but for now the image resolution is the same, so could be grainy 43 | 44 | > **Warning:** Profile images over 3x will be rendered as 3x to prevent server bandwidth consumption too high and client heavy image downloads. 45 | 46 | ### Other tips 47 | 48 | #### Force username letter uppercase 49 | 50 | If you want to force an username letter to be uppercase, just write the username with the letter in uppercase, Profilator will be able to parse it anyway and you'll see that in uppercase, too.\ 51 | See the example below: 52 | 53 | 54 | 55 | | lowercase | forced uppercase | 56 | | ---- | ---- | 57 | | [![jamesbond's Profilator](https://profilator.deno.dev/jamesbond?v=1.0.0.alpha.4)](https://github.com/jamesbond) | [![JamesBond's Profilator](https://profilator.deno.dev/JamesBond?v=1.0.0.alpha.4)](https://github.com/JamesBond) | 58 | 59 | 60 | 61 | ## How it works 62 | 63 | Under the hood, GitHub Profilator uses the GitHub API to fetch the profile data and then uses a [pre-built template](https://github.com/Bellisario/profilator/blob/main/assets/template.svg) to generate the image, with the all the data needed. 64 | 65 | ## Technologies 66 | 67 | I decided to use [Deno](https://deno.land) for this project because it's a great tool for building simple and fast servers, with the help of the awesome [Deno Deploy](https://deno.com/deploy). 68 | 69 | The template is built with Figma (and then manually modified) and you can find the .fig file [here](https://github.com/Bellisario/profilator/blob/main/assets/GitHub%20Profilator.fig). 70 | 71 | ## Why should you use GitHub Profilator? 72 | 73 | There is a simple answer to this question: like the description said "you can add GitHub profiles to Markdown in a snap" and I can also add you are able also to get a beautiful profile display for your GitHub profile, and not an "ugly" one like below (you can see on a lot of repositories): 74 | 75 | | [![Giorgio Bellisario](https://github.com/Bellisario.png?size=100)](https://github.com/Bellisario) | 76 | | -------------------------------------------------------------------------------------------------- | 77 | | [Giorgio Bellisario](https://github.com/Bellisario) | 78 | 79 | ## Development 80 | 81 | To get started, clone the repo: 82 | 83 | ```bash 84 | git clone https://github.com/Bellisario/profilator.git 85 | ``` 86 | 87 | Then, you can run the following command to start the server: 88 | 89 | ```bash 90 | deno task dev 91 | ``` 92 | 93 | You can also run the following command to start the server in production mode: 94 | 95 | ```bash 96 | deno task start 97 | ``` 98 | 99 | --- 100 | 101 | **Warning:** You could need to create a new GitHub personal access token to use this server (especially if your IP Address is associated from GitHub as "too many requests"). 102 | 103 | ### To use a personal access token 104 | 105 | Create a new one from [here](https://github.com/settings/tokens/new?description=GitHub%20Profilator%20DEV) (it requires no permissions). 106 | 107 | Then, you can set the token into a file named `.github_token` in the root of the project. You can also use the terminal like this: 108 | 109 | ```bash 110 | echo > .github_token 111 | ``` 112 | 113 | If you prefer, you can also create a new environment variable called `GITHUB_TOKEN` and set it to the token, but this is not recommended for development use: it's only recommended if you want to use the server in production mode (for example) on [Deno Deploy](https://deno.com/deploy). 114 | 115 | ## How to contribute 116 | 117 | Feel free to [open an issue](https://github.com/Bellisario/profilator/issues/new/choose) or a [pull request](https://github.com/Bellisario/profilator/pulls) but follow [Contributing Guidelines](https://github.com/Bellisario/profilator/blob/main/CONTRIBUTING.md). 118 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | import { Context, decoder, encode, ky } from './deps.ts'; 2 | 3 | export const DEV = Deno.env.get('DEV') === 'true'; 4 | if (DEV === true) { 5 | console.log('DEV mode enabled'); 6 | } 7 | 8 | type Base64 = string; 9 | interface ProfileData { 10 | username: string; 11 | name: string; 12 | image: Base64; 13 | scale: number; 14 | } 15 | interface GitHubAPI { 16 | login: string; 17 | id: number; 18 | node_id: string; 19 | avatar_url: string; 20 | gravatar_id: string; 21 | url: string; 22 | html_url: string; 23 | followers_url: string; 24 | following_url: string; 25 | gists_url: string; 26 | starred_url: string; 27 | subscriptions_url: string; 28 | organizations_url: string; 29 | repos_url: string; 30 | events_url: string; 31 | received_events_url: string; 32 | type: string; 33 | site_admin: boolean; 34 | name: string | null; 35 | company?: string | null; 36 | blog: string; 37 | location: string; 38 | email?: string | null; 39 | hireable?: string | boolean | null; 40 | bio: string; 41 | twitter_username?: string | null; 42 | public_repos: number; 43 | public_gists: number; 44 | followers: number; 45 | following: number; 46 | created_at: Date; 47 | updated_at: Date; 48 | } 49 | 50 | /** 51 | * Replaces the given text {{token}} with the given value. 52 | */ 53 | export class Replacer { 54 | #text; 55 | constructor(text: string) { 56 | this.#text = text; 57 | } 58 | replace(search: string, replace: string) { 59 | this.#text = this.#text.replaceAll(`{{${search}}}`, replace); 60 | } 61 | get result() { 62 | return this.#text; 63 | } 64 | } 65 | 66 | function getDEVGitHubToken() { 67 | try { 68 | const data = Deno.readFileSync('.github_token'); 69 | return decoder.decode(data); 70 | } catch { 71 | return; 72 | } 73 | } 74 | 75 | const GITHUB_TOKEN: string = Deno.env.get('GITHUB_TOKEN') || 76 | getDEVGitHubToken() || ''; 77 | 78 | /** 79 | * Get name of user from GitHub API, returns object with 'err' if fails (and means an user does not exist) 80 | */ 81 | export async function getUserName( 82 | username: string, 83 | ): Promise<{ name: string; err: boolean }> { 84 | try { 85 | const res: GitHubAPI = await ky.get( 86 | `https://api.github.com/users/${username}`, 87 | { 88 | headers: { 89 | Authorization: `token ${GITHUB_TOKEN}`, 90 | }, 91 | }, 92 | ).json(); 93 | return { 94 | err: false, 95 | name: res.name || '', 96 | }; 97 | } catch { 98 | return { 99 | err: true, 100 | name: '', 101 | }; 102 | } 103 | } 104 | 105 | const template = decoder.decode(await Deno.readFile('./assets/template.svg')); 106 | 107 | /** 108 | * Returns a valid scale from the given scale number. 109 | */ 110 | export function scaler(scale: number) { 111 | if (typeof scale !== 'number') { 112 | // if scale is not provided (or invalid), use default 113 | return 1; 114 | } 115 | // prevent scale to be below 0.5 116 | return scale < 0.5 ? 0.5 : scale; 117 | } 118 | 119 | /** 120 | * Return a new profile SVG (as a string) with the given params. 121 | */ 122 | export function Profile(params: ProfileData) { 123 | const replacer = new Replacer(template); 124 | const defaultHeight = 200; 125 | const defaultWidth = 150; 126 | const scale = params.scale; 127 | 128 | const height = defaultHeight * scale; 129 | const width = defaultWidth * scale; 130 | 131 | // @ts-ignore: Delete scale to avoid unnecessary forEach loop 132 | delete params.scale; 133 | 134 | Object.keys(params).forEach((key: string) => { 135 | //@ts-ignore: Cannot params[key] with key of type string 136 | replacer.replace(key, params[key]); 137 | }); 138 | replacer.replace('height', height.toString()); 139 | replacer.replace('width', width.toString()); 140 | return replacer.result; 141 | } 142 | 143 | /** 144 | * Return a Base64 string of the given image URL. 145 | */ 146 | export async function getImageBase64(url: string): Promise { 147 | const res = await ky.get(url).arrayBuffer(); 148 | return encode(res); 149 | } 150 | 151 | /** 152 | * Return a Base64 string of the given image path. 153 | */ 154 | export async function getLocalImageBase64(url: string): Promise { 155 | const res = (await Deno.readFile(url)).buffer; 156 | return encode(res); 157 | } 158 | 159 | /** 160 | * Seconds in a day 161 | */ 162 | export const oneDaySeconds = 24 * 60 * 60; 163 | 164 | /** 165 | * Blank GIF Base64 encoded 166 | */ 167 | const blankGif = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; 168 | 169 | const internalProfilatorsData: { [key: string]: ProfileData } = { 170 | '@profilator': { 171 | username: 'Profilator', 172 | name: 'Snap GitHub profiles', 173 | image: await getLocalImageBase64(`${Deno.cwd()}/assets/profile.png`), 174 | scale: 1, 175 | }, 176 | '@blank': { 177 | username: '', 178 | name: '', 179 | image: blankGif, 180 | scale: 1, 181 | }, 182 | '404': { 183 | username: 'Not Found', 184 | name: 'GitHub user not found.', 185 | image: blankGif, 186 | scale: 1, 187 | }, 188 | }; 189 | 190 | /** 191 | * Default profile profilators for internal use 192 | */ 193 | export const defaultProfiles = { 194 | '@profilator': Profile(internalProfilatorsData['@profilator']), 195 | '@blank': Profile(internalProfilatorsData['@blank']), 196 | '404': Profile(internalProfilatorsData['404']), 197 | }; 198 | 199 | /** 200 | * Detect if HTML is excepted as response of the given request 201 | */ 202 | export function acceptsHTML(ctx: Context): boolean { 203 | return ctx.request.accepts()?.includes('text/html') ?? false; 204 | } 205 | 206 | /** 207 | * Detect if a request is made by a GitHub Camo Bot 208 | */ 209 | export function isGitHubCamoBot(ctx: Context): boolean { 210 | return ctx.request.headers.get('User-Agent')?.includes('GitHub-Camo') ?? 211 | false; 212 | } 213 | --------------------------------------------------------------------------------