Skip to content

Commit

Permalink
feat: Support all proposal events on discord
Browse files Browse the repository at this point in the history
  • Loading branch information
ChaituVR committed Feb 29, 2024
1 parent 201f3a5 commit 4ecfedb
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 37 deletions.
5 changes: 3 additions & 2 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ async function processEvents() {

for (const event of events) {
const proposalId = event.id.replace('proposal/', '');
const proposal = await getProposal(proposalId);
let proposal = await getProposal(proposalId);
const subscribers = await getSubscribers(event.space);

if (event.event === 'proposal/deleted')
proposal = { id: proposalId, space: { id: event.space } };
if (proposal) {
providers.forEach(provider => {
provider(event, proposal, subscribers);
Expand Down
1 change: 1 addition & 0 deletions src/helpers/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ CREATE TABLE subscriptions (
channel VARCHAR(64) NOT NULL,
space VARCHAR(256) NOT NULL,
mention VARCHAR(64) NOT NULL,
events JSON,
created VARCHAR(64) NOT NULL,
updated VARCHAR(64) NOT NULL,
PRIMARY KEY (guild, channel, space),
Expand Down
188 changes: 153 additions & 35 deletions src/providers/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {
codeBlock,
underscore,
inlineCode,
DiscordAPIError
DiscordAPIError,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
ComponentType
} from 'discord.js';
import db from '../helpers/mysql';
import removeMd from 'remove-markdown';
import { shortenAddress } from '../helpers/utils';
import { getSpace } from '../helpers/utils';
import { shortenAddress, getSpace } from '../helpers/utils';
import { capture } from '@snapshot-labs/snapshot-sentry';
import { timeOutgoingRequest, outgoingMessages } from '../helpers/metrics';

Expand Down Expand Up @@ -54,8 +56,6 @@ const client: any = new Client({
}
});

export let ready = false;

const commands = [
new SlashCommandBuilder()
.setName('ping')
Expand Down Expand Up @@ -103,7 +103,12 @@ const commands = [
.setName('space')
.setDescription('space to subscribe to')
.setRequired(true)
)
),
new SlashCommandBuilder()
.setName('select-events')
.setDescription('Select events to subscribe.')
.setDMPermission(false)
.setDefaultMemberPermissions(0)
];

const rest = new REST({ version: '10' }).setToken(token);
Expand Down Expand Up @@ -163,27 +168,101 @@ const checkPermissions = async (channelId, botId) => {
};

client.on('ready', async () => {
ready = true;
console.log(`[discord] bot logged as "${client.user.tag}"`);
setActivity('!');

await loadSubscriptions();
});

async function snapshotSelectEventsCommandHandler(interaction) {
const select = new StringSelectMenuBuilder()
.setCustomId('selectedEvents')
.setPlaceholder('Make a selection!')
.addOptions(
new StringSelectMenuOptionBuilder()
.setLabel('proposal/created')
.setDescription('When a proposal is created.')
.setValue('proposal/created'),
new StringSelectMenuOptionBuilder()
.setLabel('proposal/start')
.setDescription('When a proposal starts.')
.setValue('proposal/start')
.setDefault(true),
new StringSelectMenuOptionBuilder()
.setLabel('proposal/end')
.setDescription('When a proposal end.')
.setValue('proposal/end'),
new StringSelectMenuOptionBuilder()
.setLabel('proposal/deleted')
.setDescription('When a proposal is deleted.')
.setValue('proposal/deleted')
)
.setMinValues(1)
.setMaxValues(4);

const row = new ActionRowBuilder().addComponents(select);

const response = await interaction
.reply({
content: 'Select events!',
components: [row],
ephemeral: true
})
.catch(capture);

const collector = response.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
time: 3_600_000
});

collector.on('collect', async i => {
const selection = i.values;
try {
const response = await db.queryAsync(
`UPDATE subscriptions SET events = ? WHERE guild = ?`,
[JSON.stringify(selection), i.guildId]
);
if (response.affectedRows === 0) {
i.update({
content: `No subscriptions found on this server. Please add a subscription first.`,
components: [],
ephemeral: true
});
}
} catch (e) {
capture(e);
}
await i
.update({
content: `Success! You will notified for events ${selection
.map(a => `\`${a}\``)
.join(', ')}`,
components: [],
ephemeral: true
})
.catch(capture);
});
}

async function snapshotHelpCommandHandler(interaction) {
const subscriptions = await db.queryAsync(
'SELECT * FROM subscriptions WHERE guild = ?',
interaction.guildId
);
let subscriptionsDescription = `\n\n**Subscriptions (${subscriptions.length})**\n\n`;
const events = JSON.parse(subscriptions[0]?.events || `["proposal/start"]`);
if (subscriptions.length > 0) {
subscriptions.forEach(subscription => {
subscriptionsDescription += `<#${subscription.channel}> ${subscription.space}\n`;
});
} else {
subscriptionsDescription += 'No subscriptions\n';
}
subscriptionsDescription += `\n\n**Events**
${events.map(e => `\`${e}\``).join('\n ')}
`;
subscriptionsDescription += `\n**Commands**`;

const addSubscriptionExample = codeBlock(
`/add channel:#snapshot space:yam.eth mention:@everyone`
);
Expand All @@ -200,29 +279,47 @@ async function snapshotHelpCommandHandler(interaction) {
'https://github.com/snapshot-labs/brand/blob/master/icon/icon.png?raw=true'
)
.addFields(
{ name: '`/ping`', value: 'Description: Make sure the bot is online.' },
{
name: '`/help`',
value: 'Description: List all commands and current notifications.'
name: '`/ping` - Make sure the bot is online.',
value: '----------------------------------------------------'
},
{
name: '`/add`',
value: `Description: Add notifications on a channel when a proposal start.
name: '`/help` - List all commands and current notifications.',
value: '----------------------------------------------------'
},
{
name: '`/add` - Add notifications on a channel when a proposal start.',
value: `
Options:
*channel*: Channel to post the events
*space*: Space id to subscribe to
*mention*: Mention role (optional)
**channel**: Channel to post the events
**space**: Space id to subscribe to
**mention**: Mention role (optional)
Example:
${addSubscriptionExample}`
${addSubscriptionExample}
----------------------------------------------------`
},
{
name: '`/remove`',
value: `Description: Remove notifications on a channel.
name: '`/remove` - Remove notifications on a channel.',
value: `
Options:
*channel*: Channel to post the events
*space*: Space id to subscribe to
**channel**: Channel to post the events
**space**: Space id to subscribe to
Example:
${removeSubscriptionExample}
----------------------------------------------------`
},
{
name: '`/select-events ` - Select events for your notifications.',
value: `
Options:
\`proposal/created\`
\`proposal/start\` (default)
\`proposal/end\`
\`proposal/deleted\`
----------------------------------------------------
Have any questions? Join our discord: https://discord.snapshot.org`
Expand Down Expand Up @@ -255,16 +352,21 @@ async function snapshotCommandHandler(interaction, commandType) {
if (!space)
return interaction.reply(`Space not found: ${inlineCode(spaceId)}`);

const query = `SELECT events FROM subscriptions WHERE guild = ?`;
const subscriptions = await db.queryAsync(query, interaction.guildId);
const events = subscriptions[0]?.events || '';

const subscription = [
interaction.guildId,
channelId,
spaceId,
mention || '',
events,
ts,
ts
];
await db.queryAsync(
`INSERT INTO subscriptions (guild, channel, space, mention, created, updated) VALUES (?, ?, ?, ?, ?, ?)
`INSERT INTO subscriptions (guild, channel, space, mention, events, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE guild = ?, channel = ?, space = ?, mention = ?, updated = ?`,
[...subscription, ...subscription]
);
Expand Down Expand Up @@ -309,6 +411,8 @@ client.on('interactionCreate', async interaction => {
snapshotCommandHandler(interaction, 'add');
} else if (interaction.commandName === 'remove') {
snapshotCommandHandler(interaction, 'remove');
} else if (interaction.commandName === 'select-events') {
snapshotSelectEventsCommandHandler(interaction);
}
});

Expand All @@ -334,17 +438,41 @@ export const sendMessage = async (channel, message) => {
}
};

const sendToSubscribers = (event, proposal, embed, components) => {
if (subs[proposal.space.id] || subs['*']) {
[...(subs['*'] || []), ...(subs[proposal.space.id] || [])].forEach(sub => {
if (sub.events && !JSON.parse(sub.events).includes(event)) return;
let eventName = event.replace('proposal/', '');
if (!eventName.endsWith('ed')) eventName = `${eventName}ed`;
sendMessage(sub.channel, {
content: `Proposal ${eventName} on ${proposal.space.id} space ${sub.mention}`,
embeds: [embed],
components
});
});
}
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function send(eventObj, proposal, _subscribers) {
const event = eventObj.event;
try {
// Only supports proposal/start event
if (event !== 'proposal/start') return;
if (event === 'proposal/deleted') {
const color = '#FF0000';
const embed = new EmbedBuilder().setColor(color).addFields({
name: 'Proposal ID',
value: `\`${proposal.id}\``,
inline: true
});
sendToSubscribers(event, proposal, embed, []);
return;
}

const status = 'Active';
let status = proposal.start > Date.now() / 1e3 ? 'Pending' : 'Active';
if (proposal.end < Date.now() / 1e3) status = 'Ended';
const color = '#21B66F';

const url = `https://snapshot.org/#/${proposal.space.id}/proposal/${proposal.id}`;

let components =
!proposal.choices.length || proposal.choices.length > 5
? []
Expand Down Expand Up @@ -385,17 +513,7 @@ export async function send(eventObj, proposal, _subscribers) {
)
.setDescription(preview || ' ');

if (subs[proposal.space.id] || subs['*']) {
[...(subs['*'] || []), ...(subs[proposal.space.id] || [])].forEach(
sub => {
sendMessage(sub.channel, {
content: `${sub.mention} `,
embeds: [embed],
components
});
}
);
}
sendToSubscribers(event, proposal, embed, components);

return { success: true };
} catch (e: any) {
Expand Down

0 comments on commit 4ecfedb

Please sign in to comment.