File size: 10,990 Bytes
7d8aa82 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | import {
Client, GatewayIntentBits, Partials,
EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle,
Events, GuildMember, ButtonInteraction,
} from 'discord.js'
import { prisma } from './lib/prisma'
// ββ Role names β create these in your Discord server βββββββββββββββββββββββββ
export const ROLES = {
SOVEREIGN: 'Sovereign',
CONTRIBUTOR: 'Verified Contributor',
DEVELOPER: 'Developer',
BUILDER: 'Builder',
UK_BUILDER: 'UK Builder',
CYPHERPUNK: 'Cypherpunk Guild',
OBSERVER: 'Observer',
}
// ββ GitHub contributor check ββββββββββββββββββββββββββββββββββββββββββββββββββ
async function isRepoContributor(githubUsername: string): Promise<boolean> {
try {
const res = await fetch(
`https://api.github.com/repos/SNAPKITTYWEST/DEVFLOW-FINANCE/contributors?per_page=100`,
{ headers: { 'User-Agent': 'snapkitty-bot', ...(process.env.GITHUB_TOKEN ? { Authorization: `token ${process.env.GITHUB_TOKEN}` } : {}) } }
)
if (!res.ok) return false
const contributors = await res.json() as Array<{ login: string }>
return contributors.some(c => c.login.toLowerCase() === githubUsername.toLowerCase())
} catch { return false }
}
// ββ Write member to DB ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function upsertCommunityMember(data: {
discordId: string
discordTag: string
role: string
githubUsername?: string
verified?: boolean
}) {
try {
await prisma.communityMember.upsert({
where: { discordId: data.discordId },
update: { role: data.role, githubUsername: data.githubUsername, verified: data.verified ?? false, updatedAt: new Date() },
create: {
discordId: data.discordId,
discordTag: data.discordTag,
role: data.role,
githubUsername: data.githubUsername,
verified: data.verified ?? false,
},
})
} catch (e) { console.error('DB write failed:', e) }
}
// ββ Welcome embed + role buttons βββββββββββββββββββββββββββββββββββββββββββββ
function buildWelcomeEmbed(member: GuildMember) {
return new EmbedBuilder()
.setColor(0x6d28d9)
.setTitle('Saint Errant Β· Sovereign Digital Society')
.setDescription(
`**${member.user.username}** β you have been granted entry.\n\n` +
`This is not a server. This is a sovereign digital society.\n` +
`SnapKitty is the operating system. The agents are the mind.\n` +
`The chain is the record. The fortress does not sleep.\n\n` +
`**Declare your guild to receive your clearance:**`
)
.addFields(
{ name: 'β‘ Developer', value: 'Building on the sovereign codebase', inline: true },
{ name: 'π¨ Builder', value: 'Working in the broader ecosystem', inline: true },
{ name: 'π Cypherpunk', value: 'Security, cryptography, zero-trust ops', inline: true },
{ name: 'π¬π§ UK Builder', value: 'UK Treasury Social Nest member', inline: true },
{ name: 'π Observer', value: 'Witness to the machine', inline: true },
)
.setFooter({ text: 'Saint Errant Β· Sovereign Digital Society β built in the open, proven in chain' })
}
function buildRoleButtons() {
const row1 = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId('role_developer').setLabel('β‘ Developer').setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId('role_builder').setLabel('π¨ Builder').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId('role_cypherpunk').setLabel('π Cypherpunk').setStyle(ButtonStyle.Danger),
)
const row2 = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId('role_uk').setLabel('π¬π§ UK Builder').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId('role_observer').setLabel('π Observer').setStyle(ButtonStyle.Secondary),
)
return [row1, row2]
}
// ββ Assign role helper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function assignRole(member: GuildMember, roleName: string): Promise<boolean> {
try {
const role = member.guild.roles.cache.find(r => r.name === roleName)
if (!role) { console.warn(`Role "${roleName}" not found in server`); return false }
await member.roles.add(role)
return true
} catch (e) { console.error('Role assign failed:', e); return false }
}
// ββ Gateway client ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function startGateway() {
const token = process.env.DISCORD_BOT_TOKEN
if (!token) { console.warn('DISCORD_BOT_TOKEN not set β gateway disabled'); return }
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages,
],
partials: [Partials.Channel, Partials.Message],
})
// ββ New member joins βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
client.on(Events.GuildMemberAdd, async (member) => {
console.log(`New member: ${member.user.tag}`)
const welcomeChannel = member.guild.channels.cache.find(
ch => ch.name === 'welcome' || ch.name === 'general'
)
if (welcomeChannel?.isTextBased()) {
await welcomeChannel.send({
content: `<@${member.id}>`,
embeds: [buildWelcomeEmbed(member)],
components: buildRoleButtons(),
})
}
// Also DM the member
try {
await member.send({
embeds: [buildWelcomeEmbed(member)],
components: buildRoleButtons(),
})
} catch { /* DMs may be closed */ }
})
// ββ Button interactions ββββββββββββββββββββββββββββββββββββββββββββββββββββ
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isButton()) return
const btn = interaction as ButtonInteraction
const member = btn.member as GuildMember
if (btn.customId === 'role_developer') {
await assignRole(member, ROLES.DEVELOPER)
await upsertCommunityMember({ discordId: member.id, discordTag: member.user.tag, role: 'developer' })
await btn.reply({
content: `**Developer role assigned.**\n\nNow run \`/verify github-username:YourGitHubHandle\` to verify your contributions to DEVFLOW-FINANCE and unlock **Verified Contributor** status.`,
ephemeral: true,
})
}
else if (btn.customId === 'role_builder') {
await assignRole(member, ROLES.BUILDER)
await upsertCommunityMember({ discordId: member.id, discordTag: member.user.tag, role: 'builder' })
await btn.reply({
content: `**Builder role assigned.** Welcome to the ecosystem.\n\nCheck out the War Room at https://collectivekitty.com and use \`/ask\` to talk to any of the 11 agents.`,
ephemeral: true,
})
}
else if (btn.customId === 'role_uk') {
await assignRole(member, ROLES.UK_BUILDER)
await upsertCommunityMember({ discordId: member.id, discordTag: member.user.tag, role: 'uk_builder' })
await btn.reply({
content: `**UK Builder role assigned.**\n\nYou're now part of the UK Treasury Social Nest. Use \`/ask agent:sterling message:walk me through UK Tier 0\` to get started with the UK business credit system.`,
ephemeral: true,
})
}
else if (btn.customId === 'role_cypherpunk') {
await assignRole(member, ROLES.CYPHERPUNK)
await upsertCommunityMember({ discordId: member.id, discordTag: member.user.tag, role: 'cypherpunk' })
await btn.reply({
content: `**Cypherpunk Guild β clearance granted.**\n\n` +
`The fortress runs SHA-256 sealed decisions, WORM immutable ledger, Ed25519 signature verification on every interaction, and a three-pillar preflight that kills unsigned payloads before they're processed.\n\n` +
`Use \`/ask agent:sentinel message:show me the threat model\` to start.\n` +
`The chain is at https://github.com/SNAPKITTYWEST/DEVFLOW-FINANCE`,
ephemeral: true,
})
}
else if (btn.customId === 'role_observer') {
await assignRole(member, ROLES.OBSERVER)
await upsertCommunityMember({ discordId: member.id, discordTag: member.user.tag, role: 'observer' })
await btn.reply({
content: `**Observer clearance granted.**\n\nWitness the machine at https://collectivekitty.com\nThe chain: https://github.com/SNAPKITTYWEST/DEVFLOW-FINANCE`,
ephemeral: true,
})
}
})
client.once(Events.ClientReady, (c) => {
console.log(`Gateway online β logged in as ${c.user.tag}`)
console.log(`Watching ${c.guilds.cache.size} server(s)`)
})
client.login(token)
return client
}
// ββ GitHub verification slash handler (called from index.ts) ββββββββββββββββββ
export async function handleVerify(
discordId: string,
discordTag: string,
githubUsername: string,
member: GuildMember
): Promise<string> {
const isContributor = await isRepoContributor(githubUsername)
if (isContributor) {
await assignRole(member, ROLES.CONTRIBUTOR)
await upsertCommunityMember({
discordId, discordTag,
role: 'verified_contributor',
githubUsername,
verified: true,
})
return `**Verified.** @${githubUsername} is a contributor to DEVFLOW-FINANCE.\n**Verified Contributor** role assigned. Welcome to the core.`
} else {
await assignRole(member, ROLES.DEVELOPER)
await upsertCommunityMember({
discordId, discordTag,
role: 'developer',
githubUsername,
verified: false,
})
return `**@${githubUsername}** β not yet a contributor to DEVFLOW-FINANCE.\n**Developer** role assigned. Make your first PR at https://github.com/SNAPKITTYWEST/DEVFLOW-FINANCE and run \`/verify\` again to unlock Verified Contributor.`
}
}
|