Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add comment from web to discord | bot code restructuring #21

Merged
merged 15 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ prisma/dev.db

packages/composedb/.ceramic
.idea/
.nyc_output/
6 changes: 6 additions & 0 deletions apps/discord-bot/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"node-option": [
"experimental-specifier-resolution=node",
"loader=ts-node/esm"
]
}
17 changes: 17 additions & 0 deletions apps/discord-bot/.nycrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"all": true,
"check-coverage": true,
"extension": [".ts"],
"include": [
"src/**/*.ts",
"src/core/**/*.ts",
"src/bots/discord/**/*.ts",
"src/bots/discord/**/**/*.ts",
"src/core/utils/response.ts"
],
"loader": "ts-node/esm",
"branches": 40,
"lines": 40,
"functions": 40,
"statements": 40
}
25 changes: 23 additions & 2 deletions apps/discord-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
"start": "node dist/index.js",
"start:bot": "node dist/index.js",
"build": "tsup",
"nodemon": "nodemon",
"dev": "tsup && node dist/index.js",
"dev:bot": "tsup && node dist/index.js"
"dev:bot": "tsup && node dist/index.js",
"test:bot": "export NODE_ENV=Test && npx mocha tests/**/*.spec.ts tests/**/**/*.spec.ts -- --silent",
"coverage": "nyc --reporter=html --reporter=text mocha -r ts-node/esm --node-option experimental-specifier-resolution=node tests/**/*.spec.ts tests/unit/**/*.spec.ts"
},
"dependencies": {
"@composedb/client": "^0.4.3",
"@composedb/types": "0.4.3",
"@devnode/composedb": "*",
"@devnode/database": "*",
"@stablelib/random": "^1.0.2",
Expand All @@ -33,11 +37,28 @@
"discord.js": "^14.7.1",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"@composedb/types": "0.4.3"
"joi": "^17.8.4",
"lodash": "^4.17.21"
},
"devDependencies": {
"@devnode/tsconfig": "*",
"@types/chai": "^4.3.4",
"@types/chai-as-promised": "^7.1.5",
"@types/chai-spies": "^1.0.3",
"@types/lodash": "^4.14.191",
"@types/mocha": "^10.0.1",
"@types/sinon": "^10.0.13",
"@types/sinon-chai": "^3.2.9",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-http": "^4.3.0",
"chai-spies": "^1.0.0",
"mocha": "^10.2.0",
"nodemon": "^2.0.21",
"nyc": "^15.1.0",
"rimraf": "^3.0.2",
"sinon": "^15.0.2",
"sinon-chai": "^3.7.0",
"tsup": "^6.5.0",
"typescript": "^4.9.4"
}
Expand Down
81 changes: 81 additions & 0 deletions apps/discord-bot/src/bots/discord/comments/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {ChannelType, Client, Message, MessageType, ThreadChannel} from "discord.js";
import {PostCommentToSocialPayload} from "../../../core/types";
import {buildMessage} from "../utils";
import {config, constants, getBotDid} from "../../../config";
import {composeMutationHandler, composeQueryHandler} from "@devnode/composedb";
import {ComposeClient} from "@composedb/client";
import {logger} from "../../../core/utils/logger";

export const handleNewComment = async (compose: ComposeClient, message: Message<boolean>) => {
if (message.author.bot) return; // ignoring bots

switch (message.channel.type) {
case ChannelType.GuildText:
case ChannelType.PublicThread:
break;
default:
return; // not in text or thread channel
}

if (
message.channel.name !== config.discord.channel &&
message.channel.parent?.name !== config.discord.channel
rushidhanwant marked this conversation as resolved.
Show resolved Hide resolved
) return; // not in devnode channel

switch (message.type) {
case MessageType.ThreadCreated:
break;
case MessageType.Default:
if (message.channel.type !== ChannelType.PublicThread) { // early exit if not in thread
await deleteMessage(message);
await message.author.send(constants.replies.noMsgOutOfThread);
return;
}
break;
default:
return;
}

const user = await composeQueryHandler().fetchUserByPlatformDetails(constants.PLATFORM_DISCORD_NAME, message.author.id);
if (!user) {
await deleteMessage(message);
await message.author.send(constants.replies.userUnknown);
return;
}

const thread = await composeQueryHandler().fetchThreadBySocialThreadId(message.channel.id);
if (!thread) return;

const comment = {
threadId: thread.node.id,
userId: user.node.id,
comment: message.content,
createdFrom: constants.PLATFORM_DISCORD_NAME,
createdAt: message.createdAt.toISOString(),
};

compose.setDID(await getBotDid());
const mutation = await composeMutationHandler(compose);
const result = await mutation.createComment(comment);
if(result.errors && result.errors.length > 0) {
logger.error('discord', {e: result.errors});
await message.delete().catch((e) => logger.error('discord', {e}));
}
};

async function deleteMessage(message: Message<boolean>) {
await message.delete().catch(() => {
message.reply(constants.replies.noPermsToDel);
});
}

export const postComment = async (client: Client, payload: PostCommentToSocialPayload) => {
const server = client.guilds.cache.get(payload.serverId);
const thread = server?.channels.cache.get(payload.threadId) as ThreadChannel;

if (!server) {
throw new Error("unknown server");
}

return await thread.send(buildMessage({...payload, body: payload.text}));
};
13 changes: 13 additions & 0 deletions apps/discord-bot/src/bots/discord/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Events} from "discord.js";
import * as commentHandler from "./comments/handler";
import * as threadHandler from "./threads/handler";
import {Clients} from "../../core/types";

export {commentHandler, threadHandler};

export const attachListeners = (clients: Clients) => {
const discord = clients.discord;

discord.on(Events.MessageCreate, (message) => commentHandler.handleNewComment(clients.compose, message));
discord.on(Events.ThreadCreate, (thread) => threadHandler.handleNewThread(clients.compose, thread));
};
63 changes: 63 additions & 0 deletions apps/discord-bot/src/bots/discord/threads/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {AnyThreadChannel, ChannelType, TextChannel} from "discord.js";
import {Clients, Node, PostThreadToSocialPayload, User} from "../../../core/types";
import {config, constants, getBotDid} from "../../../config";
import {buildMessage, buildThread} from "../utils";
import {composeMutationHandler, composeQueryHandler} from "@devnode/composedb";
import {logger} from "../../../core/utils/logger";
import {ComposeClient} from "@composedb/client";

export const handleNewThread = async (compose: ComposeClient, thread: AnyThreadChannel<boolean>) => {
const threadOwner = await thread.fetchOwner();
if (!threadOwner || !threadOwner.user || threadOwner.user.bot) return; //We ignore bots
if (thread.type !== ChannelType.PublicThread) return;//We only care about public threads
if (thread.parent?.name !== config.discord.channel) return;//We only care about threads in our channel

const queryHandler = composeQueryHandler();
const user: Node<User> = await queryHandler.fetchUserByPlatformDetails(constants.PLATFORM_DISCORD_NAME, threadOwner.user.id);
if (!user) {
await thread.delete().catch((e) => logger.error('discord', {e}));
await threadOwner.user.send(constants.replies.userUnknown);
return;
}

compose.setDID(await getBotDid());
const mutation = await composeMutationHandler(compose);
const community = await queryHandler.fetchCommunityUsingPlatformId(thread.guildId);
if (!community) {
await thread.delete().catch((e) => logger.error('discord', {e}));
await threadOwner.user.send("No such community exists!");
return;
}

const threadPayload = {
communityId: community.node.id,
userId: user.node.id,
threadId: thread.id,
title: thread.name,
body: thread.lastMessage?.content || " ",
createdFrom: constants.PLATFORM_DISCORD_NAME,
createdAt: thread.createdAt?.toISOString() || new Date().toISOString(),
}

const result = await mutation.createThread(threadPayload);
if(result.errors && result.errors.length > 0) {
logger.error('discord', {e: result.errors});
await thread.delete().catch((e) => logger.error('discord', {e}));
}
};

export const postThread = async ({discord, compose}: Clients, payload: PostThreadToSocialPayload) => {
const server = discord.guilds.cache.get(payload.serverId);
if (!server) {
throw new Error("unknown server");
}

const channel = server.channels.cache.find((channel) => channel.name == config.discord.channel) as TextChannel;
if (!channel) {
throw new Error("channel not found");
}

const thread = await channel.threads.create(buildThread(payload.title));
await thread.send(buildMessage({...payload}));
return thread.id;
};
7 changes: 7 additions & 0 deletions apps/discord-bot/src/bots/discord/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface DiscordMessage {
userName: string;
userAvatar: string;
userProfileLink: string;
redirectLink: string;
body: string;
}
39 changes: 39 additions & 0 deletions apps/discord-bot/src/bots/discord/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
ActionRowBuilder,
ButtonBuilder,
EmbedBuilder,
GuildTextThreadCreateOptions, MessageCreateOptions,
ThreadAutoArchiveDuration
} from "discord.js";
import {DiscordMessage} from "./types";

export const buildThread = (title: string): GuildTextThreadCreateOptions<any> => {
return {
name: title,
reason: "thread was created on devnode platform",
autoArchiveDuration: ThreadAutoArchiveDuration.OneWeek,
}
}

export const buildMessage = (message: DiscordMessage): MessageCreateOptions => {
const {body, userName, userAvatar, userProfileLink, redirectLink} = message;
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(buildLinkButton(redirectLink));
return {
embeds: [buildUserEmbed(userName, body, userAvatar, userProfileLink)],
components: [row],
}
}

export const buildUserEmbed = (name: string, body: string, avatar: string, url: string) => {
return new EmbedBuilder()
.setColor(0x323338)
.setAuthor({ name, url, iconURL: avatar})
.setDescription(body);
}

export const buildLinkButton = (url: string) => {
return new ButtonBuilder()
.setLabel('View on Devnode')
.setURL(url)
.setStyle(5)
}
23 changes: 23 additions & 0 deletions apps/discord-bot/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {DIDSession} from "did-session";

export const config = {
server: {
port: process.env.AGGREGATOR_PORT || 4000,
},
compose: {
nodeUrl: process.env.CERAMIC_NODE || "",
graphqlUrl: process.env.CERAMIC_GRAPH || "",
session: process.env.DID_SESSION || "",
},
discord: {
token: process.env.DISCORD_TOKEN || "",
bot: process.env.DISCORD_BOT_NAME || "devnode-bot",
channel: process.env.DISCORD_SERVER_NAME || "devnode",
},
devnodeWebsite: process.env.DEVNODE_WEBSITE || "http://localhost:3000",
};

export const getBotDid = async (): Promise<any> => {
const session = await DIDSession.fromSession(config.compose.session);
return session.did;
}
13 changes: 13 additions & 0 deletions apps/discord-bot/src/config/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {config} from "./config";

export const constants = {
PLATFORM_DEVNODE_ID: "devnode",
PLATFORM_DEVNODE_NAME: "devnode",
PLATFORM_DISCORD_NAME: "discord",

replies: {
noPermsToDel: "I should delete this but I can't! ADMIN PLS HELP!",
noMsgOutOfThread: "You can only start threads or reply to threads in the devnode channel",
userUnknown: `You must have an account on devnode to create thread or reply. Please create an account on ${config.devnodeWebsite}`,
},
}
2 changes: 2 additions & 0 deletions apps/discord-bot/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./config";
export * from "./constants";
40 changes: 40 additions & 0 deletions apps/discord-bot/src/core/comments/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Request, Response} from "express";
import {Clients, Comment, Node} from "../types";
import _ from "lodash";
import {config, constants} from "../../config";
import {Resp} from "../utils/response";
import {Client} from "discord.js";
import {commentHandler as discordCommentHandler} from "../../bots/discord";
import {composeQueryHandler} from "@devnode/composedb";
import {logger} from "../utils/logger";
import {communityHasSocial, getSocialCommunityId} from "../utils/data";

export const postComment = async (clients: Clients, req: Request, res: Response) => {
const {commentId} = req.body;
const comment: Node<Comment> = await composeQueryHandler().fetchCommentDetails(commentId);
const socials = _.get(comment, "node.thread.community.socialPlatforms.edges");

if (communityHasSocial(socials, constants.PLATFORM_DISCORD_NAME)) {
postCommentToDiscord(clients.discord, comment);
} else {
return Resp.notOk(res, "No discord for this community, bailing out!");
}
return Resp.ok(res, "Posted to socials!");
};

export const postCommentToDiscord = (discordClient: Client, comment: Node<Comment>) => {
const text = _.get(comment, "node.text");
const socials = _.get(comment, "node.thread.community.socialPlatforms.edges");
const threadStreamId = _.get(comment, "node.threadId");
const userName = _.get(comment, "node.user.userPlatforms[0].platformUsername");
const userId = _.get(comment, "node.user.id");
const userAvatar = _.get(comment, "node.user.userPlatforms[0].platformAvatar");
const userProfileLink = config.devnodeWebsite.concat(`/${userId}/profile`);
const redirectLink = config.devnodeWebsite.concat(`/${threadStreamId}`);
const serverId = getSocialCommunityId(socials, constants.PLATFORM_DISCORD_NAME);
const threadId = _.get(comment, "node.thread.threadId");

const payload = {text, userName, serverId, threadId, threadStreamId, userAvatar, userProfileLink, redirectLink};
discordCommentHandler.postComment(discordClient, payload)
.catch((e) => logger.error('core', {payload, e}));
}
1 change: 1 addition & 0 deletions apps/discord-bot/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./server";
21 changes: 21 additions & 0 deletions apps/discord-bot/src/core/middleware/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import joi from "joi";
import {NextFunction, Request, Response} from "express";

export const commentSchema = joi.object({
commentId: joi.string().required(),
});

export const threadSchema = joi.object({
threadId: joi.string().required(),
});

export const validator = (schema: joi.Schema) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
req.body = await schema.validateAsync(req.body);
return next();
} catch (e) {
return res.status(401).json(e);
}
}
}
Loading