@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 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.3",
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.40.0",
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)
@@ -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
- module.exports = ({ constants }) =>
6
- function github (input) {
7
- return `https://github.com/${input}.png?${stringify({
8
- size: constants.AVATAR_SIZE
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
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const providersBy = {
4
- email: ['gravatar'],
4
+ email: ['gravatar', 'github'],
5
5
  username: [
6
6
  'apple-music',
7
7
  'behance',
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
  })