diff --git a/connector/src/platforms/linkedin.js b/connector/src/platforms/linkedin.js index 7999592..a8949d7 100644 --- a/connector/src/platforms/linkedin.js +++ b/connector/src/platforms/linkedin.js @@ -46,6 +46,34 @@ async function uploadVideo(token, owner, src) { return video; } +// Company Page names. LinkedIn caps lookups per app and member per day, so names are cached for a day and +// fetched in batches of 50, from organizationsLookup first and the organizations resource if that is capped. +const NAME_TTL = 24 * 3600 * 1000; +const nameCache = new Map(); +async function pageNames(token, ids) { + const names = {}; + const missing = []; + for (const id of ids) { + const hit = nameCache.get(id); + if (hit && Date.now() - hit.at < NAME_TTL) names[id] = hit.name; else missing.push(id); + } + for (let i = 0; i < missing.length; i += 50) { + const list = `List(${missing.slice(i, i + 50).join(",")})`; + for (const resource of ["organizationsLookup", "organizations"]) { + try { + const r = await request("LinkedIn", `${API}/rest/${resource}?ids=${list}`, rest(token)); + for (const [id, o] of Object.entries(r.data.results || {})) { + if (!o.localizedName) continue; + names[id] = o.localizedName; + nameCache.set(id, { name: o.localizedName, at: Date.now() }); + } + break; + } catch { /* capped or not allowed: try the next resource, else keep ids */ } + } + } + return names; +} + export default { id: "linkedin", name: "LinkedIn", @@ -84,14 +112,7 @@ export default { try { const { data } = await request("LinkedIn", `${API}/rest/organizationAcls`, { ...rest(tokens.accessToken), query: { q: "roleAssignee", role: "ADMINISTRATOR", state: "APPROVED" } }); const ids = (data.elements || []).map((e) => String(e.organization).split(":").pop()); - // One batch call per 50 Pages: single-Page lookups have a small daily limit per member. - const names = {}; - for (let i = 0; i < ids.length; i += 50) { - try { - const r = await request("LinkedIn", `${API}/rest/organizations?ids=List(${ids.slice(i, i + 50).join(",")})`, rest(tokens.accessToken)); - for (const [id, o] of Object.entries(r.data.results || {})) names[id] = o.localizedName; - } catch { /* keep ids */ } - } + const names = await pageNames(tokens.accessToken, ids); for (const id of ids) out.push({ id: `urn:li:organization:${id}`, name: `${names[id] || `Company ${id}`} (company Page)` }); } catch { /* no organization scopes: personal profile only */ } return out; diff --git a/connector/test/platforms.test.mjs b/connector/test/platforms.test.mjs index 3752e55..cd12960 100644 --- a/connector/test/platforms.test.mjs +++ b/connector/test/platforms.test.mjs @@ -141,7 +141,7 @@ test("LinkedIn: connect, list company Pages, post image to a company Page", asyn const { conn } = await connect("linkedin", "acme", { identify: [ ["GET", "https://api.linkedin.com/v2/me", json({ id: "abc", localizedFirstName: "Pat", localizedLastName: "Lee" })], ["GET", "https://api.linkedin.com/rest/organizationAcls", json({ elements: [{ organization: "urn:li:organization:42" }] })], - ["GET", "https://api.linkedin.com/rest/organizations?ids=List(42)", json({ results: { 42: { localizedName: "Acme Co" } } })], + ["GET", "https://api.linkedin.com/rest/organizationsLookup?ids=List(42)", json({ results: { 42: { localizedName: "Acme Co" } } })], ] }); assert.equal(conn.account.name, "Pat Lee"); const targets = await P.targetsFor(conn.id);