├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── main.yml │ └── pull_request.yml ├── .gitignore ├── .npmrc ├── .puppeteerrc.js ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── fly.toml ├── gulpfile.js ├── package.json ├── public ├── README.md ├── banner.png ├── css │ ├── style.css │ └── style.min.css ├── fallback.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── js │ ├── main.js │ └── main.min.js ├── logo-raw.png ├── logo-transparent.png ├── logo.png └── robots.txt ├── src ├── authentication.js ├── avatar │ ├── auto.js │ ├── index.js │ ├── provider.js │ └── resolve.js ├── constant.js ├── index.js ├── providers │ ├── deviantart.js │ ├── dribbble.js │ ├── duckduckgo.js │ ├── github.js │ ├── gitlab.js │ ├── google.js │ ├── gravatar.js │ ├── index.js │ ├── instagram.js │ ├── microlink.js │ ├── onlyfans.js │ ├── readcv.js │ ├── reddit.js │ ├── soundcloud.js │ ├── substack.js │ ├── telegram.js │ ├── tiktok.js │ ├── twitch.js │ ├── x.js │ └── youtube.js ├── send │ ├── cache.js │ └── index.js ├── server.js ├── ua.js └── util │ ├── browserless.js │ ├── cacheable-lookup.js │ ├── error.js │ ├── got.js │ ├── html-get.js │ ├── is-iterable.js │ ├── keyv.js │ ├── memoize.js │ ├── reachable-url.js │ ├── redis │ ├── create.js │ ├── index.js │ └── ua.js │ └── uuid.js └── test ├── endpoints.js ├── helpers.js └── query-parameters.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 80 13 | indent_brace_style = 1TBS 14 | spaces_around_operators = true 15 | quote_type = auto 16 | 17 | [package.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | # Check for updates to GitHub Actions every weekday 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | contributors: 10 | if: "${{ github.event.head_commit.message != 'build: contributors' }}" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: lts/* 22 | - name: Contributors 23 | run: | 24 | git config --global user.email ${{ secrets.GIT_EMAIL }} 25 | git config --global user.name ${{ secrets.GIT_USERNAME }} 26 | npm run contributors 27 | - name: Push changes 28 | run: | 29 | git push origin ${{ github.head_ref }} 30 | 31 | release: 32 | if: | 33 | !startsWith(github.event.head_commit.message, 'chore(release):') && 34 | !startsWith(github.event.head_commit.message, 'docs:') && 35 | !startsWith(github.event.head_commit.message, 'ci:') 36 | needs: [contributors] 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v4 41 | with: 42 | token: ${{ secrets.GITHUB_TOKEN }} 43 | - name: Setup Node.js 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: lts/* 47 | - name: Setup PNPM 48 | uses: pnpm/action-setup@v4 49 | with: 50 | version: latest 51 | run_install: true 52 | - name: Test 53 | run: npm test 54 | - name: Release 55 | env: 56 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 57 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 58 | run: | 59 | git config --global user.email ${{ secrets.GIT_EMAIL }} 60 | git config --global user.name ${{ secrets.GIT_USERNAME }} 61 | git pull origin master 62 | npm run release 63 | deploy: 64 | needs: [release] 65 | runs-on: ubuntu-latest 66 | steps: 67 | - name: Checkout 68 | uses: actions/checkout@v4 69 | with: 70 | token: ${{ secrets.GITHUB_TOKEN }} 71 | - name: Install 72 | uses: superfly/flyctl-actions/setup-flyctl@master 73 | - name: Deploy 74 | run: flyctl deploy --remote-only --detach 75 | env: 76 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 77 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: pull_request 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | if: github.ref != 'refs/heads/master' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: lts/* 24 | - name: Setup PNPM 25 | uses: pnpm/action-setup@v4 26 | with: 27 | version: latest 28 | run_install: true 29 | - name: Test 30 | run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # npm 3 | ############################ 4 | node_modules 5 | npm-debug.log 6 | .node_history 7 | yarn.lock 8 | package-lock.json 9 | 10 | ############################ 11 | # tmp, editor & OS files 12 | ############################ 13 | .tmp 14 | *.swo 15 | *.swp 16 | *.swn 17 | *.swm 18 | .DS_Store 19 | *# 20 | *~ 21 | .idea 22 | *sublime* 23 | nbproject 24 | 25 | ############################ 26 | # Tests 27 | ############################ 28 | testApp 29 | coverage 30 | .nyc_output 31 | 32 | ############################ 33 | # Other 34 | ############################ 35 | .envrc 36 | .env 37 | .vercel 38 | scripts 39 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | audit=false 2 | fund=false 3 | lockfile=false 4 | loglevel=error 5 | save-prefix=~ 6 | save=false 7 | strict-peer-dependencies=false 8 | unsafe-perm=true 9 | -------------------------------------------------------------------------------- /.puppeteerrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | module.exports = { 6 | cacheDirectory: path.join(__dirname, 'node_modules/.cache') 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### 3.7.31 (2025-06-03) 6 | 7 | ### 3.7.30 (2025-06-02) 8 | 9 | ### 3.7.29 (2025-05-20) 10 | 11 | ### 3.7.28 (2025-05-20) 12 | 13 | ### 3.7.27 (2025-05-05) 14 | 15 | ### 3.7.26 (2025-05-01) 16 | 17 | ### 3.7.25 (2025-04-29) 18 | 19 | ### 3.7.24 (2025-04-24) 20 | 21 | ### 3.7.23 (2025-04-09) 22 | 23 | ### 3.7.22 (2025-04-04) 24 | 25 | ### 3.7.21 (2025-04-02) 26 | 27 | ### 3.7.20 (2025-03-28) 28 | 29 | ### 3.7.19 (2025-03-21) 30 | 31 | ### 3.7.18 (2025-03-18) 32 | 33 | ### 3.7.17 (2025-03-14) 34 | 35 | ### 3.7.16 (2025-03-13) 36 | 37 | ### 3.7.15 (2025-03-12) 38 | 39 | ### 3.7.14 (2025-03-06) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * flyio configuration ([5e1fdc6](https://github.com/Kikobeats/unavatar/commit/5e1fdc6b031cd79157eea9c1fac9aac651a5a144)) 45 | 46 | ### 3.7.13 (2025-02-09) 47 | 48 | ### 3.7.12 (2025-01-30) 49 | 50 | ### 3.7.11 (2025-01-16) 51 | 52 | ### 3.7.10 (2025-01-04) 53 | 54 | ### 3.7.9 (2024-12-19) 55 | 56 | ### 3.7.8 (2024-12-06) 57 | 58 | ### 3.7.7 (2024-12-04) 59 | 60 | ### 3.7.6 (2024-11-14) 61 | 62 | ### 3.7.5 (2024-11-05) 63 | 64 | ### 3.7.4 (2024-10-22) 65 | 66 | ### 3.7.3 (2024-10-17) 67 | 68 | ### 3.7.2 (2024-10-08) 69 | 70 | ### 3.7.1 (2024-10-03) 71 | 72 | ## 3.7.0 (2024-10-01) 73 | 74 | 75 | ### Features 76 | 77 | * add onlyfans ([0cfe669](https://github.com/Kikobeats/unavatar/commit/0cfe669ed2c090915846069781aead93ff4f20fc)) 78 | 79 | ### 3.6.36 (2024-09-30) 80 | 81 | ### 3.6.35 (2024-09-30) 82 | 83 | ### 3.6.34 (2024-09-30) 84 | 85 | ### 3.6.33 (2024-09-19) 86 | 87 | ### 3.6.32 (2024-09-15) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * handle no data no fallback ([3ddeb1a](https://github.com/Kikobeats/unavatar/commit/3ddeb1a13dae2d3b3f2a4ec330872ebf273021da)) 93 | 94 | ### 3.6.31 (2024-09-13) 95 | 96 | ### 3.6.30 (2024-09-10) 97 | 98 | ### 3.6.29 (2024-09-08) 99 | 100 | ### 3.6.28 (2024-09-03) 101 | 102 | ### 3.6.27 (2024-08-27) 103 | 104 | ### 3.6.26 (2024-08-15) 105 | 106 | ### 3.6.25 (2024-08-13) 107 | 108 | ### 3.6.24 (2024-08-12) 109 | 110 | ### 3.6.23 (2024-08-12) 111 | 112 | ### 3.6.22 (2024-08-12) 113 | 114 | ### 3.6.21 (2024-08-10) 115 | 116 | ### 3.6.20 (2024-08-07) 117 | 118 | ### 3.6.19 (2024-08-01) 119 | 120 | ### 3.6.18 (2024-07-26) 121 | 122 | ### 3.6.17 (2024-07-12) 123 | 124 | ### 3.6.16 (2024-06-24) 125 | 126 | ### 3.6.15 (2024-06-15) 127 | 128 | ### 3.6.14 (2024-06-13) 129 | 130 | ### 3.6.13 (2024-06-06) 131 | 132 | ### 3.6.12 (2024-06-06) 133 | 134 | ### 3.6.11 (2024-05-27) 135 | 136 | ### 3.6.10 (2024-05-27) 137 | 138 | ### 3.6.9 (2024-05-24) 139 | 140 | ### 3.6.8 (2024-05-17) 141 | 142 | ### 3.6.7 (2024-05-11) 143 | 144 | ### 3.6.6 (2024-05-08) 145 | 146 | ### 3.6.5 (2024-05-07) 147 | 148 | ### 3.6.4 (2024-05-06) 149 | 150 | ### 3.6.3 (2024-04-24) 151 | 152 | ### 3.6.2 (2024-04-24) 153 | 154 | ### 3.6.1 (2024-04-24) 155 | 156 | ## 3.6.0 (2024-04-21) 157 | 158 | 159 | ### Features 160 | 161 | * better flyio pattern ([ed2326e](https://github.com/Kikobeats/unavatar/commit/ed2326e6257067143548f94e5b6d12884b4a4e91)) 162 | 163 | ### 3.5.6 (2024-04-21) 164 | 165 | 166 | ### Bug Fixes 167 | 168 | * skip ua if REDIS_URI_UA is missing ([df8481c](https://github.com/Kikobeats/unavatar/commit/df8481c667ac7983e000d67312358c36f2fcbea7)) 169 | 170 | ### 3.5.5 (2024-04-17) 171 | 172 | ### [3.5.4](https://github.com/Kikobeats/unavatar/compare/v3.5.2...v3.5.4) (2024-04-03) 173 | 174 | ### 3.5.2 (2024-03-30) 175 | 176 | ### 3.5.1 (2024-01-15) 177 | 178 | ## 3.5.0 (2024-01-15) 179 | 180 | 181 | ### Features 182 | 183 | * add req-frequency ([32bf91e](https://github.com/Kikobeats/unavatar/commit/32bf91ebed11d084748c825a0d72e0cd955b6ac6)) 184 | 185 | 186 | ### Bug Fixes 187 | 188 | * ensure fallback url is reachable ([c173f3c](https://github.com/Kikobeats/unavatar/commit/c173f3c858324d085a6aac85760fb41e4079ea25)) 189 | 190 | ## 3.4.0 (2024-01-09) 191 | 192 | 193 | ### Features 194 | 195 | * use in memory rate limiter ([416c978](https://github.com/Kikobeats/unavatar/commit/416c9785701a15922390ba5da895de590efe8f9d)) 196 | 197 | ### [3.3.13](https://github.com/Kikobeats/unavatar/compare/v3.3.12...v3.3.13) (2024-01-08) 198 | 199 | ### 3.3.12 (2024-01-07) 200 | 201 | ### 3.3.11 (2024-01-05) 202 | 203 | ### 3.3.10 (2024-01-04) 204 | 205 | ### 3.3.9 (2024-01-01) 206 | 207 | ### 3.3.8 (2023-12-31) 208 | 209 | ### 3.3.7 (2023-12-28) 210 | 211 | ### 3.3.6 (2023-12-28) 212 | 213 | 214 | ### Bug Fixes 215 | 216 | * minor issues ([#297](https://github.com/Kikobeats/unavatar/issues/297)) ([99bcbdb](https://github.com/Kikobeats/unavatar/commit/99bcbdb89ba16d4cd9f5dd70f3c179e1cb4cc212)) 217 | 218 | ### [3.3.5](https://github.com/Kikobeats/unavatar/compare/v3.3.4...v3.3.5) (2023-12-28) 219 | 220 | 221 | ### Bug Fixes 222 | 223 | * linter ([66bba8c](https://github.com/Kikobeats/unavatar/commit/66bba8c31320ca8de0ca9a19351621164269d888)) 224 | * puppeteer setup ([2a02c22](https://github.com/Kikobeats/unavatar/commit/2a02c227ad9e6f060a7e7f502e1e73ae72c1f6e0)) 225 | 226 | ### 3.3.4 (2023-06-28) 227 | 228 | ### 3.3.3 (2023-06-28) 229 | 230 | ### 3.3.2 (2023-06-13) 231 | 232 | ### 3.3.1 (2023-06-04) 233 | 234 | 235 | ### Bug Fixes 236 | 237 | * pass arguments properly ([6991992](https://github.com/Kikobeats/unavatar/commit/6991992a6b3d449c8486dbc8bd4ea3c1bb7e80c1)), closes [#252](https://github.com/Kikobeats/unavatar/issues/252) 238 | 239 | ## 3.3.0 (2023-06-04) 240 | 241 | 242 | ### Features 243 | 244 | * requests are cancelables ([#251](https://github.com/Kikobeats/unavatar/issues/251)) ([8ed1878](https://github.com/Kikobeats/unavatar/commit/8ed187897886aa1cd0d24f1f22b0bb48eaed8e52)) 245 | 246 | ### 3.2.7 (2023-05-14) 247 | 248 | ### 3.2.6 (2023-05-05) 249 | 250 | ### 3.2.5 (2023-05-05) 251 | 252 | ### 3.2.4 (2023-05-04) 253 | 254 | ### 3.2.3 (2023-05-03) 255 | 256 | ### 3.2.2 (2023-04-23) 257 | 258 | ### 3.2.1 (2023-04-23) 259 | 260 | ## [3.2.0](https://github.com/Kikobeats/unavatar/compare/v3.1.0...v3.2.0) (2023-04-10) 261 | 262 | 263 | ### Features 264 | 265 | * add tiktok support ([#230](https://github.com/Kikobeats/unavatar/issues/230)) ([02750a8](https://github.com/Kikobeats/unavatar/commit/02750a88b6577535607227aac65b5db0b392a0c0)), closes [#225](https://github.com/Kikobeats/unavatar/issues/225) 266 | * revamp website ([#231](https://github.com/Kikobeats/unavatar/issues/231)) ([d2dda16](https://github.com/Kikobeats/unavatar/commit/d2dda16cfe679c40e2140d85331893db8eadf2d7)) 267 | 268 | ## [3.1.0](https://github.com/Kikobeats/unavatar/compare/v3.0.0...v3.1.0) (2023-03-08) 269 | 270 | 271 | ### Features 272 | 273 | * add readcv support ([#228](https://github.com/Kikobeats/unavatar/issues/228)) ([e2cd2f6](https://github.com/Kikobeats/unavatar/commit/e2cd2f684cb238d7b44d80b0d08299d7df698265)) 274 | 275 | ## [3.0.0](https://github.com/Kikobeats/unavatar/compare/v2.11.19...v3.0.0) (2023-03-05) 276 | 277 | 278 | ### Features 279 | 280 | * revamp project ([b4d3185](https://github.com/Kikobeats/unavatar/commit/b4d3185ceb41f628b15e8c48116759149a96eb69)) 281 | 282 | ### 2.11.19 (2023-02-25) 283 | 284 | ### 2.11.18 (2023-02-25) 285 | 286 | 287 | ### Bug Fixes 288 | 289 | * gitlab selector ([275f9b9](https://github.com/Kikobeats/unavatar/commit/275f9b9626bee46b03afee4c7f348f896455ea59)) 290 | 291 | ### 2.11.17 (2023-02-24) 292 | 293 | 294 | ### Bug Fixes 295 | 296 | * user the right reachable method ([#219](https://github.com/Kikobeats/unavatar/issues/219)) ([af958b1](https://github.com/Kikobeats/unavatar/commit/af958b1915f31385e3d1a0a318d727b4a82280e1)), closes [#218](https://github.com/Kikobeats/unavatar/issues/218) 297 | 298 | ### 2.11.16 (2023-02-22) 299 | 300 | ### 2.11.15 (2023-02-22) 301 | 302 | ### 2.11.14 (2023-02-22) 303 | 304 | ### 2.11.13 (2023-02-22) 305 | 306 | ### 2.11.12 (2023-02-05) 307 | 308 | ### 2.11.11 (2023-01-26) 309 | 310 | ### 2.11.10 (2023-01-10) 311 | 312 | ### 2.11.9 (2023-01-02) 313 | 314 | ### 2.11.8 (2023-01-02) 315 | 316 | ### 2.11.7 (2022-12-21) 317 | 318 | ### 2.11.6 (2022-12-21) 319 | 320 | ### 2.11.5 (2022-12-21) 321 | 322 | ### 2.11.4 (2022-12-21) 323 | 324 | ### 2.11.3 (2022-12-21) 325 | 326 | ### 2.11.2 (2022-12-21) 327 | 328 | ### 2.11.1 (2022-12-20) 329 | 330 | ## 2.11.0 (2022-12-20) 331 | 332 | 333 | ### Features 334 | 335 | * add traffic control ([#205](https://github.com/Kikobeats/unavatar/issues/205)) ([9865f54](https://github.com/Kikobeats/unavatar/commit/9865f54e6b1b20a118a88a83e523aa952246571a)) 336 | 337 | ### 2.10.13 (2022-12-20) 338 | 339 | ### 2.10.12 (2022-11-19) 340 | 341 | ### 2.10.11 (2022-07-06) 342 | 343 | ### 2.10.10 (2022-06-11) 344 | 345 | ### 2.10.9 (2022-05-29) 346 | 347 | ### 2.10.8 (2022-05-17) 348 | 349 | ### 2.10.7 (2022-02-25) 350 | 351 | ### 2.10.6 (2022-02-14) 352 | 353 | ### 2.10.5 (2022-01-11) 354 | 355 | ### 2.10.4 (2022-01-11) 356 | 357 | ### 2.10.3 (2022-01-11) 358 | 359 | ### 2.10.2 (2022-01-11) 360 | 361 | 362 | ### Bug Fixes 363 | 364 | * regions should be an array ([fd24093](https://github.com/Kikobeats/unavatar/commit/fd24093d9229d77097be694f970ca26987248530)) 365 | 366 | ### 2.10.1 (2022-01-11) 367 | 368 | ## 2.10.0 (2022-01-11) 369 | 370 | 371 | ### Features 372 | 373 | * use polka instead of express ([c680e04](https://github.com/Kikobeats/unavatar/commit/c680e04b3578bafda1d05b45119ae5e955f15abb)) 374 | 375 | ## 2.9.0 (2022-01-10) 376 | 377 | 378 | ### Features 379 | 380 | * add rate limiter ([99ae399](https://github.com/Kikobeats/unavatar/commit/99ae3991d2560611fa1d75d13144dc86bd8f9fbf)) 381 | 382 | ### 2.8.7 (2022-01-08) 383 | 384 | 385 | ### Bug Fixes 386 | 387 | * ensure to wait until twitter dom is loaded ([#177](https://github.com/Kikobeats/unavatar/issues/177)) ([dbea09f](https://github.com/Kikobeats/unavatar/commit/dbea09f26ab495e961580f93f4036fcf3f4dd1cc)) 388 | 389 | ### 2.8.6 (2022-01-08) 390 | 391 | 392 | ### Bug Fixes 393 | 394 | * rename fallback into data variable ([d98cdf2](https://github.com/Kikobeats/unavatar/commit/d98cdf2c1a755c3942af39e80e9b92f5a3b584ab)) 395 | 396 | ### 2.8.5 (2022-01-08) 397 | 398 | ### 2.8.4 (2022-01-07) 399 | 400 | 401 | ### Bug Fixes 402 | 403 | * ensure fallback is present ([a50d505](https://github.com/Kikobeats/unavatar/commit/a50d505a7a873ff82c3434c361dc30375958c25e)) 404 | 405 | ### 2.8.3 (2022-01-06) 406 | 407 | 408 | ### Bug Fixes 409 | 410 | * disable crossOriginResourcePolicy ([#176](https://github.com/Kikobeats/unavatar/issues/176)) ([12475c5](https://github.com/Kikobeats/unavatar/commit/12475c575ab0d7a566bf110fff9356c84d953a10)) 411 | 412 | ### 2.8.2 (2022-01-03) 413 | 414 | ### 2.8.1 (2021-11-21) 415 | 416 | 417 | ### Bug Fixes 418 | 419 | * ensure input is not empty ([da1693d](https://github.com/Kikobeats/unavatar/commit/da1693d584b12ee62d9c4cb2b3a2e0d4dc3c20af)) 420 | 421 | ## 2.8.0 (2021-11-21) 422 | 423 | 424 | ### Features 425 | 426 | * add base64 URLs support ([814b76c](https://github.com/Kikobeats/unavatar/commit/814b76ca3647e2f8c7cb334095c6f7cb840e0a7e)) 427 | 428 | ### 2.7.3 (2021-11-21) 429 | 430 | ### 2.7.2 (2021-11-20) 431 | 432 | ### 2.7.1 (2021-11-20) 433 | 434 | ## 2.7.0 (2021-10-24) 435 | 436 | 437 | ### Features 438 | 439 | * add google as provider ([7507977](https://github.com/Kikobeats/unavatar/commit/75079777297bec72fdfa2f197bc5413df36c6417)) 440 | 441 | ### 2.6.22 (2021-10-24) 442 | 443 | ### 2.6.21 (2021-10-10) 444 | 445 | ### 2.6.20 (2021-09-27) 446 | 447 | ### 2.6.19 (2021-09-27) 448 | 449 | ### 2.6.18 (2021-09-05) 450 | 451 | ### 2.6.17 (2021-09-02) 452 | 453 | ### 2.6.16 (2021-09-02) 454 | 455 | ### 2.6.15 (2021-08-27) 456 | 457 | ### 2.6.14 (2021-08-27) 458 | 459 | 460 | ### Bug Fixes 461 | 462 | * pass mongo uri connection ([83273e2](https://github.com/Kikobeats/unavatar/commit/83273e245652d30e79fa250b00fda78e434a8925)) 463 | 464 | ### 2.6.13 (2021-08-27) 465 | 466 | ### 2.6.12 (2021-08-26) 467 | 468 | ### 2.6.11 (2021-08-19) 469 | 470 | ### 2.6.10 (2021-08-19) 471 | 472 | ### 2.6.9 (2021-08-19) 473 | 474 | ### 2.6.8 (2021-07-17) 475 | 476 | ### 2.6.7 (2021-07-17) 477 | 478 | ### 2.6.6 (2021-07-17) 479 | 480 | ### 2.6.5 (2021-07-16) 481 | 482 | ### 2.6.4 (2021-07-16) 483 | 484 | ### 2.6.3 (2021-07-16) 485 | 486 | 487 | ### Bug Fixes 488 | 489 | * ensure optimized URL is reachabel ([0db0b1a](https://github.com/Kikobeats/unavatar/commit/0db0b1acc74ecce0463c0f808251403cfa279965)) 490 | 491 | ### 2.6.2 (2021-07-16) 492 | 493 | 494 | ### Bug Fixes 495 | 496 | * avoid unnecessary avatar URL calls ([a8c7bef](https://github.com/Kikobeats/unavatar/commit/a8c7bef3b7842019033e999844a367c70ddb0ac5)) 497 | 498 | ### 2.6.1 (2021-07-16) 499 | 500 | ## 2.6.0 (2021-07-16) 501 | 502 | 503 | ### Features 504 | 505 | * use a image optimizer service ([bda2e68](https://github.com/Kikobeats/unavatar/commit/bda2e680e508f50373371cfade0838a2a2f6d817)) 506 | 507 | ### 2.5.10 (2021-07-13) 508 | 509 | ### 2.5.9 (2021-07-13) 510 | 511 | ### 2.5.8 (2021-07-12) 512 | 513 | ### 2.5.7 (2021-07-12) 514 | 515 | ### 2.5.6 (2021-07-12) 516 | 517 | 518 | ### Bug Fixes 519 | 520 | * linter ([fc8e3d0](https://github.com/Kikobeats/unavatar/commit/fc8e3d0bfabdf800c412469a9f88d70c4883e645)) 521 | 522 | ### 2.5.5 (2021-07-09) 523 | 524 | ### 2.5.4 (2021-07-09) 525 | 526 | 527 | ### Bug Fixes 528 | 529 | * use microlink for getting twitter avatar ([3f6569c](https://github.com/Kikobeats/unavatar/commit/3f6569c4acd50103d3f012260d4cf5088ea379f0)), closes [#121](https://github.com/Kikobeats/unavatar/issues/121) 530 | 531 | ### 2.5.3 (2021-07-09) 532 | 533 | ### 2.5.2 (2021-07-09) 534 | 535 | ### [2.5.1](https://github.com/Kikobeats/unavatar/compare/v2.5.0...v2.5.1) (2021-06-29) 536 | 537 | ## [2.5.0](https://github.com/Kikobeats/unavatar/compare/v2.4.2...v2.5.0) (2021-04-23) 538 | 539 | 540 | ### Features 541 | 542 | * improve substack implementation ([#129](https://github.com/Kikobeats/unavatar/issues/129)) ([d8b532c](https://github.com/Kikobeats/unavatar/commit/d8b532c927e4ab746b36741b8d570cfad0402247)) 543 | 544 | 545 | ### Bug Fixes 546 | 547 | * interface ([005b66e](https://github.com/Kikobeats/unavatar/commit/005b66e43419b7b9effccdf8518b4183d1be9f89)) 548 | 549 | ### [2.4.2](https://github.com/Kikobeats/unavatar/compare/v2.4.1...v2.4.2) (2021-01-18) 550 | 551 | ### [2.4.1](https://github.com/Kikobeats/unavatar/compare/v2.4.0...v2.4.1) (2020-12-17) 552 | 553 | ## [2.4.0](https://github.com/Kikobeats/unavatar/compare/v2.2.29...v2.4.0) (2020-12-17) 554 | 555 | 556 | ### Features 557 | 558 | * add domain fallback/favicon DuckDuckGo provider ([#120](https://github.com/Kikobeats/unavatar/issues/120)) ([d441229](https://github.com/Kikobeats/unavatar/commit/d4412293d1d381acf36ed90e47ce8c60acfcd09a)) 559 | 560 | ## [2.3.0](https://github.com/Kikobeats/unavatar/compare/v2.2.29...v2.3.0) (2020-12-14) 561 | 562 | 563 | ### Features 564 | 565 | * add domain fallback/favicon DuckDuckGo provider ([#120](https://github.com/Kikobeats/unavatar/issues/120)) ([d441229](https://github.com/Kikobeats/unavatar/commit/d4412293d1d381acf36ed90e47ce8c60acfcd09a)) 566 | 567 | ### [2.2.29](https://github.com/Kikobeats/unavatar/compare/v2.2.28...v2.2.29) (2020-12-08) 568 | 569 | 570 | ### Bug Fixes 571 | 572 | * instagram image fetch ([#118](https://github.com/Kikobeats/unavatar/issues/118)) ([167037d](https://github.com/Kikobeats/unavatar/commit/167037d4c8d4b7f627466fb48b9a0b1a5df82f9b)) 573 | * linter ([65f6306](https://github.com/Kikobeats/unavatar/commit/65f630686695b8d72c132a02c5adb934f7c5c47c)) 574 | 575 | ### [2.2.28](https://github.com/Kikobeats/unavatar/compare/v2.2.27...v2.2.28) (2020-12-07) 576 | 577 | 578 | ### Bug Fixes 579 | 580 | * instagram provider ([#117](https://github.com/Kikobeats/unavatar/issues/117)) ([387815d](https://github.com/Kikobeats/unavatar/commit/387815d8d9e8b88ffe90bb21e155b41f33b072b7)) 581 | 582 | ### [2.2.27](https://github.com/Kikobeats/unavatar/compare/v2.2.26...v2.2.27) (2020-08-15) 583 | 584 | ### [2.2.26](https://github.com/Kikobeats/unavatar/compare/v2.2.25...v2.2.26) (2020-08-14) 585 | 586 | ### [2.2.25](https://github.com/Kikobeats/unavatar/compare/v2.2.24...v2.2.25) (2020-07-19) 587 | 588 | ### [2.2.24](https://github.com/Kikobeats/unavatar/compare/v2.2.23...v2.2.24) (2020-07-19) 589 | 590 | ### [2.2.23](https://github.com/Kikobeats/unavatar/compare/v2.2.22...v2.2.23) (2020-07-19) 591 | 592 | ### [2.2.22](https://github.com/Kikobeats/unavatar/compare/v2.2.21...v2.2.22) (2020-06-28) 593 | 594 | ### [2.2.21](https://github.com/Kikobeats/unavatar/compare/v2.2.20...v2.2.21) (2020-06-09) 595 | 596 | ### [2.2.20](https://github.com/Kikobeats/unavatar/compare/v2.2.19...v2.2.20) (2020-06-09) 597 | 598 | ### [2.2.19](https://github.com/Kikobeats/unavatar/compare/v2.2.18...v2.2.19) (2020-06-09) 599 | 600 | ### [2.2.18](https://github.com/Kikobeats/unavatar/compare/v2.2.17...v2.2.18) (2020-06-07) 601 | 602 | ### [2.2.17](https://github.com/Kikobeats/unavatar/compare/v2.2.16...v2.2.17) (2020-05-28) 603 | 604 | ### [2.2.16](https://github.com/Kikobeats/unavatar/compare/v2.2.15...v2.2.16) (2020-05-16) 605 | 606 | ### [2.2.15](https://github.com/Kikobeats/unavatar/compare/v2.2.14...v2.2.15) (2020-05-12) 607 | 608 | ### [2.2.14](https://github.com/Kikobeats/unavatar/compare/v2.2.13...v2.2.14) (2020-05-04) 609 | 610 | ### [2.2.13](https://github.com/Kikobeats/unavatar/compare/v2.2.12...v2.2.13) (2020-03-28) 611 | 612 | ### [2.2.12](https://github.com/Kikobeats/unavatar/compare/v2.2.11...v2.2.12) (2020-03-25) 613 | 614 | ### [2.2.11](https://github.com/Kikobeats/unavatar/compare/v2.2.10...v2.2.11) (2020-02-04) 615 | 616 | ### [2.2.10](https://github.com/Kikobeats/unavatar/compare/v2.2.9...v2.2.10) (2020-02-03) 617 | 618 | ### [2.2.9](https://github.com/Kikobeats/unavatar/compare/v2.2.8...v2.2.9) (2020-01-31) 619 | 620 | ### [2.2.8](https://github.com/Kikobeats/unavatar/compare/v2.2.7...v2.2.8) (2020-01-28) 621 | 622 | ### [2.2.7](https://github.com/Kikobeats/unavatar/compare/v2.2.6...v2.2.7) (2020-01-27) 623 | 624 | ### [2.2.6](https://github.com/Kikobeats/unavatar/compare/v2.2.5...v2.2.6) (2020-01-27) 625 | 626 | 627 | ### Bug Fixes 628 | 629 | * gravatar respects default avatar ([fecfebc](https://github.com/Kikobeats/unavatar/commit/fecfebcaa5e02f909de4da8a2cf904e105023adf)), closes [#66](https://github.com/Kikobeats/unavatar/issues/66) 630 | 631 | ### [2.2.5](https://github.com/Kikobeats/unavatar/compare/v2.2.4...v2.2.5) (2020-01-23) 632 | 633 | ### [2.2.4](https://github.com/Kikobeats/unavatar/compare/v2.2.3...v2.2.4) (2020-01-20) 634 | 635 | ### [2.2.3](https://github.com/Kikobeats/unavatar/compare/v2.2.2...v2.2.3) (2020-01-14) 636 | 637 | ### [2.2.2](https://github.com/Kikobeats/unavatar/compare/v2.2.1...v2.2.2) (2020-01-14) 638 | 639 | ### [2.2.1](https://github.com/Kikobeats/unavatar/compare/v2.2.0...v2.2.1) (2020-01-14) 640 | 641 | ## [2.2.0](https://github.com/Kikobeats/unavatar/compare/v2.1.3...v2.2.0) (2019-11-01) 642 | 643 | 644 | ### Features 645 | 646 | * add gitlab support ([8fc7ed0](https://github.com/Kikobeats/unavatar/commit/8fc7ed07263baed6e9dcf8340511e1bed4c9525c)), closes [#63](https://github.com/Kikobeats/unavatar/issues/63) 647 | 648 | ### [2.1.3](https://github.com/Kikobeats/unavatar/compare/v2.1.2...v2.1.3) (2019-10-31) 649 | 650 | ### [2.1.2](https://github.com/Kikobeats/unavatar/compare/v2.1.1...v2.1.2) (2019-10-11) 651 | 652 | ### [2.1.1](https://github.com/Kikobeats/unavatar/compare/v2.1.0...v2.1.1) (2019-10-10) 653 | 654 | ## [2.1.0](https://github.com/Kikobeats/unavatar/compare/v2.0.6...v2.1.0) (2019-10-10) 655 | 656 | 657 | ### Features 658 | 659 | * telegram provider ([#61](https://github.com/Kikobeats/unavatar/issues/61)) ([2332e05](https://github.com/Kikobeats/unavatar/commit/2332e053eaf56b3934d056d09142f1b591877e04)) 660 | 661 | ### [2.0.6](https://github.com/Kikobeats/unavatar/compare/v2.0.5...v2.0.6) (2019-08-20) 662 | 663 | ### [2.0.5](https://github.com/Kikobeats/unavatar/compare/v2.0.4...v2.0.5) (2019-08-20) 664 | 665 | ### [2.0.4](https://github.com/Kikobeats/unavatar/compare/v2.0.3...v2.0.4) (2019-08-15) 666 | 667 | ### [2.0.3](https://github.com/Kikobeats/unavatar/compare/v2.0.2...v2.0.3) (2019-08-15) 668 | 669 | ### [2.0.2](https://github.com/Kikobeats/unavatar/compare/v2.0.1...v2.0.2) (2019-08-14) 670 | 671 | ### [2.0.1](https://github.com/Kikobeats/unavatar/compare/v2.0.0...v2.0.1) (2019-08-14) 672 | 673 | ## [2.0.0](https://github.com/Kikobeats/unavatar/compare/v1.2.28...v2.0.0) (2019-08-11) 674 | 675 | ### [1.2.28](https://github.com/Kikobeats/unavatar/compare/v1.2.27...v1.2.28) (2019-08-11) 676 | 677 | ### [1.2.27](https://github.com/Kikobeats/unavatar/compare/v1.2.26...v1.2.27) (2019-06-28) 678 | 679 | 680 | ### Build System 681 | 682 | * remove github icon ([d4e6970](https://github.com/Kikobeats/unavatar/commit/d4e6970)) 683 | 684 | 685 | 686 | ### [1.2.26](https://github.com/Kikobeats/unavatar/compare/v1.2.24...v1.2.26) (2019-06-28) 687 | 688 | 689 | ### Bug Fixes 690 | 691 | * typo ([80e6590](https://github.com/Kikobeats/unavatar/commit/80e6590)) 692 | 693 | 694 | ### Build System 695 | 696 | * migrate to gulp v4 ([a1e0eb4](https://github.com/Kikobeats/unavatar/commit/a1e0eb4)) 697 | * update travis ([3a792b9](https://github.com/Kikobeats/unavatar/commit/3a792b9)) 698 | 699 | 700 | 701 | ### [1.2.25](https://github.com/Kikobeats/unavatar/compare/v1.2.24...v1.2.25) (2019-06-19) 702 | 703 | 704 | ### Bug Fixes 705 | 706 | * typo ([80e6590](https://github.com/Kikobeats/unavatar/commit/80e6590)) 707 | 708 | 709 | ### Build System 710 | 711 | * update travis ([3a792b9](https://github.com/Kikobeats/unavatar/commit/3a792b9)) 712 | 713 | 714 | 715 | ### [1.2.24](https://github.com/Kikobeats/unavatar/compare/v1.2.23...v1.2.24) (2019-06-09) 716 | 717 | 718 | ### Bug Fixes 719 | 720 | * github icon ([100e34c](https://github.com/Kikobeats/unavatar/commit/100e34c)) 721 | 722 | 723 | 724 | ### [1.2.23](https://github.com/Kikobeats/unavatar/compare/v1.2.22...v1.2.23) (2019-06-09) 725 | 726 | 727 | ### Bug Fixes 728 | 729 | * setup clearbit fallback properly ([da0464d](https://github.com/Kikobeats/unavatar/commit/da0464d)) 730 | 731 | 732 | 733 | ### [1.2.22](https://github.com/Kikobeats/unavatar/compare/v1.2.21...v1.2.22) (2019-05-28) 734 | 735 | 736 | 737 | ### [1.2.21](https://github.com/Kikobeats/unavatar/compare/v1.2.20...v1.2.21) (2019-05-20) 738 | 739 | 740 | ### Build System 741 | 742 | * change git-authors-cli position ([efd9908](https://github.com/Kikobeats/unavatar/commit/efd9908)) 743 | 744 | 745 | 746 | ### [1.2.20](https://github.com/Kikobeats/unavatar/compare/v1.2.19...v1.2.20) (2019-05-20) 747 | 748 | 749 | 750 | ### [1.2.19](https://github.com/Kikobeats/unavatar/compare/v1.2.18...v1.2.19) (2019-05-17) 751 | 752 | 753 | 754 | ### [1.2.18](https://github.com/Kikobeats/unavatar/compare/v1.2.17...v1.2.18) (2019-05-13) 755 | 756 | 757 | 758 | ### [1.2.17](https://github.com/Kikobeats/unavatar/compare/v1.2.16...v1.2.17) (2019-05-05) 759 | 760 | 761 | 762 | ## [1.2.16](https://github.com/Kikobeats/unavatar/compare/v1.2.15...v1.2.16) (2019-05-05) 763 | 764 | 765 | 766 | ## [1.2.15](https://github.com/Kikobeats/unavatar/compare/v1.2.14...v1.2.15) (2019-05-03) 767 | 768 | 769 | 770 | ## [1.2.14](https://github.com/Kikobeats/unavatar/compare/v1.2.13...v1.2.14) (2019-04-22) 771 | 772 | 773 | 774 | ## [1.2.13](https://github.com/Kikobeats/unavatar/compare/v1.2.12...v1.2.13) (2019-04-21) 775 | 776 | 777 | 778 | 779 | ## [1.2.12](https://github.com/Kikobeats/unavatar/compare/v1.2.11...v1.2.12) (2019-04-06) 780 | 781 | 782 | 783 | 784 | ## [1.2.11](https://github.com/Kikobeats/unavatar/compare/v1.2.10...v1.2.11) (2019-04-06) 785 | 786 | 787 | ### Bug Fixes 788 | 789 | * **package:** update cacheable-response to version 1.4.0 ([#45](https://github.com/Kikobeats/unavatar/issues/45)) ([65ede42](https://github.com/Kikobeats/unavatar/commit/65ede42)) 790 | 791 | 792 | 793 | 794 | ## [1.2.10](https://github.com/Kikobeats/unavatar/compare/v1.2.9...v1.2.10) (2019-04-06) 795 | 796 | 797 | 798 | 799 | ## [1.2.9](https://github.com/Kikobeats/unavatar/compare/v1.2.8...v1.2.9) (2019-04-04) 800 | 801 | 802 | 803 | 804 | ## [1.2.8](https://github.com/Kikobeats/unavatar/compare/v1.2.7...v1.2.8) (2019-03-13) 805 | 806 | 807 | ### Bug Fixes 808 | 809 | * **package:** update p-timeout to version 3.0.0 ([#38](https://github.com/Kikobeats/unavatar/issues/38)) ([7201b1e](https://github.com/Kikobeats/unavatar/commit/7201b1e)) 810 | 811 | 812 | 813 | 814 | ## [1.2.7](https://github.com/Kikobeats/unavatar/compare/v1.2.6...v1.2.7) (2019-03-10) 815 | 816 | 817 | ### Bug Fixes 818 | 819 | * **package:** update jsdom to version 14.0.0 ([#36](https://github.com/Kikobeats/unavatar/issues/36)) ([95f02bf](https://github.com/Kikobeats/unavatar/commit/95f02bf)) 820 | 821 | 822 | 823 | 824 | ## [1.2.6](https://github.com/Kikobeats/unavatar/compare/v1.2.5...v1.2.6) (2019-03-04) 825 | 826 | 827 | 828 | 829 | ## [1.2.5](https://github.com/Kikobeats/unavatar/compare/v1.2.4...v1.2.5) (2019-02-23) 830 | 831 | 832 | 833 | 834 | ## [1.2.4](https://github.com/Kikobeats/unavatar/compare/v1.2.3...v1.2.4) (2019-01-17) 835 | 836 | 837 | 838 | 839 | ## [1.2.3](https://github.com/Kikobeats/unavatar/compare/v1.2.2...v1.2.3) (2018-12-19) 840 | 841 | 842 | 843 | 844 | ## [1.2.2](https://github.com/Kikobeats/unavatar/compare/v1.2.1...v1.2.2) (2018-12-12) 845 | 846 | 847 | ### Bug Fixes 848 | 849 | * **package:** update memoize-one to version 4.1.0 ([#27](https://github.com/Kikobeats/unavatar/issues/27)) ([e5d49fa](https://github.com/Kikobeats/unavatar/commit/e5d49fa)) 850 | 851 | 852 | 853 | 854 | ## [1.2.1](https://github.com/Kikobeats/unavatar/compare/v1.2.0...v1.2.1) (2018-12-11) 855 | 856 | 857 | 858 | 859 | # [1.2.0](https://github.com/Kikobeats/unavatar/compare/v1.1.0...v1.2.0) (2018-11-07) 860 | 861 | 862 | ### Features 863 | 864 | * add disable fallback ([9139375](https://github.com/Kikobeats/unavatar/commit/9139375)) 865 | 866 | 867 | 868 | 869 | # [1.1.0](https://github.com/Kikobeats/unavatar/compare/v1.0.4...v1.1.0) (2018-11-07) 870 | 871 | 872 | ### Features 873 | 874 | * add youtube support ([3706ce5](https://github.com/Kikobeats/unavatar/commit/3706ce5)), closes [#13](https://github.com/Kikobeats/unavatar/issues/13) 875 | 876 | 877 | 878 | 879 | ## [1.0.4](https://github.com/Kikobeats/unavatar/compare/v1.0.3...v1.0.4) (2018-11-05) 880 | 881 | 882 | 883 | 884 | ## [1.0.3](https://github.com/Kikobeats/unavatar/compare/v1.0.2...v1.0.3) (2018-10-30) 885 | 886 | 887 | ### Bug Fixes 888 | 889 | * **package:** update got to version 9.3.0 ([9f9ff96](https://github.com/Kikobeats/unavatar/commit/9f9ff96)) 890 | 891 | 892 | 893 | 894 | ## 1.0.2 (2018-10-30) 895 | 896 | 897 | 898 | 899 | ## 1.0.1 (2018-05-03) 900 | 901 | 902 | 903 | 904 | # 1.0.0 (2018-04-26) 905 | 906 | 907 | 908 | 909 | ## 1.0.1 (2018-05-03) 910 | 911 | * Fix return avatar url ([10264d1](https://github.com/Kikobeats/unavatar/commit/10264d1)) 912 | * Refactor ([7ac7e02](https://github.com/Kikobeats/unavatar/commit/7ac7e02)) 913 | * Remove publish step ([4e921b1](https://github.com/Kikobeats/unavatar/commit/4e921b1)) 914 | * Rename domain → clearbit ([f438bb5](https://github.com/Kikobeats/unavatar/commit/f438bb5)) 915 | 916 | 917 | 918 | 919 | # 1.0.0 (2018-04-26) 920 | 921 | * Add caching strategy ([aaf3881](https://github.com/Kikobeats/unavatar/commit/aaf3881)) 922 | * Add cheerio dep ([22415ad](https://github.com/Kikobeats/unavatar/commit/22415ad)) 923 | * Add domain support ([2c160af](https://github.com/Kikobeats/unavatar/commit/2c160af)), closes [#3](https://github.com/Kikobeats/unavatar/issues/3) 924 | * Add facebook support ([223f898](https://github.com/Kikobeats/unavatar/commit/223f898)) 925 | * Add fallback support ([aea735f](https://github.com/Kikobeats/unavatar/commit/aea735f)), closes [#2](https://github.com/Kikobeats/unavatar/issues/2) 926 | * Add in memory cache ([a2bc487](https://github.com/Kikobeats/unavatar/commit/a2bc487)) 927 | * Add instagram support ([e0780c0](https://github.com/Kikobeats/unavatar/commit/e0780c0)) 928 | * Add logo banner ([2018bb2](https://github.com/Kikobeats/unavatar/commit/2018bb2)) 929 | * Add site ([167bbbe](https://github.com/Kikobeats/unavatar/commit/167bbbe)) 930 | * Add travis integration ([0adb091](https://github.com/Kikobeats/unavatar/commit/0adb091)) 931 | * Better example ([2965408](https://github.com/Kikobeats/unavatar/commit/2965408)) 932 | * Ensure GitHub url exists ([5e18aa8](https://github.com/Kikobeats/unavatar/commit/5e18aa8)) 933 | * Ensure url to resolve is absolute ([040567f](https://github.com/Kikobeats/unavatar/commit/040567f)) 934 | * First commit ([83372e5](https://github.com/Kikobeats/unavatar/commit/83372e5)) 935 | * Fix gravatar url ([b1ee418](https://github.com/Kikobeats/unavatar/commit/b1ee418)), closes [#1](https://github.com/Kikobeats/unavatar/issues/1) 936 | * Fix link ([dbdbe4a](https://github.com/Kikobeats/unavatar/commit/dbdbe4a)) 937 | * Get Instagram full size avatar ([da04a11](https://github.com/Kikobeats/unavatar/commit/da04a11)), closes [#5](https://github.com/Kikobeats/unavatar/issues/5) 938 | * Handle stream errors ([56cf241](https://github.com/Kikobeats/unavatar/commit/56cf241)) 939 | * Handle timeouts ([01e07ed](https://github.com/Kikobeats/unavatar/commit/01e07ed)) 940 | * Improve deploy process ([b848588](https://github.com/Kikobeats/unavatar/commit/b848588)) 941 | * Only add urls that exists ([9ae0b3b](https://github.com/Kikobeats/unavatar/commit/9ae0b3b)) 942 | * Refactor ([42d8674](https://github.com/Kikobeats/unavatar/commit/42d8674)) 943 | * Refactor ([fd54287](https://github.com/Kikobeats/unavatar/commit/fd54287)) 944 | * Rename ([8ea2e4f](https://github.com/Kikobeats/unavatar/commit/8ea2e4f)) 945 | * Set the private property to avoid NPM to try to pulish the package ([0e5220f](https://github.com/Kikobeats/unavatar/commit/0e5220f)) 946 | * Tweaks ([c356313](https://github.com/Kikobeats/unavatar/commit/c356313)) 947 | * Update images ([a11e07b](https://github.com/Kikobeats/unavatar/commit/a11e07b)) 948 | * Update README.md ([80e6afa](https://github.com/Kikobeats/unavatar/commit/80e6afa)) 949 | * User better twitter strategy ([a680cf9](https://github.com/Kikobeats/unavatar/commit/a680cf9)) 950 | * Wrap into try/catch ([46a40e1](https://github.com/Kikobeats/unavatar/commit/46a40e1)) 951 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM browserless/base:latest 2 | 3 | # Application parameters and variables 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | ENV APP_DIR=/home/node/app 6 | ENV LANG="C.UTF-8" 7 | #ENV CC=clang 8 | #ENV CXX=clang++ 9 | #ENV NODE_OPTIONS='--no-deprecation' 10 | 11 | # install node20 12 | RUN . $NVM_DIR/nvm.sh && nvm_dir="${NVM_DIR:-~/.nvm}" && nvm unload && rm -rf "$nvm_dir" 13 | RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs 14 | 15 | # install pnpm 16 | RUN npm install -g pnpm 17 | 18 | WORKDIR $APP_DIR 19 | 20 | COPY package.json .npmrc .puppeteerrc.js ./ 21 | RUN pnpm install --prod 22 | 23 | COPY . . 24 | 25 | USER blessuser 26 | 27 | EXPOSE 3000 28 | 29 | CMD ["node", "src/server.js"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2018 Kiko Beats (kikobeats.com) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | public/README.md -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unavatar", 3 | "description": "Get unified user avatar.", 4 | "repository": "https://github.com/Kikobeats/unavatar", 5 | "keywords": [ 6 | "avatar", 7 | "clearbit", 8 | "microlink", 9 | "domain", 10 | "email", 11 | "github", 12 | "gravatar", 13 | "instagram", 14 | "telegram", 15 | "twitter", 16 | "youtube" 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | app = "unavatar" 2 | kill_signal = "SIGTERM" 3 | kill_timeout = 5 4 | 5 | [http_service] 6 | internal_port = 3000 7 | force_https = false 8 | auto_stop_machines = "stop" 9 | auto_start_machines = true 10 | min_machines_running = 0 11 | 12 | [http_service.concurrency] 13 | type = "requests" 14 | soft_limit = 2 15 | hard_limit = 4 16 | 17 | [[restart]] 18 | policy = "on-failure" 19 | max_retries = 5 20 | 21 | ## some useful fly commands 22 | # fly status # to see how many machines are running 23 | # fly scale vm shared-cpu-2x --vm-memory=1024 # upgrade machines 24 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const postcss = require('gulp-postcss') 4 | const concat = require('gulp-concat') 5 | const uglify = require('gulp-uglify') 6 | const gulp = require('gulp') 7 | 8 | const src = { 9 | css: ['public/css/style.css'], 10 | js: ['public/js/main.js'] 11 | } 12 | 13 | const dist = { 14 | path: 'public', 15 | name: { 16 | css: 'css/style', 17 | js: 'js/main' 18 | } 19 | } 20 | 21 | const styles = () => 22 | gulp 23 | .src(src.css) 24 | .pipe(concat(`${dist.name.css}.min.css`)) 25 | .pipe( 26 | postcss([ 27 | require('postcss-focus'), 28 | require('cssnano')({ 29 | preset: require('cssnano-preset-advanced') 30 | }) 31 | ]) 32 | ) 33 | .pipe(gulp.dest(dist.path)) 34 | 35 | const scripts = () => 36 | gulp 37 | .src(src.js) 38 | .pipe(concat(`${dist.name.js}.min.js`)) 39 | .pipe(uglify()) 40 | .pipe(gulp.dest(dist.path)) 41 | 42 | const build = gulp.parallel(styles, scripts) 43 | 44 | function watch () { 45 | gulp.watch(src.css, styles) 46 | gulp.watch(src.js, scripts) 47 | } 48 | 49 | module.exports.default = gulp.series(build, watch) 50 | module.exports.build = build 51 | module.exports.watch = watch 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unavatar", 3 | "description": "Get unified user avatar from social networks, including Instagram, SoundCloud, Telegram, Twitter, YouTube & more.", 4 | "homepage": "https://unavatar.io", 5 | "version": "3.7.31", 6 | "main": "src/index.js", 7 | "bin": { 8 | "unavatar": "bin/server" 9 | }, 10 | "author": { 11 | "email": "josefrancisco.verdu@gmail.com", 12 | "name": "Kiko Beats", 13 | "url": "https://kikobeats.com" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Ben Croker", 18 | "email": "57572400+bencroker@users.noreply.github.com" 19 | }, 20 | { 21 | "name": "David Refoua", 22 | "email": "David@Refoua.me" 23 | }, 24 | { 25 | "name": "Alexander Schlindwein", 26 | "email": "alexander.schlindwein@hotmail.de" 27 | }, 28 | { 29 | "name": "Zadkiel", 30 | "email": "hello@zadkiel.fr" 31 | }, 32 | { 33 | "name": "Omid Nikrah", 34 | "email": "omidnikrah@gmail.com" 35 | }, 36 | { 37 | "name": "Nicolas Hedger", 38 | "email": "649677+nhedger@users.noreply.github.com" 39 | }, 40 | { 41 | "name": "Angel M De Miguel", 42 | "email": "angel@bitnami.com" 43 | }, 44 | { 45 | "name": "Tom Witkowski", 46 | "email": "dev.gummibeer@gmail.com" 47 | }, 48 | { 49 | "name": "Terence Eden", 50 | "email": "edent@users.noreply.github.com" 51 | }, 52 | { 53 | "name": "Hameed Rahamathullah", 54 | "email": "hameedraha@gmail.com" 55 | }, 56 | { 57 | "name": "Jahir Fiquitiva", 58 | "email": "jahir.fiquitiva@gmail.com" 59 | }, 60 | { 61 | "name": "Mads Hougesen", 62 | "email": "mads@mhouge.dk" 63 | }, 64 | { 65 | "name": "ぶーと / Yoshiaki Ueda", 66 | "email": "oh@bootjp.me" 67 | }, 68 | { 69 | "name": "Reed Jones", 70 | "email": "reedjones@reedjones.com" 71 | }, 72 | { 73 | "name": "Rodrigo Reis", 74 | "email": "rodrigoreis22@yahoo.com.br" 75 | } 76 | ], 77 | "repository": { 78 | "type": "git", 79 | "url": "git+https://github.com/Kikobeats/unavatar.git" 80 | }, 81 | "bugs": { 82 | "url": "https://github.com/Kikobeats/unavatar/issues" 83 | }, 84 | "keywords": [ 85 | "avatar", 86 | "clearbit", 87 | "domain", 88 | "email", 89 | "github", 90 | "gravatar", 91 | "instagram", 92 | "microlink", 93 | "telegram", 94 | "twitter", 95 | "youtube" 96 | ], 97 | "dependencies": { 98 | "@keyvhq/compress": "~2.1.7", 99 | "@keyvhq/core": "~2.1.7", 100 | "@keyvhq/multi": "~2.1.7", 101 | "@keyvhq/redis": "~2.1.7", 102 | "@kikobeats/time-span": "~1.0.5", 103 | "@microlink/mql": "~0.13.14", 104 | "@microlink/ping-url": "~1.4.15", 105 | "@microlink/ua": "~1.2.5", 106 | "async-memoize-one": "~1.1.8", 107 | "browserless": "~10.7.5", 108 | "cacheable-lookup": "~6.1.0", 109 | "cacheable-response": "~2.10.2", 110 | "cheerio": "~1.0.0", 111 | "cors": "~2.8.5", 112 | "data-uri-regex": "~0.1.4", 113 | "data-uri-to-buffer": "~5.0.1", 114 | "debug-logfmt": "~1.2.3", 115 | "frequency-counter": "~1.0.1", 116 | "got": "~11.8.6", 117 | "helmet": "~8.1.0", 118 | "html-get": "~2.21.1", 119 | "http-compression": "~1.1.1", 120 | "https-tls": "~1.0.23", 121 | "ioredis": "~5.6.0", 122 | "is-absolute-url": "~3.0.3", 123 | "is-email-like": "~1.0.0", 124 | "is-url-http": "~2.3.9", 125 | "lodash": "~4.17.21", 126 | "ms": "~2.1.3", 127 | "null-prototype-object": "~1.2.0", 128 | "on-finished": "~2.4.1", 129 | "p-any": "~3.0.0", 130 | "p-cancelable": "2.1.1", 131 | "p-reflect": "~2.1.0", 132 | "p-timeout": "~4.1.0", 133 | "puppeteer": "~24.10.0", 134 | "rate-limiter-flexible": "~7.1.0", 135 | "router-http": "~1.0.10", 136 | "send-http": "~1.0.6", 137 | "serve-static": "~2.2.0", 138 | "srcset": "~4.0.0", 139 | "tangerine": "~1.6.0", 140 | "top-crawler-agents": "~1.0.28", 141 | "top-user-agents": "~2.1.39", 142 | "unique-random-array": "~2.0.0", 143 | "url-regex": "~5.0.0" 144 | }, 145 | "devDependencies": { 146 | "@commitlint/cli": "latest", 147 | "@commitlint/config-conventional": "latest", 148 | "@ksmithut/prettier-standard": "latest", 149 | "async-listen": "latest", 150 | "ava": "5", 151 | "browser-sync": "latest", 152 | "concurrently": "latest", 153 | "cssnano": "latest", 154 | "cssnano-preset-advanced": "latest", 155 | "finepack": "latest", 156 | "git-authors-cli": "latest", 157 | "github-generate-release": "latest", 158 | "gulp": "5", 159 | "gulp-autoprefixer": "latest", 160 | "gulp-concat": "latest", 161 | "gulp-postcss": "latest", 162 | "gulp-uglify": "latest", 163 | "nano-staged": "latest", 164 | "postcss": "latest", 165 | "postcss-focus": "latest", 166 | "simple-git-hooks": "latest", 167 | "standard": "latest", 168 | "standard-markdown": "latest", 169 | "standard-version": "latest", 170 | "untracked": "latest" 171 | }, 172 | "engines": { 173 | "node": "22" 174 | }, 175 | "files": [ 176 | "bin", 177 | "gulpfile.js", 178 | "public", 179 | "src" 180 | ], 181 | "scripts": { 182 | "build": "gulp build && untracked > .vercelignore", 183 | "clean": "rm -rf node_modules", 184 | "contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true", 185 | "dev": "TZ=UTC watchexec --clear=clear --on-busy-update=restart 'fkill \"Google Chrome for Testing\" --silent && NODE_OPTIONS='--no-deprecation' node src/server.js'", 186 | "dev:docker": "docker build --platform linux/arm64/v8 -t unavatar . && docker run --platform linux/arm64/v8 --name unavatar -e NODE_ENV=staging -p 3000:3000 --rm unavatar", 187 | "dev:docs": "concurrently \"npm run dev:docs:server\" \"npm run dev:docs:src\"", 188 | "dev:docs:server": "browser-sync start public --server public --files \"index.html, README.md, public/**/*.(css|js)\"", 189 | "dev:docs:src": "gulp", 190 | "lint": "standard-markdown && standard", 191 | "postrelease": "npm run release:tags && npm run release:github", 192 | "pretest": "npm run lint", 193 | "pretty": "prettier-standard index.js {core,test,bin}/**/*.js --single-quote", 194 | "release": "standard-version -a", 195 | "release:github": "github-generate-release", 196 | "release:tags": "git push --follow-tags origin HEAD:master", 197 | "test": "ava" 198 | }, 199 | "private": true, 200 | "license": "MIT", 201 | "ava": { 202 | "workerThreads": false, 203 | "serial": true, 204 | "files": [ 205 | "test/**/*.js", 206 | "!test/helpers.js" 207 | ], 208 | "timeout": "1m" 209 | }, 210 | "commitlint": { 211 | "extends": [ 212 | "@commitlint/config-conventional" 213 | ], 214 | "rules": { 215 | "body-max-line-length": [ 216 | 0 217 | ] 218 | } 219 | }, 220 | "nano-staged": { 221 | "*.js": [ 222 | "prettier-standard", 223 | "standard --fix" 224 | ], 225 | "*.md": [ 226 | "standard-markdown" 227 | ], 228 | "package.json": [ 229 | "finepack" 230 | ] 231 | }, 232 | "pnpm": { 233 | "neverBuiltDependencies": [] 234 | }, 235 | "simple-git-hooks": { 236 | "commit-msg": "npx commitlint --edit", 237 | "pre-commit": "npx nano-staged" 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /public/README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://unavatar.io/banner.png ':id=banner') 2 | 3 | Welcome to **unavatar.io**, the ultimate avatar service that offers everything you need to easily retrieve user avatars: 4 | 5 | - **Versatile**: A wide range of platforms and services including Facebook, Instagram, YouTube, Twitter, Gravatar, etc., meaning you can rule all of them just querying against unavatar. 6 | 7 | - **Speed**: Designed to be fast and efficient, all requests are being cached and delivered +200 global datacenters, allowing you to consume avatars instantly, counting more than 20 millions requests per month. 8 | 9 | - **Optimize**: All the images are not only compressed on-the-fly to reduce their size and save bandwith, but also optimized to maintain a high-quality ratio. They are ready for immediate use, enhancing the overall optimization of your website or application. 10 | 11 | - **Integration**: The service seamlessly incorporates into your current applications or websites with ease. We offer straightforward documentation and comprehensive support to ensure a quick and effortless onboarding experience. 12 | 13 | In summary, **unavatar.io** provides versatility, speed, optimization, and effortless integration, making it the ultimate avatar retrieval service. 14 | 15 | ## Quick start 16 | 17 | The service is a single endpoint exposed in **unavatar.io** that can resolve: 18 | 19 | - an **email**: https://unavatar.io/sindresorhus@gmail.com 20 | - an **username**: https://unavatar.io/kikobeats 21 | - a **domain**: https://unavatar.io/reddit.com 22 | 23 | So, no matter what type of query you use, **unavatar.io** has you covered. You can read more about that in [providers](#providers). 24 | 25 | ## Query parameters 26 | 27 | ### TTL 28 | 29 | Type: `number`|`string`
30 | Default: `'24h'`
31 | Range: from `'1h'` to `'28d'` 32 | 33 | It determines the maximum quantity of time an avatar is considered fresh. 34 | 35 | e.g., https://unavatar.io/kikobeats?ttl=1h 36 | 37 | When you look up for a user avatar for the very first time, the service will determine it and cache it respecting TTL value. 38 | 39 | The same resource will continue to be used until reach TTL expiration. After that, the resource will be computed, and cache as fresh, starting the cycle. 40 | 41 | ### Fallback 42 | 43 | Type: `string`|`boolean` 44 | 45 | When it can't be possible to get a user avatar, a fallback image is returned instead, and it can be personalized to fit better with your website or application style. 46 | 47 | You can get one from **boringavatars.com**: 48 | 49 | e.g., https://unavatar.io/github/37t?fallback=https://source.boringavatars.com/marble/120/1337_user?colors=264653r,2a9d8f,e9c46a,f4a261,e76f51 50 | 51 | or **avatar.vercel.sh**: 52 | 53 | e.g., https://unavatar.io/github/37t?fallback=https://avatar.vercel.sh/37t?size=400 54 | 55 | or a static image: 56 | 57 | e.g., https://unavatar.io/github/37t?fallback=https://avatars.githubusercontent.com/u/66378906?v=4 58 | 59 | or even a base64 encoded image. This allows you to return a transparent, base64 encoded 1x1 pixel GIF, which can be useful when you want to use your own background colour or image as a fallback. 60 | 61 | e.g., https://unavatar.io/github/37t?fallback=data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw== 62 | 63 | You can pass `fallback=false` to explicitly disable this behavior. In this case, a *404 Not Found* HTTP status code will returned when is not possible to get the user avatar. 64 | 65 | ### JSON 66 | 67 | The service returns media content by default. 68 | 69 | This is in this way to make easier consume the service from HTML markup. 70 | 71 | In case you want to get a JSON payload as response, just pass `json=true`: 72 | 73 | e.g., https://unavatar.io/kikobeats?json 74 | 75 | ## Limitations 76 | 77 | For preventing abusive usage, the service has associated a daily rate limit based on requests IP address. 78 | 79 | You can verify for your rate limit state checking the following headers in the response: 80 | 81 | - `x-rate-limit-limit`: The maximum number of requests that the consumer is permitted to make per minute. 82 | - `x-rate-limit-remaining`: The number of requests remaining in the current rate limit window. 83 | - `x-rate-limit-reset`: The time at which the current rate limit window resets in UTC epoch seconds. 84 | 85 | When you reach the API quota limit, you will experience HTTP 429 errors, meaning you need to wait until the API quota reset. If you need more quota, contact us. 86 | 87 | ## Providers 88 | 89 | With **unavatar.io**, you can retrieve user avatars based on an **email**, **domain**, or **username**. 90 | 91 | The providers are grouped based on which kind of input they can resolve. 92 | 93 | Based on that, a subset of providers will be used for resolving the user query, being the avatar resolved the fastest provider that resolve the query successfully. 94 | 95 | Alternatively, you can query for an individual provider. 96 | 97 | ### DeviantArt 98 | 99 | Type: `username` 100 | 101 | It resolves user avatar against **deviantart.com**. 102 | 103 | e.g., https://unavatar.io/deviantart/spyed 104 | 105 | ### Dribbble 106 | 107 | Type: `username` 108 | 109 | It resolves user avatar against **dribbble.com**. 110 | 111 | e.g., https://unavatar.io/dribbble/omidnikrah 112 | 113 | ### DuckDuckGo 114 | 115 | Type: `domain` 116 | 117 | It resolves user avatar using **duckduckgo.com**. 118 | 119 | e.g., https://unavatar.io/duckduckgo/gummibeer.dev 120 | 121 | ### GitHub 122 | 123 | Type: `username` 124 | 125 | It resolves user avatar against **github.com**. 126 | 127 | e.g., https://unavatar.io/github/mdo 128 | 129 | ### Google 130 | 131 | Type: `domain` 132 | 133 | It resolves user avatar using **google.com**. 134 | 135 | e.g., https://unavatar.io/google/netflix.com 136 | 137 | ### Gravatar 138 | 139 | Type: `email` 140 | 141 | It resolves user avatar against **gravatar.com**. 142 | 143 | e.g., https://unavatar.io/gravatar/sindresorhus@gmail.com 144 | 145 | 152 | 153 | ### Microlink 154 | 155 | Type: `domain` 156 | 157 | It resolves user avatar using **microlink.io**. 158 | 159 | e.g., https://unavatar.io/microlink/microlink.io 160 | 161 | ### OnlyFans 162 | 163 | It resolves user avatar using **onlyfans.com**. 164 | 165 | e.g., https://unavatar.io/onlyfans/amandaribas 166 | 167 | ### Read.cv 168 | 169 | Type: `username` 170 | 171 | It resolves user avatar against **read.cv**. 172 | 173 | e.g., https://unavatar.io/readcv/elenatorro 174 | 175 | 182 | 183 | ### SoundCloud 184 | 185 | Type: `username` 186 | 187 | It resolves user avatar against **soundcloud.com**. 188 | 189 | e.g., https://unavatar.io/soundcloud/gorillaz 190 | 191 | ### Substack 192 | 193 | Type: `username` 194 | 195 | It resolves user avatar against **substack.com**. 196 | 197 | e.g., https://unavatar.io/substack/bankless 198 | 199 | ### Telegram 200 | 201 | Type: `username` 202 | 203 | It resolves user avatar against **telegram.com**. 204 | 205 | e.g., https://unavatar.io/telegram/drsdavidsoft 206 | 207 | 214 | 215 | ### Twitch 216 | 217 | Type: `username` 218 | 219 | It resolves user avatar against **twitch.tv**. 220 | 221 | e.g., https://unavatar.io/twitch/midudev 222 | 223 | ### X/Twitter 224 | 225 | Type: `username` 226 | 227 | It resolves user avatar against **x.com**. 228 | 229 | e.g., https://unavatar.io/x/kikobeats 230 | 231 | ### YouTube 232 | 233 | Type: `username` 234 | 235 | It resolves user avatar against **youtube.com**. 236 | 237 | e.g., https://unavatar.io/youtube/casey 238 | -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/unavatar/7cf821ac3da162a33ca9b04a757144969c671967/public/banner.png -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* https://palx.jxnblk.com/3e54ff.css */ 2 | :root { 3 | --gray0: #f8f9fa; 4 | --gray1: #f1f3f5; 5 | --gray2: #e9ecef; 6 | --gray3: #dee2e6; 7 | --gray4: #ced4da; 8 | --gray5: #adb5bd; 9 | --gray6: #868e96; 10 | --gray7: #495057; 11 | --gray8: #343a40; 12 | --gray9: #212529; 13 | --gray10: #101214; 14 | --gray11: #010101; 15 | --gray12: #000; 16 | /* theme */ 17 | --primary-color: var(--gray10); 18 | --selection-color: #f9e4ac; 19 | --bg-color: #fff; 20 | --serif-font: 'Space Grotesk', sans-serif; 21 | --sans-serif-font: var(--serif-font); 22 | --code-font: Source Code Pro, Menlo, monospace; 23 | --codebox-border-color: var(--gray3); 24 | --codebox-color: var(--gray12); 25 | } 26 | 27 | ::selection { 28 | background: var(--selection-color); 29 | } 30 | 31 | ::-moz-selection { 32 | background: var(--selection-color); 33 | } 34 | * { 35 | -webkit-font-smoothing: antialiased; 36 | -webkit-overflow-scrolling: touch; 37 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 38 | -webkit-text-size-adjust: none; 39 | -webkit-touch-callout: none; 40 | box-sizing: border-box; 41 | } 42 | 43 | body, 44 | html { 45 | height: 100%; 46 | } 47 | 48 | body { 49 | -moz-osx-font-smoothing: grayscale; 50 | -webkit-font-smoothing: antialiased; 51 | color: var(--primary-color); 52 | font-family: var(--sans-serif-font); 53 | font-size: 16px; 54 | letter-spacing: 0; 55 | margin: 0; 56 | overflow-x: hidden; 57 | } 58 | 59 | /* github corner */ 60 | 61 | @keyframes b { 62 | 0%, 63 | 100% { 64 | transform: rotate(0); 65 | } 66 | 67 | 20%, 68 | 60% { 69 | transform: rotate(-25deg); 70 | } 71 | 72 | 40%, 73 | 80% { 74 | transform: rotate(10deg); 75 | } 76 | } 77 | 78 | a.github-corner { 79 | z-index: 1; 80 | } 81 | 82 | .github-corner svg { 83 | border-bottom: 0; 84 | position: fixed; 85 | right: 0; 86 | text-decoration: none; 87 | top: 0; 88 | z-index: 1; 89 | } 90 | 91 | .github-corner:hover svg .octo-arm { 92 | opacity: 1; 93 | animation: b 560ms ease-in-out; 94 | } 95 | 96 | .github-corner svg { 97 | color: white; 98 | fill: black; 99 | height: 80px; 100 | width: 80px; 101 | } 102 | 103 | main { 104 | display: block; 105 | position: relative; 106 | width: 100vw; 107 | height: 100%; 108 | z-index: 0; 109 | } 110 | 111 | .sidebar { 112 | font-family: var(--sans-serif-font); 113 | padding: 40px; 114 | width: 300px; 115 | transition: transform 0.25s ease-out; 116 | overflow: auto; 117 | height: 100vh; 118 | } 119 | 120 | .sidebar .sidebar-nav { 121 | line-height: 2em; 122 | padding-bottom: 40px; 123 | } 124 | 125 | .sidebar li { 126 | margin: 6px 0 6px 15px; 127 | } 128 | 129 | .sidebar ul { 130 | margin: 0; 131 | padding: 0; 132 | } 133 | 134 | .sidebar ul ul { 135 | margin-left: 15px; 136 | } 137 | 138 | .sidebar ul, 139 | .sidebar ul li { 140 | list-style: none; 141 | } 142 | 143 | .sidebar ul li a { 144 | display: block; 145 | color: inherit; 146 | font-size: 14px; 147 | text-decoration: none; 148 | } 149 | 150 | .sidebar ul li a:hover { 151 | text-decoration: underline; 152 | } 153 | 154 | .sidebar ul li.active > a { 155 | border-right: 2px solid var(--primary-color); 156 | font-weight: 700; 157 | } 158 | 159 | .sidebar::-webkit-scrollbar { 160 | width: 4px; 161 | } 162 | 163 | .sidebar::-webkit-scrollbar-thumb { 164 | background: transparent; 165 | border-radius: 4px; 166 | } 167 | 168 | .sidebar:hover::-webkit-scrollbar-thumb { 169 | background: hsla(0, 0%, 53%, 0.4); 170 | } 171 | 172 | .sidebar:hover::-webkit-scrollbar-track { 173 | background: hsla(0, 0%, 53%, 0.1); 174 | } 175 | 176 | body.sticky .sidebar, 177 | body.sticky .sidebar-toggle { 178 | position: fixed; 179 | } 180 | 181 | .content { 182 | padding-top: 60px; 183 | top: 0; 184 | right: 0; 185 | bottom: 0; 186 | left: 300px; 187 | position: absolute; 188 | transition: left 0.25s ease; 189 | } 190 | 191 | .markdown-section { 192 | margin: 0 auto; 193 | max-width: 800px; 194 | padding: 30px 15px 40px; 195 | position: relative; 196 | } 197 | 198 | .markdown-section>* { 199 | box-sizing: border-box; 200 | font-size: inherit; 201 | } 202 | 203 | .markdown-section> :first-child { 204 | margin-top: 0 !important; 205 | } 206 | 207 | @media print { 208 | .github-corner, 209 | .sidebar, 210 | .sidebar-toggle { 211 | display: none; 212 | } 213 | } 214 | 215 | @media screen and (max-width: 768px) { 216 | .github-corner, 217 | .sidebar { 218 | position: fixed; 219 | } 220 | 221 | .sidebar, 222 | .sidebar-toggle { 223 | display: none; 224 | } 225 | 226 | main { 227 | height: auto; 228 | overflow-x: hidden; 229 | } 230 | 231 | .content { 232 | left: 0; 233 | max-width: 100vw; 234 | position: static; 235 | padding-top: 20px; 236 | transition: transform 0.25s ease; 237 | } 238 | 239 | .github-corner:hover .octo-arm { 240 | animation: none; 241 | } 242 | } 243 | 244 | .sidebar, 245 | body { 246 | background-color: var(--bg-color); 247 | color: var(--primary-color); 248 | } 249 | 250 | .markdown-section :is(h1, h2, h3, h4, h5) { 251 | font-family: var(--serif-font); 252 | letter-spacing: -.016em; 253 | line-height: 1.25; 254 | } 255 | 256 | .markdown-section :is(h1, h2, h3, h4, h5) a:hover::after { 257 | content: ""; 258 | position: relative; 259 | width: 14px; 260 | height: 14px; 261 | margin-left: 6px; 262 | top: -2px; 263 | background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22feather%20feather-link-2%22%3E%3Cpath%20d%3D%22M15%207h3a5%205%200%200%201%205%205%205%205%200%200%201-5%205h-3m-6%200H6a5%205%200%200%201-5-5%205%205%200%200%201%205-5h3%22%3E%3C%2Fpath%3E%3Cline%20x1%3D%228%22%20y1%3D%2212%22%20x2%3D%2216%22%20y2%3D%2212%22%3E%3C%2Fline%3E%3C%2Fsvg%3E"); 264 | background-position: center; 265 | background-repeat: no-repeat; 266 | background-size: contain; 267 | display: inline-block; 268 | } 269 | 270 | .markdown-section a { 271 | color: var(--primary-color); 272 | text-decoration: none; 273 | } 274 | 275 | .markdown-section :is(li, p) a { 276 | text-decoration: none; 277 | border-bottom: 2px solid var(--gray3); 278 | } 279 | 280 | .markdown-section :is(p, li) a:hover { 281 | border-bottom: 2px solid var(--gray12); 282 | } 283 | 284 | .markdown-section p a[href^="http"]::after, 285 | .markdown-section p a[href^="https://"]::after, 286 | .markdown-section li a[href^="http"]::after, 287 | .markdown-section li a[href^="https://"]::after { 288 | content: ""; 289 | width: 11px; 290 | height: 11px; 291 | margin-left: 2px; 292 | top: 0; 293 | background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22feather%20feather-arrow-up-right%22%3E%3Cline%20x1%3D%227%22%20y1%3D%2217%22%20x2%3D%2217%22%20y2%3D%227%22%3E%3C%2Fline%3E%3Cpolyline%20points%3D%227%207%2017%207%2017%2017%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E"); 294 | background-position: center; 295 | background-repeat: no-repeat; 296 | background-size: contain; 297 | display: inline-block; 298 | } 299 | 300 | .markdown-section p a[href^="http"]:hover::after, 301 | .markdown-section li a[href^="http"]:hover::after, 302 | .markdown-section p a[href^="https://"]:hover::after, 303 | .markdown-section li a[href^="https://"]:hover::after { 304 | background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22feather%20feather-arrow-right%22%3E%3Cline%20x1%3D%225%22%20y1%3D%2212%22%20x2%3D%2219%22%20y2%3D%2212%22%3E%3C%2Fline%3E%3Cpolyline%20points%3D%2212%205%2019%2012%2012%2019%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E"); 305 | } 306 | 307 | .markdown-section h1 { 308 | font-size: 4rem; 309 | margin: 4rem 0 1rem; 310 | } 311 | 312 | .markdown-section h2 { 313 | font-size: 2.5rem; 314 | margin: 3.5rem 0 1rem; 315 | } 316 | 317 | .markdown-section h3 { 318 | font-size: 2rem; 319 | margin: 3rem 0 1rem; 320 | } 321 | 322 | .markdown-section h4 { 323 | font-size: 1.5rem; 324 | margin: 2.5rem 0 1rem; 325 | } 326 | 327 | .markdown-section h5 { 328 | font-size: 1rem; 329 | } 330 | 331 | .markdown-section p { 332 | margin: 1.5rem 0 0 0; 333 | } 334 | 335 | .markdown-section :is(p, ul) { 336 | letter-spacing: .004em; 337 | font-size: 17px; 338 | line-height: 1.6; 339 | } 340 | 341 | .markdown-section ul { 342 | padding-left: 32px; 343 | } 344 | 345 | .markdown-section ul li { 346 | margin-bottom: 1rem; 347 | } 348 | 349 | .markdown-section :is(li,p) code { 350 | background: var(--bg-color); 351 | border-radius: 3px; 352 | border: 1px solid var(--codebox-border-color); 353 | color: var(--codebox-color); 354 | font-family: var(--code-font); 355 | font-size: 0.75rem; 356 | margin: 0 2px; 357 | padding: 4px 5px; 358 | white-space: nowrap; 359 | } 360 | 361 | #banner { 362 | width: 60%; 363 | display: flex; 364 | margin: auto; 365 | } 366 | 367 | .app-name { 368 | display: flex; 369 | justify-content: center; 370 | } 371 | 372 | .app-name-link img { 373 | width: 32px; 374 | height: 32px; 375 | } 376 | -------------------------------------------------------------------------------- /public/css/style.min.css: -------------------------------------------------------------------------------- 1 | :root{--gray0:#f8f9fa;--gray1:#f1f3f5;--gray2:#e9ecef;--gray3:#dee2e6;--gray4:#ced4da;--gray5:#adb5bd;--gray6:#868e96;--gray7:#495057;--gray8:#343a40;--gray9:#212529;--gray10:#101214;--gray11:#010101;--gray12:#000;--primary-color:var(--gray10);--selection-color:#f9e4ac;--bg-color:#fff;--serif-font:"Space Grotesk",sans-serif;--sans-serif-font:var(--serif-font);--code-font:Source Code Pro,Menlo,monospace;--codebox-border-color:var(--gray3);--codebox-color:var(--gray12)}::selection{background:var(--selection-color)}::-moz-selection{background:var(--selection-color)}*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:var(--primary-color);font-family:var(--sans-serif-font);font-size:16px;letter-spacing:0;margin:0;overflow-x:hidden}@keyframes a{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}.github-corner svg{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover svg .octo-arm{animation:a .56s ease-in-out;opacity:1}.github-corner:focus-visible svg .octo-arm{animation:a .56s ease-in-out;opacity:1}.github-corner svg{color:#fff;fill:#000;height:80px;width:80px}main{display:block;height:100%;position:relative;width:100vw;z-index:0}.sidebar{font-family:var(--sans-serif-font);height:100vh;overflow:auto;padding:40px;transition:transform .25s ease-out;width:300px}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li{margin:6px 0 6px 15px}.sidebar ul{margin:0;padding:0}.sidebar ul ul{margin-left:15px}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{color:inherit;display:block;font-size:14px;text-decoration:none}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li a:focus-visible{text-decoration:underline}.sidebar ul li.active>a{border-right:2px solid var(--primary-color);font-weight:700}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53%,.4)}.sidebar:focus-visible::-webkit-scrollbar-thumb{background:hsla(0,0%,53%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53%,.1)}.sidebar:focus-visible::-webkit-scrollbar-track{background:hsla(0,0%,53%,.1)}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{bottom:0;left:300px;padding-top:60px;position:absolute;right:0;top:0;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:800px;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}@media print{.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar{position:fixed}.sidebar,.sidebar-toggle{display:none}main{height:auto;overflow-x:hidden}.content{left:0;max-width:100vw;padding-top:20px;position:static;transition:transform .25s ease}.github-corner:hover .octo-arm{animation:none}.github-corner:focus-visible .octo-arm{animation:none}}.sidebar,body{background-color:var(--bg-color);color:var(--primary-color)}.markdown-section :is(h1,h2,h3,h4,h5){font-family:var(--serif-font);letter-spacing:-.016em;line-height:1.25}.markdown-section :is(h1,h2,h3,h4,h5) a:hover:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='feather feather-link-2'%3E%3Cpath d='M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3M8 12h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:contain;content:"";display:inline-block;height:14px;margin-left:6px;position:relative;top:-2px;width:14px}.markdown-section :is(h1,h2,h3,h4,h5) a:focus-visible:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='feather feather-link-2'%3E%3Cpath d='M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3M8 12h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:contain;content:"";display:inline-block;height:14px;margin-left:6px;position:relative;top:-2px;width:14px}.markdown-section a{color:var(--primary-color);text-decoration:none}.markdown-section :is(li,p) a{border-bottom:2px solid var(--gray3);text-decoration:none}.markdown-section :is(p,li) a:hover{border-bottom:2px solid var(--gray12)}.markdown-section :is(p,li) a:focus-visible{border-bottom:2px solid var(--gray12)}.markdown-section li a[href^="https://"]:after,.markdown-section li a[href^=http]:after,.markdown-section p a[href^="https://"]:after,.markdown-section p a[href^=http]:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='feather feather-arrow-up-right'%3E%3Cpath d='M7 17 17 7M7 7h10v10'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:contain;content:"";display:inline-block;height:11px;margin-left:2px;top:0;width:11px}.markdown-section li a[href^="https://"]:hover:after,.markdown-section li a[href^=http]:hover:after,.markdown-section p a[href^="https://"]:hover:after,.markdown-section p a[href^=http]:hover:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='feather feather-arrow-right'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E")}.markdown-section li a[href^="https://"]:focus-visible:after,.markdown-section li a[href^=http]:focus-visible:after,.markdown-section p a[href^="https://"]:focus-visible:after,.markdown-section p a[href^=http]:focus-visible:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='feather feather-arrow-right'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E")}.markdown-section h1{font-size:4rem;margin:4rem 0 1rem}.markdown-section h2{font-size:2.5rem;margin:3.5rem 0 1rem}.markdown-section h3{font-size:2rem;margin:3rem 0 1rem}.markdown-section h4{font-size:1.5rem;margin:2.5rem 0 1rem}.markdown-section h5{font-size:1rem}.markdown-section p{margin:1.5rem 0 0}.markdown-section :is(p,ul){font-size:17px;letter-spacing:.004em;line-height:1.6}.markdown-section ul{padding-left:32px}.markdown-section ul li{margin-bottom:1rem}.markdown-section :is(li,p) code{background:var(--bg-color);border:1px solid var(--codebox-border-color);border-radius:3px;color:var(--codebox-color);font-family:var(--code-font);font-size:.75rem;margin:0 2px;padding:4px 5px;white-space:nowrap}#banner{display:flex;margin:auto;width:60%}.app-name{display:flex;justify-content:center}.app-name-link img{height:32px;width:32px} -------------------------------------------------------------------------------- /public/fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/unavatar/7cf821ac3da162a33ca9b04a757144969c671967/public/fallback.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/unavatar/7cf821ac3da162a33ca9b04a757144969c671967/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/unavatar/7cf821ac3da162a33ca9b04a757144969c671967/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/unavatar/7cf821ac3da162a33ca9b04a757144969c671967/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | unavatar, the ultimate avatar service that offers everything you need to easily retrieve user avatars 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | window.$docsify = { 2 | name: 'unavatar', 3 | repo: 'microlinkhq/unavatar', 4 | logo: '/fallback.png', 5 | externalLinkRel: 'noopener noreferrer' 6 | } 7 | 8 | window.addEventListener('DOMContentLoaded', () => { 9 | document.querySelector('.app-name-link').onclick = event => { 10 | event.preventDefault() 11 | window.scrollTo({ top: 0, behavior: 'smooth' }) 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /public/js/main.min.js: -------------------------------------------------------------------------------- 1 | window.$docsify={name:"unavatar",repo:"microlinkhq/unavatar",logo:"/fallback.png",externalLinkRel:"noopener noreferrer"},window.addEventListener("DOMContentLoaded",()=>{document.querySelector(".app-name-link").onclick=e=>{e.preventDefault(),window.scrollTo({top:0,behavior:"smooth"})}}); -------------------------------------------------------------------------------- /public/logo-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/unavatar/7cf821ac3da162a33ca9b04a757144969c671967/public/logo-raw.png -------------------------------------------------------------------------------- /public/logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/unavatar/7cf821ac3da162a33ca9b04a757144969c671967/public/logo-transparent.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/unavatar/7cf821ac3da162a33ca9b04a757144969c671967/public/logo.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /src/authentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const timeSpan = require('@kikobeats/time-span')({ format: require('ms') }) 4 | const debug = require('debug-logfmt')('unavatar:authentication') 5 | const { RateLimiterMemory } = require('rate-limiter-flexible') 6 | const FrequencyCounter = require('frequency-counter') 7 | const onFinished = require('on-finished') 8 | const os = require('os') 9 | 10 | const duration = timeSpan() 11 | const reqsMin = new FrequencyCounter(60) 12 | let reqs = 0 13 | 14 | const { API_KEY, RATE_LIMIT_WINDOW, RATE_LIMIT } = require('./constant') 15 | 16 | const rateLimiter = new RateLimiterMemory({ 17 | points: RATE_LIMIT, 18 | duration: RATE_LIMIT_WINDOW 19 | }) 20 | 21 | const more = (() => { 22 | const email = 'hello@microlink.io' 23 | const subject = encodeURIComponent('[unavatar] Buy an API key') 24 | const body = encodeURIComponent( 25 | 'Hello,\nI want to buy an unavatar.sh API key. Please tell me how to proceed.' 26 | ) 27 | return `mailto:${email}?subject=${subject}&body=${body}` 28 | })() 29 | 30 | const rateLimitError = (() => { 31 | const rateLimitError = new Error( 32 | JSON.stringify({ 33 | status: 'fail', 34 | code: 'ERATE', 35 | more, 36 | message: 37 | 'Your daily rate limit has been reached. You need to wait or buy a plan.' 38 | }) 39 | ) 40 | 41 | rateLimitError.statusCode = 429 42 | rateLimitError.headers = { 'Content-Type': 'application/json' } 43 | return rateLimitError 44 | })() 45 | 46 | module.exports = async (req, res, next) => { 47 | ++reqs && reqsMin.inc() 48 | onFinished(res, () => --reqs) 49 | 50 | if (req.headers['x-api-key'] === API_KEY) return next() 51 | 52 | const { msBeforeNext, remainingPoints: quotaRemaining } = await rateLimiter 53 | .consume(req.ipAddress) 54 | .catch(error => error) 55 | 56 | if (!res.writableEnded) { 57 | res.setHeader('X-Rate-Limit-Limit', RATE_LIMIT) 58 | res.setHeader('X-Rate-Limit-Remaining', quotaRemaining) 59 | res.setHeader('X-Rate-Limit-Reset', new Date(Date.now() + msBeforeNext)) 60 | 61 | const perMinute = reqsMin.freq() 62 | const perSecond = Number(perMinute / 60).toFixed(1) 63 | 64 | debug(req.ipAddress, { 65 | uptime: duration(), 66 | load: os.loadavg().map(n => n.toFixed(2)), 67 | reqs, 68 | 'req/m': perMinute, 69 | 'req/s': perSecond, 70 | quota: `${quotaRemaining}/${RATE_LIMIT}` 71 | }) 72 | } 73 | 74 | if (quotaRemaining) return next() 75 | res.setHeader('Retry-After', msBeforeNext / 1000) 76 | return next(rateLimitError) 77 | } 78 | -------------------------------------------------------------------------------- /src/avatar/auto.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const isAbsoluteUrl = require('is-absolute-url') 4 | const dataUriRegex = require('data-uri-regex') 5 | const isEmail = require('is-email-like') 6 | const pTimeout = require('p-timeout') 7 | const urlRegex = require('url-regex') 8 | const pAny = require('p-any') 9 | 10 | const { providers, providersBy } = require('../providers') 11 | const reachableUrl = require('../util/reachable-url') 12 | const isIterable = require('../util/is-iterable') 13 | const ExtendableError = require('../util/error') 14 | 15 | const { STATUS_CODES } = require('http') 16 | const { AVATAR_TIMEOUT } = require('../constant') 17 | 18 | const is = ({ input }) => { 19 | if (isEmail(input)) return 'email' 20 | if (urlRegex({ strict: false }).test(input)) return 'domain' 21 | return 'username' 22 | } 23 | 24 | const getAvatarContent = name => async input => { 25 | if (typeof input !== 'string' || input === '') { 26 | const message = 27 | input === undefined ? 'not found' : `\`${input}\` is invalid` 28 | const statusCode = input === undefined ? 404 : 400 29 | throw new ExtendableError({ name, message, statusCode }) 30 | } 31 | 32 | if (dataUriRegex().test(input)) { 33 | return { type: 'buffer', data: input } 34 | } 35 | 36 | if (!isAbsoluteUrl(input)) { 37 | throw new ExtendableError({ 38 | message: 'The URL must to be absolute.', 39 | name, 40 | statusCode: 400 41 | }) 42 | } 43 | 44 | const { statusCode, url } = await reachableUrl(input) 45 | 46 | if (!reachableUrl.isReachable({ statusCode })) { 47 | throw new ExtendableError({ 48 | message: STATUS_CODES[statusCode], 49 | name, 50 | statusCode 51 | }) 52 | } 53 | 54 | return { type: 'url', data: url } 55 | } 56 | 57 | const getAvatar = async (fn, name, args) => { 58 | const promise = Promise.resolve(fn(args)) 59 | .then(getAvatarContent(name)) 60 | .catch(error => { 61 | isIterable.forEach(error, error => { 62 | error.statusCode = error.statusCode ?? error.response?.statusCode 63 | error.name = name 64 | }) 65 | throw error 66 | }) 67 | 68 | return pTimeout(promise, AVATAR_TIMEOUT).catch(error => { 69 | error.name = name 70 | throw error 71 | }) 72 | } 73 | 74 | module.exports = async args => { 75 | const collection = providersBy[is(args)] 76 | const promises = collection.map(name => 77 | pTimeout(getAvatar(providers[name], name, args), AVATAR_TIMEOUT) 78 | ) 79 | return pAny(promises) 80 | } 81 | 82 | module.exports.getAvatar = getAvatar 83 | -------------------------------------------------------------------------------- /src/avatar/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | auto: require('./auto'), 3 | provider: require('./provider'), 4 | resolve: require('./resolve') 5 | } 6 | -------------------------------------------------------------------------------- /src/avatar/provider.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { getAvatar } = require('./auto') 4 | 5 | module.exports = (name, fn) => args => getAvatar(fn, name, args) 6 | -------------------------------------------------------------------------------- /src/avatar/resolve.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug-logfmt')('unavatar:resolve') 4 | const reachableUrl = require('../util/reachable-url') 5 | const isAbsoluteUrl = require('is-absolute-url') 6 | const memoizeOne = require('async-memoize-one') 7 | const dataUriRegex = require('data-uri-regex') 8 | const { getTtl } = require('../send/cache') 9 | const isUrlHttp = require('is-url-http') 10 | const pTimeout = require('p-timeout') 11 | const pReflect = require('p-reflect') 12 | const { omit } = require('lodash') 13 | 14 | const isIterable = require('../util/is-iterable') 15 | 16 | const { AVATAR_SIZE, AVATAR_TIMEOUT } = require('../constant') 17 | 18 | const printErrors = error => { 19 | isIterable.forEach(error, error => { 20 | if (isIterable(error)) printErrors(error) 21 | else { 22 | if (error.message) { 23 | debug.error({ 24 | name: error.name, 25 | message: error.message, 26 | statusCode: error.statusCode 27 | }) 28 | } 29 | } 30 | }) 31 | } 32 | 33 | const optimizeUrl = async (url, query) => { 34 | const { ttl, ...rest } = omit(query, ['json', 'fallback']) 35 | return `https://images.weserv.nl/?${new URLSearchParams({ 36 | url, 37 | default: url, 38 | l: 9, 39 | af: '', 40 | il: '', 41 | n: -1, 42 | w: AVATAR_SIZE, 43 | ttl: getTtl(ttl), 44 | ...rest 45 | }).toString()}` 46 | } 47 | 48 | const getDefaultFallbackUrl = ({ protocol, host }) => 49 | this.url || (this.url = `${protocol}://${host}/fallback.png`) 50 | 51 | const getFallbackUrl = memoizeOne(async ({ query, protocol, host }) => { 52 | const { fallback } = query 53 | if (fallback === false) return null 54 | if (dataUriRegex().test(fallback)) return fallback 55 | if (!isUrlHttp(fallback) || !isAbsoluteUrl(fallback)) { 56 | return getDefaultFallbackUrl({ protocol, host }) 57 | } 58 | const { statusCode, url } = await reachableUrl(fallback) 59 | return reachableUrl.isReachable({ statusCode }) 60 | ? url 61 | : getDefaultFallbackUrl({ protocol, host }) 62 | }) 63 | 64 | module.exports = fn => async (req, res) => { 65 | const protocol = req.socket.encrypted ? 'https' : 'http' 66 | const input = req.params.key 67 | const host = req.headers.host 68 | const { query } = req 69 | 70 | let { value, reason, isRejected } = await pReflect( 71 | pTimeout(fn({ input, req, res }), AVATAR_TIMEOUT) 72 | ) 73 | 74 | if (isRejected) printErrors(reason) 75 | 76 | if (value && value.type === 'url') { 77 | value.data = await optimizeUrl(value.data, query) 78 | } 79 | 80 | if (value === undefined) { 81 | const data = await getFallbackUrl({ query, protocol, host }) 82 | value = data 83 | ? { type: dataUriRegex().test(data) ? 'buffer' : 'url', data } 84 | : null 85 | } 86 | 87 | return value 88 | } 89 | -------------------------------------------------------------------------------- /src/constant.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { existsSync } = require('fs') 4 | const ms = require('ms') 5 | 6 | const TMP_FOLDER = existsSync('/dev/shm') ? '/dev/shm' : '/tmp' 7 | 8 | const { 9 | ALLOWED_REQ_HEADERS = ['accept-encoding', 'accept', 'user-agent'], 10 | AVATAR_SIZE = 400, 11 | AVATAR_TIMEOUT = 25000, 12 | TTL_DEFAULT = ms('1y'), 13 | TTL_MIN = ms('1h'), 14 | TTL_MAX = ms('28d'), 15 | NODE_ENV = 'development', 16 | PORT = 3000, 17 | RATE_LIMIT_WINDOW = 86400, 18 | RATE_LIMIT = 50 19 | } = process.env 20 | 21 | const API_URL = 22 | NODE_ENV === 'production' ? 'https://unavatar.io' : `http://127.0.0.1:${PORT}` 23 | 24 | module.exports = { 25 | ...process.env, 26 | isProduction: NODE_ENV === 'production', 27 | API_URL, 28 | ALLOWED_REQ_HEADERS, 29 | AVATAR_SIZE, 30 | AVATAR_TIMEOUT: Number(AVATAR_TIMEOUT), 31 | TTL_DEFAULT, 32 | TTL_MIN, 33 | TTL_MAX, 34 | NODE_ENV, 35 | PORT, 36 | TMP_FOLDER, 37 | RATE_LIMIT_WINDOW, 38 | RATE_LIMIT 39 | } 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const timeSpan = require('@kikobeats/time-span')({ 4 | format: n => ms(Math.round(n)) 5 | }) 6 | const debug = require('debug-logfmt')('unavatar') 7 | const serveStatic = require('serve-static') 8 | const createRouter = require('router-http') 9 | const onFinished = require('on-finished') 10 | const { forEach } = require('lodash') 11 | const send = require('send-http') 12 | const path = require('path') 13 | const ms = require('ms') 14 | 15 | const { providers } = require('./providers') 16 | const ssrCache = require('./send/cache') 17 | const avatar = require('./avatar') 18 | 19 | const { isProduction, API_URL } = require('./constant') 20 | 21 | const router = createRouter((error, _, res) => { 22 | const hasError = error !== undefined 23 | let statusCode = 404 24 | let data = 'Not Found' 25 | 26 | if (hasError) { 27 | statusCode = error.statusCode ?? 500 28 | data = error.message ?? 'Internal Server Error' 29 | if (error.statusCode === undefined) console.error(error) 30 | if ('headers' in error) { 31 | for (const [key, value] of Object.entries(error.headers)) { 32 | res.setHeader(key, value) 33 | } 34 | } 35 | } 36 | 37 | return send(res, statusCode, data) 38 | }) 39 | 40 | router 41 | .use( 42 | (req, res, next) => { 43 | if (req.url.startsWith('/twitter')) { 44 | res.writeHead(301, { Location: req.url.replace('/twitter', '/x') }) 45 | res.end() 46 | } 47 | req.ipAddress = req.headers['cf-connecting-ip'] || '::ffff:127.0.0.1' 48 | next() 49 | }, 50 | require('helmet')({ 51 | crossOriginResourcePolicy: false, 52 | contentSecurityPolicy: false 53 | }), 54 | require('http-compression')(), 55 | require('cors')(), 56 | serveStatic(path.resolve('public'), { 57 | immutable: true, 58 | maxAge: '1y' 59 | }), 60 | require('./authentication'), 61 | isProduction && require('./ua'), 62 | (req, res, next) => { 63 | req.timestamp = timeSpan() 64 | req.query = Array.from(new URLSearchParams(req.query)).reduce( 65 | (acc, [key, value]) => { 66 | try { 67 | acc[key] = value === '' ? true : JSON.parse(value) 68 | } catch (err) { 69 | acc[key] = value 70 | } 71 | return acc 72 | }, 73 | {} 74 | ) 75 | onFinished(res, () => { 76 | debug( 77 | `${req.ipAddress} ${new URL(req.url, API_URL).toString()} ${ 78 | res.statusCode 79 | } – ${req.timestamp()}` 80 | ) 81 | }) 82 | next() 83 | } 84 | ) 85 | .get('ping', (_, res) => send(res, 200, 'pong')) 86 | .get('/:key', (req, res) => 87 | ssrCache({ 88 | req, 89 | res, 90 | fn: avatar.resolve(avatar.auto) 91 | }) 92 | ) 93 | 94 | forEach(providers, (fn, provider) => 95 | router.get(`/${provider}/:key`, (req, res) => 96 | ssrCache({ req, res, fn: avatar.resolve(avatar.provider(provider, fn)) }) 97 | ) 98 | ) 99 | 100 | module.exports = router 101 | -------------------------------------------------------------------------------- /src/providers/deviantart.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function deviantart ( 8 | { input }, 9 | onCancel 10 | ) { 11 | const promise = getHTML(`https://www.deviantart.com/${input}`) 12 | onCancel(() => promise.onCancel()) 13 | const { $ } = await promise 14 | return $('meta[property="og:image"]').attr('content') 15 | }) 16 | 17 | module.exports.supported = { 18 | email: false, 19 | username: true, 20 | domain: false 21 | } 22 | -------------------------------------------------------------------------------- /src/providers/dribbble.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function dribbble ({ input }, onCancel) { 8 | const promise = getHTML(`https://dribbble.com/${input}`) 9 | onCancel(() => promise.onCancel()) 10 | const { $ } = await promise 11 | return $('.profile-avatar').attr('src') 12 | }) 13 | 14 | module.exports.supported = { 15 | email: false, 16 | username: true, 17 | domain: false 18 | } 19 | -------------------------------------------------------------------------------- /src/providers/duckduckgo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function duckduckgo ({ input }) { 4 | return `https://icons.duckduckgo.com/ip3/${input}.ico` 5 | } 6 | 7 | module.exports.supported = { 8 | email: false, 9 | username: false, 10 | domain: true 11 | } 12 | -------------------------------------------------------------------------------- /src/providers/github.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { stringify } = require('querystring') 4 | 5 | const { AVATAR_SIZE } = require('../constant') 6 | 7 | module.exports = async function github ({ input }) { 8 | return `https://github.com/${input}.png?${stringify({ 9 | size: AVATAR_SIZE 10 | })}` 11 | } 12 | 13 | module.exports.supported = { 14 | email: false, 15 | username: true, 16 | domain: false 17 | } 18 | -------------------------------------------------------------------------------- /src/providers/gitlab.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function gitlab ({ input }, onCancel) { 8 | const promise = getHTML(`https://gitlab.com/${input}`) 9 | onCancel(() => promise.onCancel()) 10 | const { $ } = await promise 11 | return $('meta[property="og:image"]').attr('content') 12 | }) 13 | 14 | module.exports.supported = { 15 | email: false, 16 | username: true, 17 | domain: false 18 | } 19 | -------------------------------------------------------------------------------- /src/providers/google.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function google ({ input }) { 4 | return `https://www.google.com/s2/favicons?domain_url=${input}&sz=128` 5 | } 6 | 7 | module.exports.supported = { 8 | email: false, 9 | username: false, 10 | domain: true 11 | } 12 | -------------------------------------------------------------------------------- /src/providers/gravatar.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { stringify } = require('querystring') 4 | const crypto = require('crypto') 5 | 6 | const { AVATAR_SIZE } = require('../constant') 7 | 8 | const md5 = str => crypto.createHash('md5').update(str).digest('hex') 9 | 10 | module.exports = function gravatar ({ input }) { 11 | return `https://gravatar.com/avatar/${md5( 12 | input.trim().toLowerCase() 13 | )}?${stringify({ 14 | size: AVATAR_SIZE, 15 | d: '404' 16 | })}` 17 | } 18 | 19 | module.exports.supported = { 20 | email: true, 21 | username: false, 22 | domain: false 23 | } 24 | -------------------------------------------------------------------------------- /src/providers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { reduce } = require('lodash') 4 | 5 | const providers = { 6 | deviantart: require('./deviantart'), 7 | dribbble: require('./dribbble'), 8 | duckduckgo: require('./duckduckgo'), 9 | github: require('./github'), 10 | gitlab: require('./gitlab'), 11 | google: require('./google'), 12 | gravatar: require('./gravatar'), 13 | // instagram: require('./instagram'), 14 | microlink: require('./microlink'), 15 | readcv: require('./readcv'), 16 | // reddit: require('./reddit'), 17 | soundcloud: require('./soundcloud'), 18 | substack: require('./substack'), 19 | telegram: require('./telegram'), 20 | // tiktok: require('./tiktok'), 21 | twitch: require('./twitch'), 22 | x: require('./x'), 23 | youtube: require('./youtube'), 24 | onlyfans: require('./onlyfans') 25 | } 26 | 27 | const providersBy = reduce( 28 | providers, 29 | (acc, { supported }, provider) => { 30 | if (supported.email) acc.email.push(provider) 31 | if (supported.username) acc.username.push(provider) 32 | if (supported.domain) acc.domain.push(provider) 33 | return acc 34 | }, 35 | { email: [], username: [], domain: [] } 36 | ) 37 | 38 | module.exports = { providers, providersBy } 39 | -------------------------------------------------------------------------------- /src/providers/instagram.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function instagram ({ input }, onCancel) { 8 | const promise = getHTML(`https://www.instagram.com/${input}`) 9 | onCancel(() => promise.onCancel()) 10 | const { $ } = await promise 11 | return $('meta[property="og:image"]').attr('content') 12 | }) 13 | 14 | module.exports.supported = { 15 | email: false, 16 | username: true, 17 | domain: false 18 | } 19 | -------------------------------------------------------------------------------- /src/providers/microlink.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | const mql = require('@microlink/mql') 5 | 6 | module.exports = PCancelable.fn(async function microlink ( 7 | { input, req }, 8 | onCancel 9 | ) { 10 | const promise = mql(`https://${input}`, { 11 | apiKey: req.headers?.['x-api-key'] 12 | }) 13 | onCancel(() => promise.onCancel()) 14 | const { data } = await promise 15 | return data.logo?.url 16 | }) 17 | 18 | module.exports.supported = { 19 | email: false, 20 | username: false, 21 | domain: true 22 | } 23 | -------------------------------------------------------------------------------- /src/providers/onlyfans.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function onlyfans ({ input }, onCancel) { 8 | const promise = getHTML(`https://onlyfans.com/${input}`, { 9 | prerender: true, 10 | puppeteerOpts: { abortTypes: ['other', 'image', 'font'] } 11 | }) 12 | onCancel(() => promise.onCancel()) 13 | const { $ } = await promise 14 | const json = JSON.parse( 15 | $('script[type="application/ld+json"]').contents().text() 16 | ) 17 | return json.mainEntity.image 18 | }) 19 | 20 | module.exports.supported = { 21 | email: false, 22 | username: true, 23 | domain: false 24 | } 25 | -------------------------------------------------------------------------------- /src/providers/readcv.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | const srcset = require('srcset') 5 | 6 | const getHTML = require('../util/html-get') 7 | 8 | module.exports = PCancelable.fn(async function readcv ({ input }, onCancel) { 9 | const promise = getHTML(`https://read.cv/${input}`) 10 | onCancel(() => promise.onCancel()) 11 | const { $ } = await promise 12 | const images = $('main > div > div > div > div > img').attr('srcset') 13 | if (!images) return 14 | const parsedImages = srcset.parse(images) 15 | return parsedImages[parsedImages.length - 1].url 16 | }) 17 | 18 | module.exports.supported = { 19 | email: false, 20 | username: true, 21 | domain: false 22 | } 23 | -------------------------------------------------------------------------------- /src/providers/reddit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function reddit ({ input }, onCancel) { 8 | const promise = getHTML(`https://www.reddit.com/user/${input}`, { 9 | headers: { 'accept-language': 'en' } 10 | }) 11 | onCancel(() => promise.onCancel()) 12 | const { $ } = await promise 13 | return $('img[alt*="avatar"]').attr('src') 14 | }) 15 | 16 | module.exports.supported = { 17 | email: false, 18 | username: true, 19 | domain: false 20 | } 21 | -------------------------------------------------------------------------------- /src/providers/soundcloud.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function soundcloud ( 8 | { input }, 9 | onCancel 10 | ) { 11 | const promise = getHTML(`https://soundcloud.com/${input}`) 12 | onCancel(() => promise.onCancel()) 13 | const { $ } = await promise 14 | return $('meta[property="og:image"]').attr('content') 15 | }) 16 | 17 | module.exports.supported = { 18 | email: false, 19 | username: true, 20 | domain: false 21 | } 22 | -------------------------------------------------------------------------------- /src/providers/substack.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function substack ({ input }, onCancel) { 8 | const promise = getHTML(`https://${input}.substack.com`) 9 | onCancel(() => promise.onCancel()) 10 | const { $ } = await promise 11 | return $('.publication-logo').attr('src') 12 | }) 13 | 14 | module.exports.supported = { 15 | email: false, 16 | username: true, 17 | domain: false 18 | } 19 | -------------------------------------------------------------------------------- /src/providers/telegram.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function telegram ({ input }, onCancel) { 8 | const promise = getHTML(`https://t.me/${input}`) 9 | onCancel(() => promise.onCancel()) 10 | const { $ } = await promise 11 | const el = $('img.tgme_page_photo_image') 12 | return el.attr('src') 13 | }) 14 | 15 | module.exports.supported = { 16 | email: false, 17 | username: true, 18 | domain: false 19 | } 20 | -------------------------------------------------------------------------------- /src/providers/tiktok.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function tiktok ({ input }, onCancel) { 8 | const promise = getHTML(`https://www.tiktok.com/@${input}`) 9 | onCancel(() => promise.onCancel()) 10 | const { $ } = await promise 11 | return $('meta[property="og:image"]').attr('content') 12 | }) 13 | 14 | module.exports.supported = { 15 | email: false, 16 | username: true, 17 | domain: false 18 | } 19 | -------------------------------------------------------------------------------- /src/providers/twitch.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function instagram ({ input }, onCancel) { 8 | const promise = getHTML(`https://www.twitch.tv/${input}`) 9 | onCancel(() => promise.onCancel()) 10 | const { $ } = await promise 11 | return $('meta[property="og:image"]').attr('content') 12 | }) 13 | 14 | module.exports.supported = { 15 | email: false, 16 | username: true, 17 | domain: false 18 | } 19 | -------------------------------------------------------------------------------- /src/providers/x.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const uniqueRandomArray = require('unique-random-array') 4 | const PCancelable = require('p-cancelable') 5 | 6 | const randomCrawlerAgent = uniqueRandomArray( 7 | require('top-crawler-agents').filter(agent => agent.startsWith('Slackbot')) 8 | ) 9 | 10 | const getHTML = require('../util/html-get') 11 | 12 | const avatarUrl = str => { 13 | if (str?.endsWith('_200x200.jpg')) { 14 | str = str.replace('_200x200.jpg', '_400x400.jpg') 15 | } 16 | return str 17 | } 18 | 19 | module.exports = PCancelable.fn(async function twitter ({ input }, onCancel) { 20 | const promise = getHTML(`https://x.com/${input}`, { 21 | headers: { 'user-agent': randomCrawlerAgent() } 22 | }) 23 | onCancel(() => promise.onCancel()) 24 | const { $ } = await promise 25 | return avatarUrl($('meta[property="og:image"]').attr('content')) 26 | }) 27 | 28 | module.exports.supported = { 29 | email: false, 30 | username: true, 31 | domain: false 32 | } 33 | -------------------------------------------------------------------------------- /src/providers/youtube.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PCancelable = require('p-cancelable') 4 | 5 | const getHTML = require('../util/html-get') 6 | 7 | module.exports = PCancelable.fn(async function youtube ({ input }, onCancel) { 8 | const promise = getHTML(`https://www.youtube.com/@${input}`) 9 | onCancel(() => promise.onCancel()) 10 | const { $ } = await promise 11 | return $('meta[property="og:image"]').attr('content') 12 | }) 13 | 14 | module.exports.supported = { 15 | email: false, 16 | username: true, 17 | domain: false 18 | } 19 | -------------------------------------------------------------------------------- /src/send/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ms = require('ms') 4 | 5 | const memoize = require('../util/memoize') 6 | const send = require('.') 7 | 8 | const { TTL_DEFAULT, TTL_MIN, TTL_MAX } = require('../constant') 9 | 10 | const getTtl = memoize(ttl => { 11 | if (ttl === undefined || ttl === null) return TTL_DEFAULT 12 | const value = typeof ttl === 'number' ? ttl : ms(ttl) 13 | if (value === undefined || value < TTL_MIN || value > TTL_MAX) { 14 | return TTL_DEFAULT 15 | } 16 | return value 17 | }) 18 | 19 | module.exports = require('cacheable-response')({ 20 | logger: require('debug-logfmt')('cacheable-response'), 21 | ttl: TTL_DEFAULT, 22 | staleTtl: false, 23 | get: async ({ req, res, fn }) => ({ 24 | ttl: getTtl(req.query.ttl), 25 | data: await fn(req, res) 26 | }), 27 | send: ({ req, res, data }) => send({ req, res, ...data }) 28 | }) 29 | 30 | module.exports.getTtl = getTtl 31 | -------------------------------------------------------------------------------- /src/send/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { dataUriToBuffer } = require('data-uri-to-buffer') 4 | const { pickBy } = require('lodash') 5 | const send = require('send-http') 6 | 7 | const got = require('../util/got') 8 | 9 | const { ALLOWED_REQ_HEADERS } = require('../constant') 10 | 11 | const pickHeaders = headers => 12 | pickBy(headers, (_, key) => ALLOWED_REQ_HEADERS.includes(key)) 13 | 14 | module.exports = ({ type, data, req, res }) => { 15 | const { query } = req 16 | const statusCode = data ? 200 : 404 17 | 18 | if (query.json) { 19 | return send(res, statusCode, { url: data }) 20 | } 21 | 22 | if (type === 'buffer' || data === undefined) { 23 | const responseData = data === undefined ? data : dataUriToBuffer(data) 24 | return send(res, statusCode, responseData) 25 | } 26 | 27 | const stream = got.stream(data, { headers: pickHeaders(req.headers) }) 28 | return stream.pipe(res) 29 | } 30 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.DEBUG = 4 | process.env.DEBUG || 5 | '*,-html-get*,-puppeteer*,-send*,-ioredis*,-cacheable-response*,-browserless*' 6 | 7 | const debug = require('debug-logfmt')('unavatar') 8 | const { createServer } = require('http') 9 | 10 | const { API_URL, NODE_ENV, PORT } = require('./constant') 11 | 12 | const server = createServer((req, res) => 13 | require('./util/uuid').withUUID(() => require('.')(req, res)) 14 | ) 15 | 16 | server.listen(PORT, () => { 17 | debug({ 18 | status: 'listening', 19 | environment: NODE_ENV, 20 | pid: process.pid, 21 | address: API_URL 22 | }) 23 | }) 24 | 25 | let isClosing = false 26 | 27 | if (NODE_ENV === 'production') { 28 | process.on('SIGTERM', () => (isClosing = true)) 29 | 30 | /** 31 | * It uses Fly.io v2 to scale to zero, similar to AWS Lambda 32 | * https://community.fly.io/t/implementing-scale-to-zero-is-super-easy/12415 33 | */ 34 | const keepAlive = (duration => { 35 | let timer 36 | const keepAlive = () => { 37 | clearTimeout(timer) 38 | timer = setTimeout(() => { 39 | server.close(() => { 40 | debug({ 41 | status: 'shutting down', 42 | reason: `No request received in ${duration / 1000}s` 43 | }) 44 | process.exit(0) 45 | }) 46 | }, duration) 47 | } 48 | keepAlive() 49 | return keepAlive 50 | })(10000) 51 | 52 | server.on('request', () => !isClosing && keepAlive()) 53 | } 54 | 55 | process.on('uncaughtException', error => { 56 | debug.error('uncaughtException', { 57 | message: error.message || error, 58 | requestUrl: error.response?.requestUrl, 59 | stack: error.stack 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/ua.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const redis = require('./util/redis/ua') 4 | const ua = redis ? require('@microlink/ua')(redis) : undefined 5 | 6 | module.exports = ua 7 | ? async (req, _, next) => { 8 | await ua.incr(req.headers['user-agent']) 9 | next() 10 | } 11 | : false 12 | -------------------------------------------------------------------------------- /src/util/browserless.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const createBrowser = require('browserless') 4 | const puppeteer = require('puppeteer') 5 | 6 | const path = require('path') 7 | 8 | const { TMP_FOLDER } = require('../constant') 9 | 10 | const getArgs = () => { 11 | const PUPPETEER_DIR = path.join(TMP_FOLDER, `puppeteer-${Date.now()}`) 12 | const DATA_DIR = path.join(PUPPETEER_DIR, 'profile') 13 | const CACHE_DIR = path.join(PUPPETEER_DIR, 'cache') 14 | 15 | const args = createBrowser.driver.defaultArgs.concat([ 16 | '--allow-running-insecure-content', // https://source.chromium.org/search?q=lang:cpp+symbol:kAllowRunningInsecureContent&ss=chromium 17 | '--disk-cache-size=33554432', // https://source.chromium.org/search?q=lang:cpp+symbol:kDiskCacheSize&ss=chromium 18 | '--enable-features=SharedArrayBuffer', // https://source.chromium.org/search?q=file:content_features.cc&ss=chromium 19 | `--disk-cache-dir=${CACHE_DIR}`, 20 | `--user-data-dir=${DATA_DIR}` 21 | ]) 22 | 23 | return { PUPPETEER_DIR, DATA_DIR, CACHE_DIR, args } 24 | } 25 | 26 | const browser = createBrowser({ 27 | args: getArgs().args, 28 | puppeteer 29 | }) 30 | 31 | module.exports = () => browser 32 | -------------------------------------------------------------------------------- /src/util/cacheable-lookup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const CacheableLookup = require('cacheable-lookup') 4 | const Tangerine = require('tangerine') 5 | 6 | const { createMultiCache, createRedisCache } = require('./keyv') 7 | 8 | const { TTL_DEFAULT } = require('../constant') 9 | 10 | module.exports = new CacheableLookup({ 11 | maxTtl: TTL_DEFAULT, 12 | cache: createMultiCache(createRedisCache({ namespace: 'dns' })), 13 | resolver: new Tangerine( 14 | { 15 | cache: false 16 | }, 17 | require('got').extend({ 18 | responseType: 'buffer', 19 | decompress: false, 20 | retry: 0 21 | }) 22 | ) 23 | }) 24 | -------------------------------------------------------------------------------- /src/util/error.js: -------------------------------------------------------------------------------- 1 | module.exports = class ExtendableError extends Error { 2 | constructor (props) { 3 | super() 4 | Object.assign(this, props) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/util/got.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const uniqueRandomArray = require('unique-random-array') 4 | const tlsHook = require('https-tls/hook') 5 | const got = require('got') 6 | 7 | const randomUserAgent = uniqueRandomArray(require('top-user-agents')) 8 | 9 | const userAgentHook = options => { 10 | if ( 11 | options.headers['user-agent'] === 12 | 'got (https://github.com/sindresorhus/got)' 13 | ) { 14 | options.headers['user-agent'] = randomUserAgent() 15 | } 16 | } 17 | 18 | const gotOpts = { 19 | dnsCache: require('./cacheable-lookup'), 20 | https: { rejectUnauthorized: false }, 21 | hooks: { beforeRequest: [userAgentHook, tlsHook] } 22 | } 23 | 24 | module.exports = got.extend(gotOpts) 25 | module.exports.gotOpts = gotOpts 26 | -------------------------------------------------------------------------------- /src/util/html-get.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const getHTML = require('html-get') 4 | 5 | const createBrowser = require('./browserless') 6 | const { gotOpts } = require('./got') 7 | 8 | const { AVATAR_TIMEOUT } = require('../constant') 9 | 10 | module.exports = async (url, { puppeteerOpts, ...opts } = {}) => { 11 | const browser = await createBrowser() 12 | const browserContext = await browser.createContext() 13 | 14 | const result = await getHTML(url, { 15 | prerender: false, 16 | ...opts, 17 | getBrowserless: () => browserContext, 18 | serializeHtml: $ => ({ $ }), 19 | puppeteerOpts: { 20 | timeout: AVATAR_TIMEOUT, 21 | ...puppeteerOpts 22 | }, 23 | gotOpts: { 24 | ...gotOpts, 25 | timeout: AVATAR_TIMEOUT 26 | } 27 | }) 28 | 29 | await Promise.resolve(browserContext).then(browserless => 30 | browserless.destroyContext() 31 | ) 32 | 33 | return result 34 | } 35 | -------------------------------------------------------------------------------- /src/util/is-iterable.js: -------------------------------------------------------------------------------- 1 | const isIterable = input => typeof input[Symbol.iterator] === 'function' 2 | 3 | isIterable.forEach = (input, fn) => { 4 | for (const item of isIterable(input) ? input : [].concat(input)) { 5 | fn(item) 6 | } 7 | } 8 | 9 | module.exports = isIterable 10 | -------------------------------------------------------------------------------- /src/util/keyv.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const keyvCompress = require('@keyvhq/compress') 4 | const KeyvRedis = require('@keyvhq/redis') 5 | const KeyvMulti = require('@keyvhq/multi') 6 | const Keyv = require('@keyvhq/core') 7 | const assert = require('assert') 8 | 9 | const redis = require('./redis') 10 | 11 | const { TTL_DEFAULT } = require('../constant') 12 | 13 | const createMultiCache = remote => 14 | new Keyv({ store: new KeyvMulti({ remote }) }) 15 | 16 | const createKeyv = opts => new Keyv({ ttl: TTL_DEFAULT, ...opts }) 17 | 18 | const createKeyvNamespace = opts => { 19 | assert(opts.namespace, '`opts.namespace` is required.') 20 | return keyvCompress(createKeyv(opts)) 21 | } 22 | 23 | const createRedisCache = opts => { 24 | const store = redis ? new KeyvRedis(redis, { emitErrors: false }) : new Map() 25 | return createKeyvNamespace({ ...opts, store }) 26 | } 27 | 28 | module.exports = { createMultiCache, createRedisCache } 29 | -------------------------------------------------------------------------------- /src/util/memoize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const NullProtoObj = require('null-prototype-object') 4 | 5 | module.exports = fn => 6 | ( 7 | cache => 8 | (...args) => 9 | cache[args] || (cache[args] = fn(...args)) 10 | )(new NullProtoObj()) 11 | -------------------------------------------------------------------------------- /src/util/reachable-url.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const createPingUrl = require('@microlink/ping-url') 4 | 5 | const { gotOpts } = require('./got') 6 | const { createRedisCache } = require('./keyv') 7 | 8 | const pingCache = createRedisCache({ namespace: 'ping' }) 9 | 10 | const pingUrl = createPingUrl(pingCache, { 11 | value: ({ url, statusCode }) => ({ url, statusCode }) 12 | }) 13 | 14 | module.exports = (url, opts) => 15 | pingUrl(url, { 16 | ...gotOpts, 17 | ...opts 18 | }) 19 | 20 | module.exports.isReachable = createPingUrl.isReachable 21 | -------------------------------------------------------------------------------- /src/util/redis/create.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Redis = require('ioredis') 4 | 5 | module.exports = uri => 6 | uri 7 | ? new Redis(uri, { 8 | lazyConnect: true, 9 | enableAutoPipelining: true, 10 | maxRetriesPerRequest: 2 11 | }) 12 | : undefined 13 | -------------------------------------------------------------------------------- /src/util/redis/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { REDIS_URI } = require('../../constant') 4 | 5 | module.exports = require('./create')(REDIS_URI) 6 | -------------------------------------------------------------------------------- /src/util/redis/ua.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { REDIS_URI_UA } = require('../../constant') 4 | 5 | module.exports = require('./create')(REDIS_URI_UA) 6 | -------------------------------------------------------------------------------- /src/util/uuid.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { AsyncLocalStorage } = require('async_hooks') 4 | const { randomUUID } = require('crypto') 5 | 6 | const asyncLocalStorage = new AsyncLocalStorage() 7 | 8 | const overrideWrite = stream => { 9 | const originalWrite = stream.write 10 | stream.write = function (data) { 11 | originalWrite.call(stream, `${getUUID()} ${data}`) 12 | } 13 | } 14 | 15 | overrideWrite(process.stderr) 16 | overrideWrite(process.stdout) 17 | 18 | const getUUID = () => asyncLocalStorage.getStore() 19 | 20 | const withUUID = fn => asyncLocalStorage.run(randomUUID(), fn) 21 | 22 | module.exports = { getUUID, withUUID } 23 | -------------------------------------------------------------------------------- /test/endpoints.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | const got = require('got').extend({ responseType: 'json' }) 5 | 6 | const { runServer } = require('./helpers') 7 | 8 | const isCI = !!process.env.CI 9 | 10 | test('ping', async t => { 11 | const serverUrl = await runServer(t) 12 | const { body, statusCode } = await got('ping', { 13 | prefixUrl: serverUrl, 14 | responseType: 'text' 15 | }) 16 | t.is(statusCode, 200) 17 | t.is(body, 'pong') 18 | }) 19 | 20 | test('youtube', async t => { 21 | const serverUrl = await runServer(t) 22 | const { body, statusCode } = await got('youtube/natelive7?json', { 23 | prefixUrl: serverUrl 24 | }) 25 | t.is(statusCode, 200) 26 | t.true(body.url.includes('images.weserv.nl')) 27 | }) 28 | 29 | test('gitlab', async t => { 30 | const serverUrl = await runServer(t) 31 | const { body, statusCode } = await got('gitlab/kikobeats?json', { 32 | prefixUrl: serverUrl 33 | }) 34 | t.is(statusCode, 200) 35 | t.true(body.url.includes('images.weserv.nl')) 36 | }) 37 | 38 | test('github', async t => { 39 | const serverUrl = await runServer(t) 40 | const { body, statusCode } = await got('github/kikobeats?json', { 41 | prefixUrl: serverUrl 42 | }) 43 | t.is(statusCode, 200) 44 | t.true(body.url.includes('images.weserv.nl')) 45 | }) 46 | 47 | test('twitter', async t => { 48 | const serverUrl = await runServer(t) 49 | const { body, statusCode } = await got('twitter/kikobeats?json', { 50 | prefixUrl: serverUrl 51 | }) 52 | t.is(statusCode, 200) 53 | t.true(body.url.includes('images.weserv.nl')) 54 | }) 55 | 56 | test('soundcloud', async t => { 57 | const serverUrl = await runServer(t) 58 | const { body, statusCode } = await got('soundcloud/kikobeats?json', { 59 | prefixUrl: serverUrl 60 | }) 61 | t.is(statusCode, 200) 62 | t.true(body.url.includes('images.weserv.nl')) 63 | }) 64 | 65 | test('deviantart', async t => { 66 | const serverUrl = await runServer(t) 67 | const { body, statusCode } = await got('deviantart/spyed?json', { 68 | prefixUrl: serverUrl 69 | }) 70 | t.is(statusCode, 200) 71 | t.true(body.url.includes('images.weserv.nl')) 72 | }) 73 | 74 | test('dribbble', async t => { 75 | const serverUrl = await runServer(t) 76 | const { body, statusCode } = await got('dribbble/omidnikrah?json', { 77 | prefixUrl: serverUrl 78 | }) 79 | t.is(statusCode, 200) 80 | t.true(body.url.includes('images.weserv.nl')) 81 | }) 82 | 83 | test('duckduckgo', async t => { 84 | const serverUrl = await runServer(t) 85 | const { body, statusCode } = await got('duckduckgo/google.com?json', { 86 | prefixUrl: serverUrl 87 | }) 88 | t.is(statusCode, 200) 89 | t.true(body.url.includes('images.weserv.nl')) 90 | }) 91 | 92 | test('google', async t => { 93 | const serverUrl = await runServer(t) 94 | const { body, statusCode } = await got('google/teslahunt.io?json', { 95 | prefixUrl: serverUrl 96 | }) 97 | t.is(statusCode, 200) 98 | t.true(body.url.includes('images.weserv.nl')) 99 | }) 100 | 101 | test('gravatar', async t => { 102 | const serverUrl = await runServer(t) 103 | const { body, statusCode } = await got( 104 | 'gravatar/sindresorhus@gmail.com?json', 105 | { 106 | prefixUrl: serverUrl 107 | } 108 | ) 109 | t.is(statusCode, 200) 110 | t.true(body.url.includes('images.weserv.nl')) 111 | }) 112 | 113 | test('telegram', async t => { 114 | const serverUrl = await runServer(t) 115 | const { body, statusCode } = await got('telegram/drsdavidsoft?json', { 116 | prefixUrl: serverUrl 117 | }) 118 | t.is(statusCode, 200) 119 | t.true(body.url.includes('images.weserv.nl')) 120 | }) 121 | ;(isCI ? test.skip : test)('substack', async t => { 122 | const serverUrl = await runServer(t) 123 | const { body, statusCode } = await got('substack/bankless?json', { 124 | prefixUrl: serverUrl 125 | }) 126 | t.is(statusCode, 200) 127 | t.true(body.url.includes('images.weserv.nl')) 128 | }) 129 | ;(isCI ? test.skip : test)('reddit', async t => { 130 | const serverUrl = await runServer(t) 131 | const { body, statusCode } = await got('reddit/kikobeats?json', { 132 | prefixUrl: serverUrl 133 | }) 134 | t.is(statusCode, 200) 135 | t.true(body.url.includes('images.weserv.nl')) 136 | }) 137 | ;(isCI ? test.skip : test)('instagram', async t => { 138 | const serverUrl = await runServer(t) 139 | const { body, statusCode } = await got('instagram/willsmith?json', { 140 | prefixUrl: serverUrl 141 | }) 142 | t.is(statusCode, 200) 143 | t.true(body.url.includes('images.weserv.nl')) 144 | }) 145 | 146 | test('twitch', async t => { 147 | const serverUrl = await runServer(t) 148 | const { body, statusCode } = await got('twitch/midudev?json', { 149 | prefixUrl: serverUrl 150 | }) 151 | t.is(statusCode, 200) 152 | t.true(body.url.includes('images.weserv.nl')) 153 | }) 154 | 155 | test('microlink', async t => { 156 | const serverUrl = await runServer(t) 157 | const { body, statusCode } = await got('microlink/teslahunt.io?json', { 158 | prefixUrl: serverUrl 159 | }) 160 | t.is(statusCode, 200) 161 | t.true(body.url.includes('images.weserv.nl')) 162 | }) 163 | 164 | test('readcv', async t => { 165 | const serverUrl = await runServer(t) 166 | const { body, statusCode } = await got('readcv/elenatorro?json', { 167 | prefixUrl: serverUrl 168 | }) 169 | t.is(statusCode, 200) 170 | t.true(body.url.includes('images.weserv.nl')) 171 | }) 172 | ;(isCI ? test.skip : test)('tiktok', async t => { 173 | const serverUrl = await runServer(t) 174 | const { body, statusCode } = await got('tiktok/carlosazaustre?json', { 175 | prefixUrl: serverUrl 176 | }) 177 | t.is(statusCode, 200) 178 | t.true(body.url.includes('images.weserv.nl')) 179 | }) 180 | 181 | test('onlyfans', async t => { 182 | const serverUrl = await runServer(t) 183 | const { body, statusCode } = await got('onlyfans/amandaribas?json', { 184 | prefixUrl: serverUrl 185 | }) 186 | t.is(statusCode, 200) 187 | t.true(body.url.includes('images.weserv.nl')) 188 | }) 189 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { default: listen } = require('async-listen') 4 | const { createServer } = require('http') 5 | 6 | const close = server => new Promise(resolve => server.close(resolve)) 7 | 8 | const runServer = async t => { 9 | const server = createServer(require('../src')) 10 | const serverUrl = await listen(server) 11 | t.teardown(() => close(server)) 12 | return serverUrl 13 | } 14 | 15 | module.exports = { runServer } 16 | -------------------------------------------------------------------------------- /test/query-parameters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { dataUriToBuffer } = require('data-uri-to-buffer') 4 | const test = require('ava') 5 | const got = require('got') 6 | const ms = require('ms') 7 | 8 | const { TTL_DEFAULT } = require('../src/constant') 9 | 10 | const { runServer } = require('./helpers') 11 | const { getTtl } = require('../src/send/cache') 12 | 13 | test('json', async t => { 14 | const serverUrl = await runServer(t) 15 | 16 | const { headers, body } = await got('github/kikobeats?json', { 17 | prefixUrl: serverUrl, 18 | responseType: 'json' 19 | }) 20 | 21 | t.truthy(body.url) 22 | t.is(headers['content-type'], 'application/json; charset=utf-8') 23 | }) 24 | 25 | test('fallback', async t => { 26 | const serverUrl = await runServer(t) 27 | 28 | const { headers, body } = await got( 29 | 'github/__notexistprofile__?fallback=https://i.imgur.com/0d1TFfQ.jpg&json', 30 | { 31 | prefixUrl: serverUrl, 32 | responseType: 'json' 33 | } 34 | ) 35 | 36 | t.is(body.url, 'https://i.imgur.com/0d1TFfQ.jpg') 37 | t.is(headers['content-type'], 'application/json; charset=utf-8') 38 | }) 39 | 40 | test('fallback # data uri', async t => { 41 | { 42 | const dataURI = 43 | 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' 44 | const serverUrl = await runServer(t) 45 | 46 | const { headers, body } = await got( 47 | `github/__notexistprofile__?fallback=${encodeURIComponent(dataURI)}&json`, 48 | { 49 | prefixUrl: serverUrl, 50 | responseType: 'json' 51 | } 52 | ) 53 | 54 | t.is(body.url, dataURI) 55 | t.is(headers['content-type'], 'application/json; charset=utf-8') 56 | } 57 | { 58 | const dataURI = 59 | 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' 60 | const serverUrl = await runServer(t) 61 | 62 | const { headers, body } = await got( 63 | `github/__notexistprofile__?fallback=${encodeURIComponent(dataURI)}`, 64 | { 65 | prefixUrl: serverUrl, 66 | responseType: 'buffer' 67 | } 68 | ) 69 | 70 | t.true(body.equals(dataUriToBuffer(dataURI))) 71 | t.is(headers['content-type'], 'application/octet-stream') 72 | } 73 | }) 74 | 75 | test('fallback # use default value if fallback provided is not reachable', async t => { 76 | const serverUrl = await runServer(t) 77 | 78 | const { headers, body } = await got( 79 | 'github/__notexistprofile__?fallback=https://nexmoew.com/thisis404.png&json', 80 | { 81 | prefixUrl: serverUrl, 82 | responseType: 'json' 83 | } 84 | ) 85 | 86 | t.is(body.url, `${serverUrl.toString()}fallback.png`) 87 | t.is(headers['content-type'], 'application/json; charset=utf-8') 88 | }) 89 | 90 | test('ttl', t => { 91 | t.is(getTtl(), TTL_DEFAULT) 92 | t.is(getTtl(''), TTL_DEFAULT) 93 | t.is(getTtl(null), TTL_DEFAULT) 94 | t.is(getTtl(undefined), TTL_DEFAULT) 95 | t.is(getTtl(0), TTL_DEFAULT) 96 | t.is(getTtl('foo'), TTL_DEFAULT) 97 | t.is(getTtl('29d'), TTL_DEFAULT) 98 | t.is(getTtl('29d'), TTL_DEFAULT) 99 | t.is(getTtl(ms('2h')), ms('2h')) 100 | t.is(getTtl('2h'), ms('2h')) 101 | }) 102 | --------------------------------------------------------------------------------