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.`
  }
}