├── .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 | 
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 |
--------------------------------------------------------------------------------