Files
gui/scripts/takeout/check-discord-sync.ts
zeekayandHanzo Dev 69c6e860d7 transplant onto tamagui v2.6.0, brand as a re-runnable transform
This repo could never sync. There was NO merge-base with tamagui — 372 commits
here against 15,791 there, unrelated histories — because it began as a
copy-paste rebrand with a flattened layout. Every "sync" since was a hand
re-application that drifted, which is why the tree sat on 7.3.1 while upstream
shipped fixes we then rediscovered by hand.

This commit's PARENT IS TAMAGUI. A future sync is `git merge tama/main` then
`node scripts/rebrand.mjs` — a real merge, because the history is now shared.

174 packages: 166 upstream, our 8 restored (chrome, shell, telemetry, recipes,
get, font-geist-{sans,mono}, @hanzo/booker), bento-get gained. 170/170 build.

Our patches to files UPSTREAM ALSO OWNS are carried, which the package-level
delta missed and which was where the value actually was:
  - useMedia's server snapshot (`initState ?? getMedia()`). Upstream still
    returns undefined there, so every Tamagui Dialog kills a prerender — the
    exact crash hanzo.app spent a day on.
  - data-slot on web host nodes, absent upstream entirely.
Both applied ONTO upstream's newer files (it rewrote useMedia +150/-63 and
createComponent +160/-55), marked `PATCH (ours, carried across the transplant)`.

Seven traps are encoded in scripts/rebrand.mjs, each found by a gate:
skip itself and lockfiles; special-case the double-branded names in BOTH scoped
and bare form; map path strings, not just paths; drop a level for the flattened
group; check disk rather than a stale index.

Rust build artifacts are NOT committed. An earlier draft of this commit swept
apps/desktop/src-tauri/target/debug in via `git add -A` — 3,859 files including
a 272MB .a, over GitHub's 100MB cap, so the push could never have landed. They
are gitignored now.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-07-30 18:25:06 -07:00

271 lines
8.1 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Check Discord role assignments vs active Stripe subscriptions
*
* Usage:
* tsx scripts/takeout/check-discord-sync.ts
*/
import { createClient } from '@supabase/supabase-js'
import Stripe from 'stripe'
import { Client, GatewayIntentBits } from '@discordjs/core'
import { REST } from '@discordjs/rest'
import { WebSocketManager } from '@discordjs/ws'
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN
// Discord IDs from features/discord/helpers.ts
const TAKEOUT_ROLE_ID = '1131082605052301403'
const GUI_DISCORD_GUILD_ID = '909986013848412191'
// Products that grant early-access
const EARLY_ACCESS_PRODUCT_IDS = [
'prod_RlRd2DVrG0frHe', // Gui Pro
'prod_NzLEazaqBgoKnC', // Takeout Stack
'prod_Rxu0x7jR0nWJSv', // Gui Pro Team Seats
]
if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY || !STRIPE_SECRET_KEY || !DISCORD_BOT_TOKEN) {
console.error('Missing required environment variables')
process.exit(1)
}
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY)
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2020-08-27',
})
async function getDiscordClient() {
const rest = new REST({ version: '10' }).setToken(DISCORD_BOT_TOKEN)
const gateway = new WebSocketManager({
token: DISCORD_BOT_TOKEN,
intents: GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent,
rest,
})
const discordClient = new Client({ rest, gateway })
return discordClient
}
async function getActiveStripeSubscriptions() {
console.log('💳 Fetching active Stripe subscriptions...')
const allSubscriptions: Stripe.Subscription[] = []
let hasMore = true
let startingAfter: string | undefined
while (hasMore) {
const params: Stripe.SubscriptionListParams = {
status: 'active',
limit: 100,
}
if (startingAfter) {
params.starting_after = startingAfter
}
const subscriptions = await stripe.subscriptions.list(params)
allSubscriptions.push(...subscriptions.data)
hasMore = subscriptions.has_more
if (hasMore && subscriptions.data.length > 0) {
startingAfter = subscriptions.data[subscriptions.data.length - 1].id
}
}
// Filter for early-access products
const takeoutSubscriptions = allSubscriptions.filter((sub) => {
return sub.items.data.some((item) => {
const productId =
typeof item.price.product === 'string'
? item.price.product
: item.price.product.id
return EARLY_ACCESS_PRODUCT_IDS.includes(productId)
})
})
console.log(` Found ${takeoutSubscriptions.length} active subscriptions`)
return takeoutSubscriptions
}
async function getUsersWithTakeoutRole() {
console.log('🎮 Fetching Discord users with Takeout role...')
const discordClient = await getDiscordClient()
try {
// Get all members with the takeout role
const members = await discordClient.api.guilds.getMembers(GUI_DISCORD_GUILD_ID, {
limit: 1000,
})
const membersWithRole = members.filter((member) =>
member.roles.includes(TAKEOUT_ROLE_ID)
)
console.log(` Found ${membersWithRole.length} Discord users with Takeout role`)
return membersWithRole.map((m) => ({
id: m.user!.id,
username: m.user!.username,
globalName: m.user!.global_name,
}))
} catch (error) {
console.error('Error fetching Discord members:', error)
throw error
}
}
async function getDiscordToSubscriptionMapping(discordUserIds: string[]) {
console.log('📊 Mapping Discord users to subscriptions...')
const { data, error } = await supabase
.from('discord_invites')
.select('discord_user_id, subscription_id')
.in('discord_user_id', discordUserIds)
if (error) {
console.error('Error fetching discord_invites:', error)
throw error
}
// Create map of discord_user_id -> subscription_ids[]
const mapping = new Map<string, string[]>()
data?.forEach((row) => {
if (row.discord_user_id) {
const existing = mapping.get(row.discord_user_id) || []
existing.push(row.subscription_id)
mapping.set(row.discord_user_id, existing)
}
})
console.log(` Found ${mapping.size} Discord users with invite records`)
return mapping
}
async function main() {
console.log('\n' + '='.repeat(80))
console.log('DISCORD SYNC CHECK')
console.log('='.repeat(80) + '\n')
// Fetch all data
const [subscriptions, discordUsersWithRole] = await Promise.all([
getActiveStripeSubscriptions(),
getUsersWithTakeoutRole(),
])
// Get subscription mappings
const discordToSubscriptions = await getDiscordToSubscriptionMapping(
discordUsersWithRole.map((u) => u.id)
)
// Create set of active subscription IDs
const activeSubscriptionIds = new Set(subscriptions.map((s) => s.id))
console.log('\n' + '='.repeat(80))
console.log('ANALYSIS')
console.log('='.repeat(80))
// Check which Discord users should have access
const validDiscordUsers: typeof discordUsersWithRole = []
const invalidDiscordUsers: ((typeof discordUsersWithRole)[0] & {
subscriptionIds?: string[]
})[] = []
const discordUsersNoMapping: typeof discordUsersWithRole = []
for (const discordUser of discordUsersWithRole) {
const subscriptionIds = discordToSubscriptions.get(discordUser.id)
if (!subscriptionIds || subscriptionIds.length === 0) {
// Discord user not linked to any subscriptions
discordUsersNoMapping.push(discordUser)
continue
}
// Check if any of their subscriptions are still active
const hasActiveSubscription = subscriptionIds.some((subId) =>
activeSubscriptionIds.has(subId)
)
if (hasActiveSubscription) {
validDiscordUsers.push(discordUser)
} else {
invalidDiscordUsers.push({
...discordUser,
subscriptionIds,
})
}
}
console.log(`\n📊 Summary:`)
console.log(` Active Stripe subscriptions: ${subscriptions.length}`)
console.log(` Discord users with Takeout role: ${discordUsersWithRole.length}`)
console.log(`\n✅ Valid (have active subscription): ${validDiscordUsers.length}`)
console.log(`❌ Invalid (no active subscription): ${invalidDiscordUsers.length}`)
console.log(`⚠️ No Supabase mapping: ${discordUsersNoMapping.length}`)
if (invalidDiscordUsers.length > 0) {
console.log(`\n⚠️ Users to remove (no active subscription):`)
console.log('='.repeat(80))
invalidDiscordUsers.forEach((user, i) => {
console.log(`${i + 1}. ${user.username} (${user.globalName || 'no global name'})`)
console.log(` Discord ID: ${user.id}`)
console.log(` Inactive Subscription IDs: ${user.subscriptionIds?.join(', ')}`)
console.log('')
})
}
if (discordUsersNoMapping.length > 0) {
console.log(`\n⚠️ Users without Supabase mapping (may be old/test accounts):`)
console.log('='.repeat(80))
discordUsersNoMapping.forEach((user, i) => {
console.log(`${i + 1}. ${user.username} (${user.globalName || 'no global name'})`)
console.log(` Discord ID: ${user.id}`)
console.log('')
})
}
console.log('='.repeat(80))
// Generate report files
const { writeFileSync, mkdirSync } = await import('fs')
const { join } = await import('path')
const tempDir = join(process.cwd(), 'tmp')
mkdirSync(tempDir, { recursive: true })
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
// Save invalid users list
if (invalidDiscordUsers.length > 0) {
const invalidUsersFile = join(tempDir, `discord-users-to-remove-${timestamp}.json`)
writeFileSync(
invalidUsersFile,
JSON.stringify(
{
generated_at: new Date().toISOString(),
count: invalidDiscordUsers.length,
users: invalidDiscordUsers.map((u) => ({
discord_id: u.id,
username: u.username,
global_name: u.globalName,
inactive_subscription_ids: u.subscriptionIds,
})),
},
null,
2
)
)
console.log(`\n📄 Saved users to remove: ${invalidUsersFile}`)
}
console.log('\n✅ Analysis complete!')
}
main().catch(console.error)