@unavatar/core 3.19.1 → 3.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,10 +19,12 @@
19
19
  - [Discord](#discord)
20
20
  - [Dribbble](#dribbble)
21
21
  - [DuckDuckGo](#duckduckgo)
22
+ - [Facebook](#facebook)
22
23
  - [GitHub](#github)
23
24
  - [GitLab](#gitlab)
24
25
  - [LinkedIn](#linkedin)
25
26
  - [Google](#google)
27
+ - [Gravatar](#gravatar)
26
28
  - [Instagram](#instagram)
27
29
  - [Ko-fi](#ko-fi)
28
30
  - [Medium](#medium)
@@ -33,6 +35,7 @@
33
35
  - [Patreon](#patreon)
34
36
  - [Pinterest](#pinterest)
35
37
  - [Printables](#printables)
38
+ - [PSN Profiles](#psn-profiles)
36
39
  - [Reddit](#reddit)
37
40
  - [Snapchat](#snapchat)
38
41
  - [SoundCloud](#soundcloud)
@@ -42,12 +45,16 @@
42
45
  - [Telegram](#telegram)
43
46
  - [Threads](#threads)
44
47
  - [TikTok](#tiktok)
48
+ - [Tumblr](#tumblr)
45
49
  - [Twitch](#twitch)
46
50
  - [Vimeo](#vimeo)
47
51
  - [WhatsApp](#whatsapp)
52
+ - [X/Twitter](#xtwitter)
53
+ - [Xbox Gamertag](#xbox-gamertag)
48
54
  - [YouTube](#youtube)
49
55
  - [Response Format](#response-format)
50
56
  - [Response Headers](#response-headers)
57
+ - [Response Errors](#response-errors)
51
58
 
52
59
  ---
53
60
 
@@ -55,14 +62,6 @@
55
62
 
56
63
  Welcome to **unavatar.io**, the ultimate avatar service that offers everything you need to easily retrieve user avatars:
57
64
 
58
- - **Versatile**: A wide range of platforms and services including [TikTok](https://unavatar.io/docs#tiktok), [Instagram](https://unavatar.io/docs#instagram), [YouTube](https://unavatar.io/docs#youtube), [X/Twitter](https://unavatar.io/docs#xtwitter), [Gravatar](https://unavatar.io/docs#gravatar), etc., meaning you can rule all of them just querying against unavatar.
59
-
60
- - **Speed**: Designed to be fast and efficient with a 93% cache hit rate, serving 21.7 TB of data across 728M requests.
61
-
62
- - **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.
63
-
64
- - **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.
65
-
66
65
  It's proudly powered by [microlink.io](https://microlink.io/), the headless browser API that handles all the heavy lifting behind the scenes to ensure your avatars are always ready.
67
66
 
68
67
  ## Quick start
@@ -350,6 +349,16 @@ Available inputs:
350
349
 
351
350
  - Domain, e.g., [unavatar.io/duckduckgo/microsoft.com](https://unavatar.io/duckduckgo/microsoft.com)
352
351
 
352
+ ### Facebook
353
+
354
+ Get any Facebook user, page, or group profile picture by their username or ID.
355
+
356
+ Available inputs:
357
+
358
+ - Username, e.g., [unavatar.io/facebook/zuck](https://unavatar.io/facebook/zuck)
359
+ - Page, e.g., [unavatar.io/facebook/windtodayco](https://unavatar.io/facebook/windtodayco)
360
+ - Group ID, e.g., [unavatar.io/facebook/1426378481098002](https://unavatar.io/facebook/1426378481098002)
361
+
353
362
  ### GitHub
354
363
 
355
364
  Get any GitHub user or organization's profile picture by their username.
@@ -391,6 +400,8 @@ Available inputs:
391
400
 
392
401
  - Domain, e.g., [unavatar.io/google/stremio.com](https://unavatar.io/google/stremio.com)
393
402
 
403
+ ### Gravatar
404
+
394
405
  Get any user's avatar by their email address via Gravatar. The most widely used global avatar service — if your users have a Gravatar set up, this is the fastest way to retrieve it.
395
406
 
396
407
  Available inputs:
@@ -478,6 +489,14 @@ Available inputs:
478
489
 
479
490
  - Username, e.g., [unavatar.io/printables/DukeDoks](https://unavatar.io/printables/DukeDoks)
480
491
 
492
+ ### PSN Profiles
493
+
494
+ Get any PlayStation Network user's profile picture by their PSN username.
495
+
496
+ Available inputs:
497
+
498
+ - Username, e.g., [unavatar.io/psnprofiles/P3](https://unavatar.io/psnprofiles/P3)
499
+
481
500
  ### Reddit
482
501
 
483
502
  Get any Reddit user's avatar by their username.
@@ -571,6 +590,14 @@ Available inputs:
571
590
 
572
591
  - Username, e.g., [unavatar.io/tiktok/carlosazaustre](https://unavatar.io/tiktok/carlosazaustre)
573
592
 
593
+ ### Tumblr
594
+
595
+ Get any Tumblr blog's profile picture by their username.
596
+
597
+ Available inputs:
598
+
599
+ - Username, e.g., [unavatar.io/tumblr/nasa](https://unavatar.io/tumblr/nasa)
600
+
574
601
  ### Twitch
575
602
 
576
603
  Get any Twitch streamer's profile picture by their username.
@@ -603,12 +630,22 @@ Available URI format inputs:
603
630
  - `channel`: [unavatar.io/whatsapp/channel:0029VaARuQ7KwqSXh9fiMc0m](https://unavatar.io/whatsapp/channel:0029VaARuQ7KwqSXh9fiMc0m)
604
631
  - `chat`: [unavatar.io/whatsapp/chat:D2FFycjQXrEIKG8qQjbwZz](https://unavatar.io/whatsapp/chat:D2FFycjQXrEIKG8qQjbwZz)
605
632
 
633
+ ### X/Twitter
634
+
606
635
  Get any X (formerly Twitter) user's profile picture by their username.
607
636
 
608
637
  Available inputs:
609
638
 
610
639
  - Username, e.g., [unavatar.io/x/elonmusk](https://unavatar.io/x/elonmusk)
611
640
 
641
+ ### Xbox Gamertag
642
+
643
+ Get any Xbox player's profile picture by their gamertag.
644
+
645
+ Available inputs:
646
+
647
+ - Gamertag, e.g., [unavatar.io/xboxgamertag/P3](https://unavatar.io/xboxgamertag/P3)
648
+
612
649
  ### YouTube
613
650
 
614
651
  Get any YouTube channel's thumbnail by their handle, legacy username, or channel ID.
@@ -668,6 +705,8 @@ x-rate-limit-remaining: 49
668
705
  x-rate-limit-reset: 1744243200
669
706
  ```
670
707
 
708
+ ## Response Errors
709
+
671
710
  Expected errors are known operational cases returned with stable codes.
672
711
 
673
712
  - **Client-side issues** return `status: "fail"` (HTTP `4xx`).
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@unavatar/core",
3
3
  "description": "Get unified user avatar from social networks, including Instagram, SoundCloud, Telegram, Twitter, YouTube & more.",
4
4
  "homepage": "https://unavatar.io",
5
- "version": "3.19.1",
5
+ "version": "3.19.3",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js",
package/src/index.js CHANGED
@@ -29,16 +29,18 @@ module.exports = ({ constants: userConstants, redis, onFetchHTML } = {}) => {
29
29
  })
30
30
  const createBrowser = require('./util/browserless')(constants)
31
31
  const getHTML = require('./util/html-get')({ createBrowser, got })
32
- const { createHtmlProvider, getOgImage } = require('./util/html-provider')({
33
- ...constants,
34
- getHTML,
35
- onFetchHTML
36
- })
32
+ const { createHtmlProvider, getOgImage, NOT_FOUND } =
33
+ require('./util/html-provider')({
34
+ ...constants,
35
+ getHTML,
36
+ onFetchHTML
37
+ })
37
38
 
38
39
  const providerCtx = {
39
40
  constants,
40
41
  createHtmlProvider,
41
42
  getOgImage,
43
+ NOT_FOUND,
42
44
  got,
43
45
  isReservedIp,
44
46
  itunesSearchCache: cache.itunesSearchCache
@@ -1,8 +1,15 @@
1
1
  'use strict'
2
2
 
3
- module.exports = ({ createHtmlProvider, getOgImage }) =>
3
+ const getAvatarUrl = ({ $, getOgImage, NOT_FOUND }) => {
4
+ const ogImage = getOgImage($)
5
+ return ogImage === undefined ? NOT_FOUND : ogImage
6
+ }
7
+
8
+ module.exports = ({ createHtmlProvider, getOgImage, NOT_FOUND }) =>
4
9
  createHtmlProvider({
5
10
  name: 'psnprofiles',
6
11
  url: input => `https://psnprofiles.com/${input}`,
7
- getter: getOgImage
12
+ getter: $ => getAvatarUrl({ $, getOgImage, NOT_FOUND })
8
13
  })
14
+
15
+ module.exports.getAvatarUrl = getAvatarUrl
@@ -28,22 +28,26 @@ const createProviderError = ({ provider, statusCode, cause, code }) =>
28
28
  message: 'Empty value returned by the provider.'
29
29
  })
30
30
 
31
+ const createErrorCause = ({ html, headers, statusCode }) => ({
32
+ html,
33
+ headers,
34
+ statusCode
35
+ })
36
+
31
37
  module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
32
38
  /**
33
39
  * @param {object} opts
34
40
  * @param {string} opts.name - Provider identifier used in logs and metrics.
35
41
  * @param {(input: string) => string | Promise<string>} opts.url - Builds the URL to fetch for a given input.
36
- * @param {($: cheerio.CheerioAPI) => string | undefined} opts.getter
42
+ * @param {($: cheerio.CheerioAPI) => string | symbol | undefined} opts.getter
37
43
  * Extracts the avatar URL from the fetched HTML.
38
- * - `string` — avatar URL found (success).
44
+ * - `string` — avatar URL found (success).
45
+ * - `NOT_FOUND` — provider-level miss.
39
46
  * - `undefined` — avatar not found (normal failure, no retry).
40
- * @param {(context: { $: cheerio.CheerioAPI, statusCode: number }) => boolean} [opts.isBlocked]
41
- * Optional provider-specific blocked-page detector, checked after the
42
- * default `is-antibot` check when getter returns empty/undefined.
43
47
  * @param {() => object} [opts.htmlOpts] - Returns extra options merged into the fetch call.
44
48
  */
45
- const createHtmlProvider = ({ name, url, getter, isBlocked, htmlOpts }) => {
46
- const provider = async function (input, context) {
49
+ const createHtmlProvider = ({ name, url, getter, htmlOpts }) => {
50
+ async function provider (input, context) {
47
51
  const providerUrl = await url(input)
48
52
 
49
53
  const attempt = async gotOpts => {
@@ -56,7 +60,8 @@ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
56
60
  },
57
61
  timeout: PROXY_TIMEOUT
58
62
  }
59
- const fetchOpts = gotOpts ? { ...defaultOpts, ...gotOpts } : defaultOpts
63
+ const fetchOpts = { ...defaultOpts, ...gotOpts }
64
+ const userAgent = fetchOpts.headers['user-agent']
60
65
  const tier = fetchOpts.tier ?? 'origin'
61
66
 
62
67
  const log = debug.duration({ provider: name, input, providerUrl, tier })
@@ -73,6 +78,11 @@ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
73
78
  : undefined
74
79
  attempt.lastHeaders = responseHeaders
75
80
  attempt.lastStatusCode = statusCode
81
+ const errorCause = createErrorCause({
82
+ html: attempt.lastHtml,
83
+ headers: responseHeaders,
84
+ statusCode
85
+ })
76
86
 
77
87
  if (isStatusCodeMissing(statusCode)) {
78
88
  const code = EMPTY_PROVIDER_VALUE_CODE.MISSING_STATUS_CODE
@@ -80,67 +90,87 @@ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
80
90
  throw createProviderError({
81
91
  provider: name,
82
92
  statusCode,
83
- cause: {
84
- html: attempt.lastHtml,
85
- headers: responseHeaders,
86
- statusCode
87
- },
93
+ cause: errorCause,
88
94
  code
89
95
  })
90
96
  }
91
97
 
98
+ const result = getter($)
92
99
  if (statusCode === httpStatus.NOT_FOUND) {
93
100
  log.error({ statusCode, status: 'not_found' })
94
101
  return NOT_FOUND
95
102
  }
96
103
 
97
- const result = getter($)
98
- if (typeof result !== 'string' || result === '') {
99
- const error = createProviderError({
104
+ function createEmptyGetterResultError () {
105
+ return createProviderError({
100
106
  provider: name,
101
107
  statusCode,
102
108
  code: EMPTY_PROVIDER_VALUE_CODE.EMPTY_GETTER_RESULT,
103
- cause: {
104
- html: attempt.lastHtml,
105
- headers: responseHeaders,
106
- statusCode
107
- }
109
+ cause: errorCause
108
110
  })
111
+ }
109
112
 
113
+ function getBlockedStatus () {
110
114
  const isRateLimited = statusCode === httpStatus.TOO_MANY_REQUESTS
111
- const providerBlocked = isBlocked?.({ $, statusCode })
115
+ if (isRateLimited) return { isBlocked: true, antibotProvider: null }
112
116
 
113
117
  const { detected: antibotDetected, provider: antibotProvider } =
114
- isRateLimited || providerBlocked
115
- ? { detected: false, provider: null }
116
- : isAntibot({
117
- url: providerUrl,
118
- statusCode,
119
- headers: responseHeaders,
120
- body: attempt.lastHtml
121
- })
122
-
123
- if (isRateLimited || providerBlocked || antibotDetected) {
124
- error.blocked = true
118
+ isAntibot({
119
+ url: providerUrl,
120
+ statusCode,
121
+ headers: responseHeaders,
122
+ body: attempt.lastHtml
123
+ })
124
+
125
+ return {
126
+ isBlocked: antibotDetected,
127
+ antibotProvider
125
128
  }
129
+ }
130
+
131
+ if (typeof result === 'string' && result !== '') {
132
+ const normalizedResult = normalizeUrl(providerUrl, result)
133
+ log({
134
+ statusCode,
135
+ status: 'success',
136
+ result: normalizedResult
137
+ })
138
+ return normalizedResult
139
+ }
140
+
141
+ // Some providers encode not-found via getter output. Check antibot
142
+ // first so challenge pages are retried as blocked, not treated as 404.
143
+ const { isBlocked: shouldMarkBlocked, antibotProvider } =
144
+ getBlockedStatus()
145
+
146
+ if (shouldMarkBlocked) {
147
+ const error = createEmptyGetterResultError()
148
+ error.blocked = true
126
149
 
127
150
  log.error({
128
151
  statusCode,
129
- status: error.blocked ? 'blocked' : undefined,
152
+ status: 'blocked',
130
153
  antibot: antibotProvider ?? undefined,
131
- userAgent: fetchOpts.headers['user-agent']
154
+ userAgent
132
155
  })
133
156
 
134
157
  throw error
135
158
  }
136
159
 
137
- const normalizedResult = normalizeUrl(providerUrl, result)
138
- log({
160
+ if (result === NOT_FOUND) {
161
+ log.error({ statusCode, status: 'not_found' })
162
+ return NOT_FOUND
163
+ }
164
+
165
+ const error = createEmptyGetterResultError()
166
+
167
+ log.error({
139
168
  statusCode,
140
- status: 'success',
141
- result: normalizedResult
169
+ antibot: antibotProvider ?? undefined,
170
+ userAgent
142
171
  })
143
- return normalizedResult
172
+
173
+ throw error
144
174
  }
145
175
 
146
176
  if (typeof onFetchHTML === 'function') {
@@ -148,8 +178,7 @@ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
148
178
  }
149
179
 
150
180
  const result = await attempt()
151
- if (result === NOT_FOUND) return
152
- return result
181
+ return result === NOT_FOUND ? undefined : result
153
182
  }
154
183
 
155
184
  provider.getUrl = url