patreon.mjs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import * as dotenv from "dotenv"
  2. import fs from "fs"
  3. dotenv.config({ path: ".env.local" })
  4. const fetchJson = async (url) => {
  5. const res = await fetch(url, {
  6. headers: { Authorization: `Bearer ${process.env.PATREON_ACCESS_TOKEN}` },
  7. })
  8. return await res.json()
  9. }
  10. const sleep = (waitTime) => new Promise(resolve => setTimeout(resolve, waitTime))
  11. let url = `https://www.patreon.com/api/oauth2/v2/campaigns/${process.env.PATREON_CAMPAIGN_ID}/members?include=user,currently_entitled_tiers&fields%5Bmember%5D=full_name,lifetime_support_cents,patron_status,pledge_relationship_start,note&fields%5Buser%5D=image_url&fields%5Btier%5D=title`
  12. const membersByTiers = {}
  13. const tierMap = {}
  14. while (true) {
  15. console.log("Fetching page...")
  16. const data = await fetchJson(url)
  17. const profilePictureMap = {}
  18. if (!data.included) {
  19. console.log("Unexpected response:", data)
  20. break;
  21. }
  22. data.included.forEach((included) => {
  23. switch (included.type) {
  24. case "user":
  25. profilePictureMap[included.id] = included.attributes.image_url
  26. break
  27. case "tier":
  28. tierMap[included.id] = included.attributes.title
  29. break
  30. }
  31. })
  32. data.data.forEach((member) => {
  33. const userId = member.relationships.user.data.id
  34. if (member.attributes.patron_status !== "active_patron") {
  35. return
  36. }
  37. const currentlyEntitledTiers =
  38. member.relationships.currently_entitled_tiers.data
  39. if (currentlyEntitledTiers?.length < 1) {
  40. return
  41. }
  42. const tierId = currentlyEntitledTiers[0].id
  43. const tierName = tierMap[tierId]
  44. const members = membersByTiers[tierName] || []
  45. members.push({
  46. id: userId,
  47. picture: profilePictureMap[userId],
  48. name: member.attributes.full_name,
  49. joinedAt: member.attributes.pledge_relationship_start,
  50. lifetimeSupportCents: member.attributes.lifetime_support_cents,
  51. note: member.attributes.note,
  52. tier: tierName,
  53. })
  54. membersByTiers[tierName] = members
  55. })
  56. url = data?.links?.next
  57. if (!url) {
  58. break
  59. }
  60. await sleep(1000)
  61. }
  62. // Sponsors are sorted by lifetimeSupportCents desc
  63. // Highlighted sponsors are sorted by lifetimeSupportCents desc
  64. // Silver sponsors are sorted by joinedAt asc, grandfathered ones have nofollow: false
  65. const silver = membersByTiers["Silver sponsor"]
  66. .map((member) => ({
  67. url: member.note,
  68. name: member.name,
  69. logo: member.picture,
  70. nofollow: true,
  71. date: member.joinedAt,
  72. }))
  73. .concat(
  74. membersByTiers["Silver sponsor (grandfathered)"].map((member) => ({
  75. url: member.note,
  76. name: member.name,
  77. logo: member.picture,
  78. nofollow: false,
  79. date: member.joinedAt,
  80. }))
  81. )
  82. .sort((a, b) => new Date(a.date) - new Date(b.date))
  83. .map((member) => ({
  84. url: member.url,
  85. logo: member.logo,
  86. name: member.name,
  87. url: member.url,
  88. nofollow: member.nofollow,
  89. }))
  90. const generalHighlighted = membersByTiers["Highlighted sponsor"]
  91. .sort((a, b) => b.lifetimeSupportCents - a.lifetimeSupportCents)
  92. .map((member) => member.name)
  93. const general = membersByTiers["Sponsor"]
  94. .sort((a, b) => b.lifetimeSupportCents - a.lifetimeSupportCents)
  95. .map((member) => member.name)
  96. fs.writeFile(
  97. "./data/patreon.json",
  98. JSON.stringify(
  99. {
  100. silver,
  101. generalHighlighted,
  102. general,
  103. },
  104. null,
  105. " "
  106. ),
  107. (err) => {
  108. if (err) {
  109. console.error(err)
  110. return
  111. }
  112. console.log("File updated")
  113. }
  114. )