@unavatar/core 3.12.0 → 3.14.0
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 +20 -0
- package/bin/index.js +28 -9
- package/package.json +1 -1
- package/src/providers/apple-music.js +5 -3
- package/src/providers/behance.js +12 -0
- package/src/providers/discord.js +96 -0
- package/src/providers/index.js +4 -0
- package/src/providers/mastodon.js +6 -6
- package/src/providers/x.js +4 -7
- package/src/util/get-og-image.js +3 -0
- package/src/util/html-provider.js +2 -5
package/README.md
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
- [Pricing](#pricing)
|
|
11
11
|
- [Providers](#providers)
|
|
12
12
|
- [Apple Music](#apple-music)
|
|
13
|
+
- [Behance](#behance)
|
|
13
14
|
- [Bluesky](#bluesky)
|
|
14
15
|
- [DeviantArt](#deviantart)
|
|
16
|
+
- [Discord](#discord)
|
|
15
17
|
- [Dribbble](#dribbble)
|
|
16
18
|
- [DuckDuckGo](#duckduckgo)
|
|
17
19
|
- [GitHub](#github)
|
|
@@ -55,8 +57,10 @@
|
|
|
55
57
|
- [Pricing](#pricing)
|
|
56
58
|
- [Providers](#providers)
|
|
57
59
|
- [Apple Music](#apple-music)
|
|
60
|
+
- [Behance](#behance)
|
|
58
61
|
- [Bluesky](#bluesky)
|
|
59
62
|
- [DeviantArt](#deviantart)
|
|
63
|
+
- [Discord](#discord)
|
|
60
64
|
- [Dribbble](#dribbble)
|
|
61
65
|
- [DuckDuckGo](#duckduckgo)
|
|
62
66
|
- [GitHub](#github)
|
|
@@ -221,6 +225,14 @@ Available URI format inputs:
|
|
|
221
225
|
- by song name: [unavatar.io/apple-music/song:harder%20better%20faster%20stronger](https://unavatar.io/apple-music/song:harder%20better%20faster%20stronger)
|
|
222
226
|
- by song ID: [unavatar.io/apple-music/song:697195787](https://unavatar.io/apple-music/song:697195787)
|
|
223
227
|
|
|
228
|
+
### Behance
|
|
229
|
+
|
|
230
|
+
Get any Behance user's profile picture by their username.
|
|
231
|
+
|
|
232
|
+
Available inputs:
|
|
233
|
+
|
|
234
|
+
- slug, e.g., [unavatar.io/behance/kikobeats](https://unavatar.io/behance/kikobeats)
|
|
235
|
+
|
|
224
236
|
### Bluesky
|
|
225
237
|
|
|
226
238
|
Get any Bluesky user's profile picture by their handle. Domain-style handles are supported.
|
|
@@ -238,6 +250,14 @@ Available inputs:
|
|
|
238
250
|
|
|
239
251
|
- slug, e.g., [unavatar.io/deviantart/spyed](https://unavatar.io/deviantart/spyed)
|
|
240
252
|
|
|
253
|
+
### Discord
|
|
254
|
+
|
|
255
|
+
Get any Discord server icon by invite code.
|
|
256
|
+
|
|
257
|
+
Available inputs:
|
|
258
|
+
|
|
259
|
+
- Invite code, e.g., [unavatar.io/discord/eret](https://unavatar.io/discord/eret)
|
|
260
|
+
|
|
241
261
|
### Dribbble
|
|
242
262
|
|
|
243
263
|
Get any Dribbble designer's profile picture by their username.
|
package/bin/index.js
CHANGED
|
@@ -28,7 +28,9 @@ module.exports = ({ baseUrl }) => {
|
|
|
28
28
|
const customHeaders = parseHeaders(flags.header)
|
|
29
29
|
|
|
30
30
|
if (!input) {
|
|
31
|
-
console.error(
|
|
31
|
+
console.error(
|
|
32
|
+
'Usage: unavatar <input> | unavatar <provider>/<key> | unavatar ping'
|
|
33
|
+
)
|
|
32
34
|
console.error(
|
|
33
35
|
'Examples: unavatar reddit.com | unavatar hello@microlink.io | unavatar x/kikobeats | unavatar ping'
|
|
34
36
|
)
|
|
@@ -39,7 +41,9 @@ module.exports = ({ baseUrl }) => {
|
|
|
39
41
|
const normalizeInput = value => {
|
|
40
42
|
try {
|
|
41
43
|
const url = new URL(value)
|
|
42
|
-
if (url.protocol === 'http:' || url.protocol === 'https:')
|
|
44
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
45
|
+
return url.hostname
|
|
46
|
+
}
|
|
43
47
|
} catch (_) {}
|
|
44
48
|
return value
|
|
45
49
|
}
|
|
@@ -57,7 +61,9 @@ module.exports = ({ baseUrl }) => {
|
|
|
57
61
|
const key = keyParts.join('/')
|
|
58
62
|
|
|
59
63
|
if (!provider || !key) {
|
|
60
|
-
console.error(
|
|
64
|
+
console.error(
|
|
65
|
+
'Invalid input format. Expected: <input>, <provider>/<key>, or "ping"'
|
|
66
|
+
)
|
|
61
67
|
console.error(
|
|
62
68
|
'Examples: unavatar reddit.com | unavatar hello@microlink.io | unavatar x/kikobeats | unavatar ping'
|
|
63
69
|
)
|
|
@@ -78,15 +84,22 @@ module.exports = ({ baseUrl }) => {
|
|
|
78
84
|
let durationInfo = null
|
|
79
85
|
let apiHeaders = null
|
|
80
86
|
let apiStatusCode = null
|
|
87
|
+
let apiHttpVersion = null
|
|
81
88
|
|
|
82
89
|
const logMeta = () => {
|
|
83
90
|
if (apiHeaders) {
|
|
84
91
|
const headerEntries = Object.entries(apiHeaders)
|
|
85
92
|
if (headerEntries.length > 0) {
|
|
86
|
-
const maxHeaderLength = Math.max(
|
|
93
|
+
const maxHeaderLength = Math.max(
|
|
94
|
+
...headerEntries.map(([key]) => key.toLowerCase().length)
|
|
95
|
+
)
|
|
87
96
|
|
|
88
97
|
console.error()
|
|
89
|
-
console.error(
|
|
98
|
+
console.error(
|
|
99
|
+
`HTTP/${apiHttpVersion || '1.1'} ${apiStatusCode} ${
|
|
100
|
+
STATUS_CODES[apiStatusCode]
|
|
101
|
+
}`
|
|
102
|
+
)
|
|
90
103
|
headerEntries
|
|
91
104
|
.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
|
92
105
|
.forEach(([key, value]) => {
|
|
@@ -106,13 +119,16 @@ module.exports = ({ baseUrl }) => {
|
|
|
106
119
|
if (flags.apiKey) requestHeaders['x-api-key'] = flags.apiKey
|
|
107
120
|
|
|
108
121
|
got(apiUrl.toString(), { headers: requestHeaders, responseType: 'json' })
|
|
109
|
-
.then(({ body, headers, statusCode, timings }) => {
|
|
122
|
+
.then(({ body, headers, statusCode, timings, httpVersion }) => {
|
|
110
123
|
apiHeaders = headers
|
|
111
124
|
apiStatusCode = statusCode
|
|
125
|
+
apiHttpVersion = httpVersion
|
|
112
126
|
|
|
113
127
|
const duration = Date.now() - startTime
|
|
114
128
|
const timeToFirstByte =
|
|
115
|
-
timings?.response && timings?.start
|
|
129
|
+
timings?.response && timings?.start
|
|
130
|
+
? Math.round(timings.response - timings.start)
|
|
131
|
+
: null
|
|
116
132
|
|
|
117
133
|
durationInfo = timeToFirstByte
|
|
118
134
|
? `Duration: ${duration}ms (TTFB: ${timeToFirstByte}ms)`
|
|
@@ -123,7 +139,7 @@ module.exports = ({ baseUrl }) => {
|
|
|
123
139
|
console.error(`${apiUrl.toString()}\n`)
|
|
124
140
|
console.error(body)
|
|
125
141
|
logMeta()
|
|
126
|
-
return
|
|
142
|
+
return
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
if (!body || !body.url) {
|
|
@@ -152,6 +168,7 @@ output: ${imageUrl}
|
|
|
152
168
|
if (response) {
|
|
153
169
|
apiHeaders = response.headers
|
|
154
170
|
apiStatusCode = response.statusCode
|
|
171
|
+
apiHttpVersion = response.httpVersion
|
|
155
172
|
}
|
|
156
173
|
|
|
157
174
|
if (!durationInfo) {
|
|
@@ -166,7 +183,9 @@ output: ${imageUrl}
|
|
|
166
183
|
if (typeof body === 'object') {
|
|
167
184
|
console.error(JSON.stringify(body, null, 2))
|
|
168
185
|
} else {
|
|
169
|
-
const rawBody = Buffer.isBuffer(body)
|
|
186
|
+
const rawBody = Buffer.isBuffer(body)
|
|
187
|
+
? body.toString('utf8')
|
|
188
|
+
: String(body)
|
|
170
189
|
try {
|
|
171
190
|
console.error(JSON.stringify(JSON.parse(rawBody), null, 2))
|
|
172
191
|
} catch (_) {
|
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.
|
|
5
|
+
"version": "3.14.0",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.js",
|
|
@@ -39,7 +39,7 @@ module.exports = ({ createHtmlProvider, itunesSearchCache, got }) => {
|
|
|
39
39
|
const searchEntityId = memoize(
|
|
40
40
|
async ({ query, type }) => {
|
|
41
41
|
const entityConfig = APPLE_MUSIC_ENTITY_TYPES[type]
|
|
42
|
-
if (!entityConfig) return
|
|
42
|
+
if (!entityConfig) return
|
|
43
43
|
const { entity, idKey } = entityConfig
|
|
44
44
|
|
|
45
45
|
const url = `https://itunes.apple.com/search?term=${encodeURIComponent(
|
|
@@ -63,7 +63,7 @@ module.exports = ({ createHtmlProvider, itunesSearchCache, got }) => {
|
|
|
63
63
|
if (!hasExplicitType) {
|
|
64
64
|
for (const searchType of APPLE_MUSIC_SEARCH_TYPES) {
|
|
65
65
|
const entityId = await searchEntityId({ query: id, type: searchType })
|
|
66
|
-
if (entityId) return `${APPLE_MUSIC_STOREFRONT}/${searchType}/${entityId}`
|
|
66
|
+
if (entityId) { return `${APPLE_MUSIC_STOREFRONT}/${searchType}/${entityId}` }
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
return `${APPLE_MUSIC_STOREFRONT}/search?term=${encodeURIComponent(id)}`
|
|
@@ -76,7 +76,9 @@ module.exports = ({ createHtmlProvider, itunesSearchCache, got }) => {
|
|
|
76
76
|
if (isNumericId(id)) return `${APPLE_MUSIC_STOREFRONT}/${type}/${id}`
|
|
77
77
|
|
|
78
78
|
const entityId = await searchEntityId({ query: id, type })
|
|
79
|
-
return `${APPLE_MUSIC_STOREFRONT}/${type}/${
|
|
79
|
+
return `${APPLE_MUSIC_STOREFRONT}/${type}/${
|
|
80
|
+
entityId || encodeURIComponent(id)
|
|
81
|
+
}`
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
return createHtmlProvider({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const getAvatarUrl = input => `https://www.behance.net/${input}`
|
|
4
|
+
|
|
5
|
+
module.exports = ({ createHtmlProvider, getOgImage }) =>
|
|
6
|
+
createHtmlProvider({
|
|
7
|
+
name: 'behance',
|
|
8
|
+
url: getAvatarUrl,
|
|
9
|
+
getter: getOgImage
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
module.exports.getAvatarUrl = getAvatarUrl
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const DISCORD_INVITE_HOST_RE =
|
|
4
|
+
/^(?:www\.)?(?:discord\.gg|discord(?:app)?\.com)$/i
|
|
5
|
+
|
|
6
|
+
const DISCORD_GUILD_ID_RE =
|
|
7
|
+
/^https:\/\/cdn\.discordapp\.com\/splashes\/(\d+)\/[^/?#]+\.[^/?#]+(?:\?[^#]+)?(?:#.*)?$/
|
|
8
|
+
|
|
9
|
+
const DISCORD_INVITE_URL_BASE = 'https://discord.com/invite/'
|
|
10
|
+
|
|
11
|
+
const getInviteCode = input => {
|
|
12
|
+
if (typeof input !== 'string') return
|
|
13
|
+
|
|
14
|
+
const value = input.trim()
|
|
15
|
+
if (value === '') return
|
|
16
|
+
|
|
17
|
+
const hasProtocol = value.includes('://')
|
|
18
|
+
const hasPath = value.includes('/')
|
|
19
|
+
|
|
20
|
+
if (!hasProtocol && !hasPath) return value
|
|
21
|
+
|
|
22
|
+
const normalizedInput = hasProtocol ? value : `https://${value}`
|
|
23
|
+
|
|
24
|
+
let url
|
|
25
|
+
try {
|
|
26
|
+
url = new URL(normalizedInput)
|
|
27
|
+
} catch {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!DISCORD_INVITE_HOST_RE.test(url.hostname)) return
|
|
32
|
+
|
|
33
|
+
const [firstSegment, secondSegment] = url.pathname.split('/').filter(Boolean)
|
|
34
|
+
if (!firstSegment) return
|
|
35
|
+
|
|
36
|
+
if (url.hostname.includes('discord.gg')) return firstSegment
|
|
37
|
+
|
|
38
|
+
if (firstSegment === 'invite' || firstSegment === 'invites') {
|
|
39
|
+
return secondSegment
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const buildInviteUrl = inviteCode => `${DISCORD_INVITE_URL_BASE}${inviteCode}`
|
|
44
|
+
|
|
45
|
+
const getInviteUrl = input => {
|
|
46
|
+
const inviteCode = getInviteCode(input)
|
|
47
|
+
return inviteCode ? buildInviteUrl(inviteCode) : undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const getInviteApiUrl = inviteCode =>
|
|
51
|
+
`https://discord.com/api/v9/invites/${encodeURIComponent(
|
|
52
|
+
inviteCode
|
|
53
|
+
)}?with_counts=true&with_expiration=true`
|
|
54
|
+
|
|
55
|
+
const getGuildIdFromOgImage = ogImage =>
|
|
56
|
+
ogImage?.match(DISCORD_GUILD_ID_RE)?.[1]
|
|
57
|
+
|
|
58
|
+
const getAvatarUrl = ({ guildId, ogImage, iconHash }) => {
|
|
59
|
+
const resolvedGuildId = guildId || getGuildIdFromOgImage(ogImage)
|
|
60
|
+
if (!resolvedGuildId || !iconHash) return
|
|
61
|
+
return `https://cdn.discordapp.com/icons/${resolvedGuildId}/${iconHash}.webp`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = ({ createHtmlProvider, getOgImage, got }) => {
|
|
65
|
+
const fromInvite = createHtmlProvider({
|
|
66
|
+
name: 'discord',
|
|
67
|
+
url: buildInviteUrl,
|
|
68
|
+
getter: getOgImage
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return async function discord (input, context) {
|
|
72
|
+
const inviteCode = getInviteCode(input)
|
|
73
|
+
if (!inviteCode) return
|
|
74
|
+
|
|
75
|
+
const ogImage = await fromInvite(inviteCode, context)
|
|
76
|
+
if (!ogImage) return
|
|
77
|
+
|
|
78
|
+
const metadataResponse = await got(getInviteApiUrl(inviteCode), {
|
|
79
|
+
responseType: 'json',
|
|
80
|
+
throwHttpErrors: false
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (metadataResponse.statusCode >= 400) return
|
|
84
|
+
|
|
85
|
+
const guild = metadataResponse.body?.guild
|
|
86
|
+
const iconHash = guild?.icon
|
|
87
|
+
const guildId = guild?.id
|
|
88
|
+
|
|
89
|
+
return getAvatarUrl({ guildId, ogImage, iconHash })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports.getAvatarUrl = getAvatarUrl
|
|
94
|
+
module.exports.getGuildIdFromOgImage = getGuildIdFromOgImage
|
|
95
|
+
module.exports.getInviteCode = getInviteCode
|
|
96
|
+
module.exports.getInviteUrl = getInviteUrl
|
package/src/providers/index.js
CHANGED
|
@@ -4,8 +4,10 @@ const providersBy = {
|
|
|
4
4
|
email: ['gravatar'],
|
|
5
5
|
username: [
|
|
6
6
|
'apple-music',
|
|
7
|
+
'behance',
|
|
7
8
|
'bluesky',
|
|
8
9
|
'deviantart',
|
|
10
|
+
'discord',
|
|
9
11
|
'dribbble',
|
|
10
12
|
'github',
|
|
11
13
|
'gitlab',
|
|
@@ -38,8 +40,10 @@ const providersBy = {
|
|
|
38
40
|
module.exports = ctx => {
|
|
39
41
|
const providers = {
|
|
40
42
|
'apple-music': require('./apple-music')(ctx),
|
|
43
|
+
behance: require('./behance')(ctx),
|
|
41
44
|
bluesky: require('./bluesky')(ctx),
|
|
42
45
|
deviantart: require('./deviantart')(ctx),
|
|
46
|
+
discord: require('./discord')(ctx),
|
|
43
47
|
dribbble: require('./dribbble')(ctx),
|
|
44
48
|
duckduckgo: require('./duckduckgo')(ctx),
|
|
45
49
|
github: require('./github')(ctx),
|
|
@@ -18,15 +18,15 @@ const isValidServer = server => {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const parseMastodonInput = input => {
|
|
21
|
-
if (typeof input !== 'string') return
|
|
21
|
+
if (typeof input !== 'string') return
|
|
22
22
|
|
|
23
23
|
const cleaned = input.startsWith('@') ? input.slice(1) : input
|
|
24
24
|
const parts = cleaned.split('@')
|
|
25
|
-
if (parts.length !== 2) return
|
|
25
|
+
if (parts.length !== 2) return
|
|
26
26
|
|
|
27
27
|
const [username, server] = parts
|
|
28
|
-
if (!username || !server) return
|
|
29
|
-
if (!isValidServer(server)) return
|
|
28
|
+
if (!username || !server) return
|
|
29
|
+
if (!isValidServer(server)) return
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
32
|
username,
|
|
@@ -37,11 +37,11 @@ const parseMastodonInput = input => {
|
|
|
37
37
|
module.exports = ({ got, isReservedIp }) => {
|
|
38
38
|
const mastodon = async function (input) {
|
|
39
39
|
const parsed = parseMastodonInput(input)
|
|
40
|
-
if (!parsed) return
|
|
40
|
+
if (!parsed) return
|
|
41
41
|
|
|
42
42
|
const { username, server } = parsed
|
|
43
43
|
|
|
44
|
-
if (await isReservedIp(server)) return
|
|
44
|
+
if (await isReservedIp(server)) return
|
|
45
45
|
|
|
46
46
|
const { body } = await got(
|
|
47
47
|
`https://${server}/api/v1/accounts/lookup?acct=${encodeURIComponent(
|
package/src/providers/x.js
CHANGED
|
@@ -12,17 +12,14 @@ const toHighResolution = url => {
|
|
|
12
12
|
return url
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const getProfileImage =
|
|
16
|
-
toHighResolution(
|
|
17
|
-
$jsonld('mainEntity.image.contentUrl')($) ||
|
|
18
|
-
$('meta[property="og:image"]').attr('content')
|
|
19
|
-
)
|
|
15
|
+
const getProfileImage = ($, getOgImage) =>
|
|
16
|
+
toHighResolution($jsonld('mainEntity.image.contentUrl')($) || getOgImage($))
|
|
20
17
|
|
|
21
|
-
module.exports = ({ createHtmlProvider }) =>
|
|
18
|
+
module.exports = ({ createHtmlProvider, getOgImage }) =>
|
|
22
19
|
createHtmlProvider({
|
|
23
20
|
name: 'x',
|
|
24
21
|
url: input => `https://x.com/${input}`,
|
|
25
|
-
getter: getProfileImage
|
|
22
|
+
getter: $ => getProfileImage($, getOgImage)
|
|
26
23
|
})
|
|
27
24
|
|
|
28
25
|
module.exports.getProfileImage = getProfileImage
|
|
@@ -5,6 +5,7 @@ const debug = require('debug-logfmt')('html-provider')
|
|
|
5
5
|
const isAntibot = require('is-antibot')
|
|
6
6
|
|
|
7
7
|
const randomCrawlerAgent = require('./crawler-agent')
|
|
8
|
+
const getOgImage = require('./get-og-image')
|
|
8
9
|
const httpStatus = require('./http-status')
|
|
9
10
|
const ExtendableError = require('./error')
|
|
10
11
|
|
|
@@ -27,10 +28,6 @@ const createProviderError = ({ provider, statusCode, cause, code }) =>
|
|
|
27
28
|
message: 'Empty value returned by the provider.'
|
|
28
29
|
})
|
|
29
30
|
|
|
30
|
-
const getOgImage = $ =>
|
|
31
|
-
$('meta[property="og:image"]').attr('content') ||
|
|
32
|
-
$('meta[name="og:image"]').attr('content')
|
|
33
|
-
|
|
34
31
|
module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
|
|
35
32
|
/**
|
|
36
33
|
* @param {object} opts
|
|
@@ -151,7 +148,7 @@ module.exports = ({ PROXY_TIMEOUT, getHTML, onFetchHTML }) => {
|
|
|
151
148
|
}
|
|
152
149
|
|
|
153
150
|
const result = await attempt()
|
|
154
|
-
if (result === NOT_FOUND) return
|
|
151
|
+
if (result === NOT_FOUND) return
|
|
155
152
|
return result
|
|
156
153
|
}
|
|
157
154
|
|