Skip to content

Commit

Permalink
Improve notifications and message embeds
Browse files Browse the repository at this point in the history
  • Loading branch information
peterpolman committed Jun 12, 2024
1 parent a8b5b16 commit 03f756b
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 50 deletions.
36 changes: 17 additions & 19 deletions apps/api/src/app/proxies/DiscordDataProxy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import axios, { AxiosRequestConfig } from 'axios';
import { client, PermissionFlagsBits } from '../../discord';
import { DiscordGuild, DiscordGuildDocument, PoolDocument } from '@thxnetwork/api/models';
import { DiscordGuildDocument } from '@thxnetwork/api/models';
import { ActionRowBuilder, ButtonBuilder, Guild } from 'discord.js';
import { WIDGET_URL } from '../config/secrets';
import { logger } from '../util/logger';
import { AccessTokenKind, OAuthRequiredScopes } from '@thxnetwork/common/enums';
import { DISCORD_API_ENDPOINT } from '@thxnetwork/common/constants';
Expand Down Expand Up @@ -30,27 +29,26 @@ export async function discordClient(config: AxiosRequestConfig) {

export default class DiscordDataProxy {
static async sendChannelMessage(
pool: PoolDocument,
content: string,
channelId: string,
message: string,
embeds: TDiscordEmbed[] = [],
buttons?: TDiscordButton[],
) {
const discordGuild = await DiscordGuild.findOne({ poolId: String(pool._id) });
if (discordGuild && discordGuild.channelId) {
try {
const channel: any = await client.channels.fetch(discordGuild.channelId);
const components = [];
if (buttons) components.push(this.createButtonActionRow(buttons));

const botMember = channel.guild.members.cache.get(client.user.id);
if (!botMember.permissionsIn(channel).has(PermissionFlagsBits.SendMessages)) {
throw new Error('Insufficient channel permissions for bot to send messages.');
}

await channel.send({ content, embeds, components });
} catch (error) {
logger.error(error);
if (!channelId) return;

try {
const channel: any = await client.channels.fetch(channelId);
const components = [];
if (buttons) components.push(this.createButtonActionRow(buttons));

const botMember = channel.guild.members.cache.get(client.user.id);
if (!botMember.permissionsIn(channel).has(PermissionFlagsBits.SendMessages)) {
throw new Error('Insufficient channel permissions for bot to send messages.');
}

await channel.send({ content: message, embeds, components });
} catch (error) {
logger.error(error);
}
}

Expand Down
17 changes: 17 additions & 0 deletions apps/api/src/app/services/DiscordService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ export default class DiscordService {
}
}

static async getGuilds(poolId: string): Promise<TDiscordGuild[]> {
const discordGuilds = await DiscordGuild.find({ poolId });
if (!discordGuilds.length) return;

const guilds = [];
for (const g of discordGuilds) {
try {
// Might fail if bot is removed from the guild
await client.guilds.fetch(g.guildId);
guilds.push(g);
} catch (error) {
logger.error(error);
}
}
return guilds;
}

static async getMember(guildId: string, userId: string) {
try {
// Might fail if bot is removed from the guild
Expand Down
148 changes: 117 additions & 31 deletions apps/api/src/app/services/NotificationService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QuestVariant } from '@thxnetwork/common/enums';
import { PoolDocument } from '@thxnetwork/api/models';
import { PoolDocument, QuestSocialDocument } from '@thxnetwork/api/models';
import { logger } from '../util/logger';
import { sleep } from '../util';
import { Notification, Widget, Participant } from '@thxnetwork/api/models';
Expand All @@ -17,6 +17,8 @@ import DiscordDataProxy from '../proxies/DiscordDataProxy';
import AnalyticsService from '../services/AnalyticsService';
import { subDays } from 'date-fns';
import * as html from 'html-entities';
import DiscordService from './DiscordService';
import { QuestRequirement } from 'libs/sdk/src';

const MAIL_CHUNK_SIZE = 600;
const emojiMap = ['🥇', '🥈', '🥉'];
Expand Down Expand Up @@ -93,10 +95,10 @@ async function sendQuestPublishNotification(
const { amount, amounts } = quest as any;

const embed = {
title: quest.title,
description: quest.description,
title: html.decode(quest.title),
description: html.decode(quest.description),
author: {
name: pool.settings.title,
name: html.decode(pool.settings.title),
icon_url: brand ? brand.logoImgUrl : '',
url: widget.domain,
},
Expand All @@ -116,42 +118,126 @@ async function sendQuestPublishNotification(
],
};

await DiscordDataProxy.sendChannelMessage(
pool,
`Hi @everyone! We published a **${QuestVariant[variant]} Quest**.`,
[embed],
[
{
customId: `${DiscordButtonVariant.QuestComplete}:${quest.variant}:${quest._id}`,
label: 'Complete Quest!',
style: ButtonStyle.Success,
},
{ label: 'More Info', style: ButtonStyle.Link, url: WIDGET_URL + `/c/${pool.settings.slug}` },
],
);
// Adding additional embeds for specific quest requirements
const requirements = getQuestRequirements(quest, variant);
if (requirements.length) {
embed.fields.push(...requirements);
}

// Get notification configuration and send message in connected guilds
const guilds = await DiscordService.getGuilds(pool.id);
const defaultMessage = `Hi @everyone! We published a **${QuestVariant[variant]} Quest**.`;

for (const guild of guilds) {
const { message, channelId, isEnabled } = guild.notifications.questCreate;

// If notification is not enabled continue
if (!isEnabled) continue;

await DiscordDataProxy.sendChannelMessage(
channelId,
message || defaultMessage,
[embed],
[
{
customId: `${DiscordButtonVariant.QuestComplete}:${quest.variant}:${quest._id}`,
label: 'Complete Quest!',
style: ButtonStyle.Success,
},
{ label: 'More Info', style: ButtonStyle.Link, url: WIDGET_URL + `/c/${pool.settings.slug}` },
],
);
}
}

function getQuestRequirements(quest: TQuest, variant: QuestVariant) {
switch (variant) {
case QuestVariant.Daily: {
return [
{
name: 'Requirement',
value: 'Visit and claim points daily!',
inline: false,
},
];
}
case QuestVariant.Twitter: {
const { interaction: interactionString, contentMetadata } = quest as QuestSocialDocument;
const interaction = Number(interactionString);
const metadata = JSON.parse(contentMetadata);
const twitterPostQuests = [
QuestRequirement.TwitterLike,
QuestRequirement.TwitterRetweet,
QuestRequirement.TwitterLikeRetweet,
];

if (twitterPostQuests.includes(interaction)) {
const labelMap = {
[QuestRequirement.TwitterLike]: 'Like',
[QuestRequirement.TwitterRetweet]: 'Retweet',
[QuestRequirement.TwitterLikeRetweet]: 'Like & Retweet',
};
return [
{
name: 'Requirement',
value: `[${labelMap[interaction]}](${metadata.url})`,
inline: false,
},
];
} else if (interaction === QuestRequirement.TwitterFollow) {
return [
{
name: 'Requirement',
value: `[Follow ${metadata.name}](https://x.com/${metadata.username})`,
inline: false,
},
];
} else if (interaction === QuestRequirement.TwitterQuery) {
return [
{
name: 'Requirement',
value: `Make a post! Please click more info for it's requirements.`,
inline: false,
},
];
}
break;
}
default: {
return [
{
name: 'Requirement',
value: 'See quest description.',
inline: false,
},
];
}
}
return [];
}

async function sendQuestEntryNotification(pool: PoolDocument, quest: TQuest, account: TAccount, amount: number) {
const index = Math.floor(Math.random() * celebratoryWords.length);
const discord = account.tokens && account.tokens.find((a) => a.kind === 'discord');
const user = discord && discord.userId ? `<@${discord.userId}>` : `**${account.username}**`;
const content = `${celebratoryWords[index]} ${user} completed the **${html.decode(
const guilds = await DiscordService.getGuilds(pool.id);
const defaultMessage = `${celebratoryWords[index]} ${user} completed the **${html.decode(
quest.title,
)}** quest and earned **${amount} points.**`;

await DiscordDataProxy.sendChannelMessage(
pool,
content,
[],
[
{
customId: `${DiscordButtonVariant.QuestComplete}:${quest.variant}:${quest._id}`,
label: 'Complete Quest',
style: ButtonStyle.Primary,
},
{ label: 'More Info', style: ButtonStyle.Link, url: WIDGET_URL + `/c/${pool.settings.slug}/quests` },
],
);
for (const guild of guilds) {
const { message, channelId, isEnabled } = guild.notifications.questEntryCreate;

// If notification is not enabled continue
if (!isEnabled) continue;

await DiscordDataProxy.sendChannelMessage(
channelId,
message || defaultMessage,
[],
[{ label: 'More Info', style: ButtonStyle.Link, url: WIDGET_URL + `/c/${pool.settings.slug}/quests` }],
);
}
}

export async function sendWeeklyDigestJob() {
Expand Down

0 comments on commit 03f756b

Please sign in to comment.