@unavatar/core 3.19.3 → 3.19.4
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/package.json +2 -2
- package/src/index.js +1 -0
- package/src/providers/github.js +141 -5
- package/src/providers/index.js +1 -1
- package/src/util/cache.js +4 -0
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.
|
|
5
|
+
"version": "3.19.4",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.js",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"ms": "~2.1.3",
|
|
140
140
|
"p-any": "~3.0.0",
|
|
141
141
|
"p-timeout": "~4.1.0",
|
|
142
|
-
"puppeteer": "~24.
|
|
142
|
+
"puppeteer": "~24.41.0",
|
|
143
143
|
"re2": "~1.24.0",
|
|
144
144
|
"srcset": "~4.0.0",
|
|
145
145
|
"stable-regex": "~1.0.1",
|
package/src/index.js
CHANGED
|
@@ -43,6 +43,7 @@ module.exports = ({ constants: userConstants, redis, onFetchHTML } = {}) => {
|
|
|
43
43
|
NOT_FOUND,
|
|
44
44
|
got,
|
|
45
45
|
isReservedIp,
|
|
46
|
+
githubSearchCache: cache.githubSearchCache,
|
|
46
47
|
itunesSearchCache: cache.itunesSearchCache
|
|
47
48
|
}
|
|
48
49
|
const { providers, providersBy } = require('./providers')(providerCtx)
|
package/src/providers/github.js
CHANGED
|
@@ -1,10 +1,146 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const memoize = require('@keyvhq/memoize')
|
|
3
4
|
const { stringify } = require('querystring')
|
|
5
|
+
const isEmail = require('is-email-like')
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const GITHUB_API_URL = 'https://api.github.com'
|
|
8
|
+
const SEARCH_USERS_PER_PAGE = 10
|
|
9
|
+
const SEARCH_COMMITS_PER_PAGE = 20
|
|
10
|
+
const COMMIT_SEARCH_ACCEPT_HEADER = 'application/vnd.github+json'
|
|
11
|
+
|
|
12
|
+
const normalizeCacheKey = value => value.trim().toLowerCase()
|
|
13
|
+
const createLookupCacheKey = prefix => value =>
|
|
14
|
+
`${prefix}:${normalizeCacheKey(value)}`
|
|
15
|
+
|
|
16
|
+
const fetchJsonBody = async ({ got, url, options }) => {
|
|
17
|
+
const { body } = await got(url, {
|
|
18
|
+
responseType: 'json',
|
|
19
|
+
...options
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return body
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const getUsernameAvatarUrl = ({ constants, input }) =>
|
|
26
|
+
`https://github.com/${input}.png?${stringify({
|
|
27
|
+
size: constants.AVATAR_SIZE
|
|
28
|
+
})}`
|
|
29
|
+
|
|
30
|
+
const createSearchUsersByEmail = ({ githubSearchCache, got }) =>
|
|
31
|
+
memoize(
|
|
32
|
+
async email => {
|
|
33
|
+
const body = await fetchJsonBody({
|
|
34
|
+
got,
|
|
35
|
+
url: `${GITHUB_API_URL}/search/users?q=${encodeURIComponent(
|
|
36
|
+
email
|
|
37
|
+
)}&per_page=${SEARCH_USERS_PER_PAGE}`
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return body?.items ?? []
|
|
41
|
+
},
|
|
42
|
+
githubSearchCache,
|
|
43
|
+
{ key: createLookupCacheKey('search-users') }
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const createGetUser = ({ githubSearchCache, got }) =>
|
|
47
|
+
memoize(
|
|
48
|
+
login =>
|
|
49
|
+
fetchJsonBody({
|
|
50
|
+
got,
|
|
51
|
+
url: `${GITHUB_API_URL}/users/${encodeURIComponent(login)}`
|
|
52
|
+
}),
|
|
53
|
+
githubSearchCache,
|
|
54
|
+
{ key: createLookupCacheKey('user') }
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const createSearchCommitsByEmail = ({ githubSearchCache, got }) =>
|
|
58
|
+
memoize(
|
|
59
|
+
async email => {
|
|
60
|
+
const body = await fetchJsonBody({
|
|
61
|
+
got,
|
|
62
|
+
url: `${GITHUB_API_URL}/search/commits?q=${encodeURIComponent(
|
|
63
|
+
`author-email:${email}`
|
|
64
|
+
)}&per_page=${SEARCH_COMMITS_PER_PAGE}`,
|
|
65
|
+
options: {
|
|
66
|
+
headers: { accept: COMMIT_SEARCH_ACCEPT_HEADER }
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return body?.items ?? []
|
|
71
|
+
},
|
|
72
|
+
githubSearchCache,
|
|
73
|
+
{ key: createLookupCacheKey('search-commits') }
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const findExactPublicProfileMatch = async ({
|
|
77
|
+
email,
|
|
78
|
+
getUser,
|
|
79
|
+
searchUsersByEmail
|
|
80
|
+
}) => {
|
|
81
|
+
const candidates = await searchUsersByEmail(email)
|
|
82
|
+
const normalizedEmail = email.toLowerCase()
|
|
83
|
+
|
|
84
|
+
for (const candidate of candidates) {
|
|
85
|
+
const user = await getUser(candidate.login)
|
|
86
|
+
|
|
87
|
+
if (user?.email?.toLowerCase() === normalizedEmail) {
|
|
88
|
+
return user.avatar_url
|
|
89
|
+
}
|
|
10
90
|
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const findCommitConsensusMatch = async ({ email, searchCommitsByEmail }) => {
|
|
94
|
+
const commits = await searchCommitsByEmail(email)
|
|
95
|
+
const counts = new Map()
|
|
96
|
+
|
|
97
|
+
for (const item of commits) {
|
|
98
|
+
const linkedUser = item.author ?? item.committer
|
|
99
|
+
if (!linkedUser?.login || !linkedUser?.avatar_url) continue
|
|
100
|
+
|
|
101
|
+
const entry = counts.get(linkedUser.login) ?? {
|
|
102
|
+
avatarUrl: linkedUser.avatar_url,
|
|
103
|
+
count: 0
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
entry.count += 1
|
|
107
|
+
counts.set(linkedUser.login, entry)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let winner
|
|
111
|
+
for (const entry of counts.values()) {
|
|
112
|
+
if (!winner || entry.count > winner.count) winner = entry
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return winner?.avatarUrl
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = ({ constants, githubSearchCache, got }) => {
|
|
119
|
+
const searchUsersByEmail = createSearchUsersByEmail({
|
|
120
|
+
githubSearchCache,
|
|
121
|
+
got
|
|
122
|
+
})
|
|
123
|
+
const getUser = createGetUser({ githubSearchCache, got })
|
|
124
|
+
const searchCommitsByEmail = createSearchCommitsByEmail({
|
|
125
|
+
githubSearchCache,
|
|
126
|
+
got
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return async function github (input) {
|
|
130
|
+
if (!isEmail(input)) return getUsernameAvatarUrl({ constants, input })
|
|
131
|
+
|
|
132
|
+
const exactMatch = await findExactPublicProfileMatch({
|
|
133
|
+
email: input,
|
|
134
|
+
getUser,
|
|
135
|
+
searchUsersByEmail
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if (exactMatch) return exactMatch
|
|
139
|
+
|
|
140
|
+
return findCommitConsensusMatch({ email: input, searchCommitsByEmail })
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports.getUsernameAvatarUrl = getUsernameAvatarUrl
|
|
145
|
+
module.exports.findExactPublicProfileMatch = findExactPublicProfileMatch
|
|
146
|
+
module.exports.findCommitConsensusMatch = findCommitConsensusMatch
|
package/src/providers/index.js
CHANGED
package/src/util/cache.js
CHANGED
|
@@ -4,6 +4,10 @@ const ms = require('ms')
|
|
|
4
4
|
|
|
5
5
|
module.exports = ({ createMultiCache, createRedisCache }) => ({
|
|
6
6
|
dnsCache: createMultiCache(createRedisCache({ namespace: 'dns', ttl: ms('1d') })),
|
|
7
|
+
githubSearchCache: createRedisCache({
|
|
8
|
+
namespace: 'github-search',
|
|
9
|
+
ttl: ms('1d')
|
|
10
|
+
}),
|
|
7
11
|
pingCache: createMultiCache(createRedisCache({ namespace: 'ping', ttl: ms('1d') })),
|
|
8
12
|
itunesSearchCache: createRedisCache({ namespace: 'itunes-search', ttl: ms('7d') })
|
|
9
13
|
})
|