Ever been in a meeting discussing important project updates in Google Chat while your devrel team drops fire memes in Discord? Yeah, me too. Today, we're fixing that communication chaos once and for all.
The Problem Many Modern Teams Face
Let's be honest. Your team probably lives in multiple chat apps. Marketing loves Slack, developers worship Discord, and management insists on Google Chat because "it integrates with our workspace." Meanwhile, important messages are getting lost in the shuffle faster than my motivation on Monday mornings.
Sound familiar? Well, grab your favorite caffeinated beverage because we're about to build something that'll make you the office hero.
What We're Building
We're creating a Discord bot that acts like a digital messenger pigeon between Discord and Google Chat. Here's what this little guy will do:
- Listen to a specific Discord channel like it's eavesdropping at a coffee shop
- Capture messages and format them nicely (because we're not animals)
- Forward everything to Google Chat with rich formatting and direct links
- Make your team think you're some kind of integration wizard
By the end of this tutorial, you'll have a bot that bridges these platforms seamlessly. No more "did you see what Janaka posted in Discord?" moments.
Before We Start Coding
Make sure you've got these obvious but important stuff.
- Node.js (version 20 or newer - if you're still on Node 14, it's time to upgrade, friend)
- NPM (comes with Node.js, like fries with a burger)
- Discord Developer Account with bot creation privileges
- Google Workspace Account (the kind that gives you access to Google Chat)
- A Choreo Account: If you don't have one, sign up free at https://console.choreo.dev/signup
Step 1: Creating Your Discord Bot
First, let's bring a new Discord bot into existence:
- Head over to the Discord Developer Portal
- Click "New Application" and give it a name (I went with "PingKong" - King Kong, but for pinging across apps.)
- Navigate to the "Bot" tab and click "Add Bot".
- Important: Enable "Message Content Intent" - this is like giving your bot permission to actually read what people are saying.
- Copy that bot token (keep it secret, keep it safe).
- In OAuth2 URL Generator, select "bot" scope and these permissions:
- Read Messages/View Channels
- Send Messages
- Read Message History
- Use the generated URL to invite your bot to your server.
Pro tip: Your bot will show up offline initially - don't panic, it's not broken, just offline until we turn it up
Step 2: Setting Up Google Chat Webhook
Google Chat webhooks are surprisingly straightforward and probably the easiest thing to do in this tutorial:
- Open Google Chat and find your Chat Space.
- Click the space name β "Apps & integrations"
- Click "Add webhooks"
- Give it a name.
- Add an avatar if you're feeling fancy.
- Hit "Save" and copy that webhook URL
Step 3: Project Setup
Time to create our project structure:
- Create a new directory for the project home.
mkdir discord-to-gchat-bot
cd discord-to-gchat-bot
- Initialize a new Node project.
npm init -y
- Install our dependencies.
npm install discord.js axios dotenv typescript @types/node
npm install -D typescript ts-node
- Set up TypeScript.
npx tsc --init
What we just installed:
-
discord.js
- The library that connects your bot to Discord. -
axios
- For making HTTP requests. -
dotenv
- Keeps our secrets safe in environment variables. -
typescript
- Because we're professionals who like type safety.
Step 4: TypeScript Configuration
Update your tsconfig.json
with these settings:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
This configuration basically tells TypeScript: "Compile our code to ES2020, be strict about types, and don't include node_modules in compilation" (because that would be chaos).
Step 5: Environment Variables
Create a .env
file in your project root:
DISCORD_TOKEN=your_discord_bot_token_here
GOOGLE_CHAT_WEBHOOK_URL=your_webhook_url_here
TARGET_CHANNEL_ID=discord_channel_id_to_watch
Getting your Discord channel ID: Enable Developer Mode in Discord settings (Settings β Advanced), then right-click any channel and select "Copy ID".
Security note: Never commit your .env file to version control. It's like posting your passwords on a billboard.
Step 6: Type Definitions
Create src/types.ts
:
export enum BOT_EVENTS {
READY = "ready",
MESSAGE_CREATE = "messageCreate",
ERROR = "error",
}
export interface EnvironmentVariables {
DISCORD_TOKEN: string;
GOOGLE_CHAT_WEBHOOK_URL: string;
TARGET_CHANNEL_ID: string;
}
export interface GoogleChatPayload {
cardsV2: {
cardId: string;
card: {
header: {
title: string;
subtitle?: string;
imageUrl?: string;
imageType?: "CIRCLE" | "SQUARE";
imageAltText?: string;
};
sections: {
header?: string;
widgets: {
decoratedText?: {
topLabel?: string;
text: string;
startIcon?: {
knownIcon?: string;
iconUrl?: string;
};
};
textParagraph?: {
text: string;
};
buttonList?: {
buttons: {
text: string;
onClick: {
openLink?: {
url: string;
};
};
}[];
};
}[];
}[];
};
}[];
}
What's happening here:
- We're defining the structure for Google Chat's card format (it's like HTML but in JSON form). For more info, refer to the Google Chat Cards Documentation.
- TypeScript will yell at us if we try to use the wrong structure (which is good!)
- Think of interfaces as contracts - they define what shape our data should have
Step 7: The Main Event - Building Our Bot
Create src/server.ts
- this is where the magic happens:
import { Client, GatewayIntentBits, Message, TextChannel } from "discord.js";
import axios from "axios";
import * as dotenv from "dotenv";
import { BOT_EVENTS, EnvironmentVariables, GoogleChatPayload } from "./types";
// Discord's logo - because branding matters
const discordLogoUrl = "<REPLACE_WITH_A_URL_TO_DISCORD_LOGO>";
dotenv.config(); // Load our secret variables
// This function makes sure we have all our required environment variables
// Think of it as a pre-flight checklist
const validateEnv = (): EnvironmentVariables => {
const { DISCORD_TOKEN, GOOGLE_CHAT_WEBHOOK_URL, TARGET_CHANNEL_ID } = process.env;
if (!DISCORD_TOKEN || !GOOGLE_CHAT_WEBHOOK_URL || !TARGET_CHANNEL_ID) {
throw new Error("Missing required environment variables - check your .env file!");
}
return { DISCORD_TOKEN, GOOGLE_CHAT_WEBHOOK_URL, TARGET_CHANNEL_ID };
};
const env = validateEnv();
// Create our Discord client with the right permissions
const client = new Client({
intents: [
GatewayIntentBits.Guilds, // Access to server info
GatewayIntentBits.GuildMessages, // Read messages
GatewayIntentBits.MessageContent // Actually see message content
],
});
Now for the heart of our bot - the message forwarding function:
/**
* This function takes a Discord message and formats it nicely for Google Chat
*/
async function sendToGoogleChat(discordMessage: Message): Promise<void> {
try {
let channelName: string;
let threadTitle = "";
// Handle different channel types (because Discord has many flavors)
if (discordMessage.channel.isThread()) {
const thread = discordMessage.channel;
const parentChannel = thread.parent;
if (parentChannel) {
channelName = parentChannel.name;
threadTitle = thread.name;
} else {
channelName = thread.name;
}
} else {
channelName = (discordMessage.channel as TextChannel).name;
}
// Extract the details from the message
const username = discordMessage.author.username;
const avatarUrl = discordMessage.author.displayAvatarURL();
const messageContent = discordMessage.content;
// Create a direct link back to the original message
// It's like leaving breadcrumbs, but digital
const messageLink = `https://discord.com/channels/${discordMessage.guild?.id}/${discordMessage.channel.id}/${discordMessage.id}`;
// Build our fancy Google Chat card
const payload: GoogleChatPayload = {
cardsV2: [
{
cardId: `discord-message-${discordMessage.id}`,
card: {
header: {
title: "New Message from Discord",
subtitle: threadTitle || channelName,
imageUrl: discordLogoUrl,
imageType: "CIRCLE",
imageAltText: "Discord logo",
},
sections: [
{
widgets: [
{
decoratedText: {
topLabel: "Posted by",
text: username,
startIcon: {
iconUrl: avatarUrl,
},
},
},
{
textParagraph: {
text: messageContent || "(No text content - probably an emoji battle)",
},
},
{
buttonList: {
buttons: [
{
text: "View in Discord",
onClick: {
openLink: {
url: messageLink,
},
},
},
],
},
},
],
},
],
},
},
],
};
// Send it off to Google Chat
await axios.post(env.GOOGLE_CHAT_WEBHOOK_URL, payload, {
headers: {
"Content-Type": "application/json",
},
});
console.log(`Message forwarded from ${username} in ${channelName} β
`);
} catch (error) {
console.error("Error sending to Google Chat:", error);
// In production, you might want to add retry logic here
}
}
Finally, let's set up our event listeners:
// Listen for new messages (this is where the magic happens)
client.on(BOT_EVENTS.MESSAGE_CREATE, async (message: Message) => {
// Skip DM messages - we're only interested in server messages
if (!message.guild) return;
// Ignore bot messages (including our own) - prevents infinite loops
if (message.author.bot) return;
// Only process messages from our target channel or its threads
const channelId = message.channel.id;
const parentId = message.channel.isThread() ? message.channel.parentId : null;
if (channelId !== env.TARGET_CHANNEL_ID && parentId !== env.TARGET_CHANNEL_ID) return;
// Forward the message to Google Chat
await sendToGoogleChat(message);
});
// When the bot successfully connects
client.on(BOT_EVENTS.READY, () => {
console.log(`π€ Bot logged in as ${client.user?.tag}!`);
console.log(`π Monitoring channel ID: ${env.TARGET_CHANNEL_ID}`);
console.log(`π Ready to accept messages!`);
});
// Handle errors gracefully
client.on(BOT_EVENTS.ERROR, (error: Error) => {
console.error("Discord client error:", error.message);
// In a real app, you might want to restart the bot or alert administrators
});
// Start the bot
client.login(env.DISCORD_TOKEN).catch((error) => {
console.error("Failed to login:", error.message);
process.exit(1);
});
Step 8: Package Scripts
Update your package.json
scripts section with the following:
{
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "tsc && node dist/server.js"
}
}
Note: These are pretty standard TypeScript stuff.
Step 9: Launch Time! π
Build and run your bot:
# Compile TypeScript to JavaScript
npm run build
# Start the bot
npm start
If everything works, you should see your bot come online in Discord and start monitoring your channel. Send a test message and watch it appear in Google Chat like magic!
Taking It a Step Further
Filter Messages by Keywords
Want to only forward important messages? Add some filtering.
// Only forward messages containing "urgent" or "important"
const keywords = ['urgent', 'important', 'breaking'];
if (!keywords.some(keyword => message.content.toLowerCase().includes(keyword))) {
return;
}
Monitor Multiple Channels
Replace single channel monitoring with multiple:
const targetChannels = env.TARGET_CHANNEL_ID.split(',');
if (!targetChannels.includes(channelId) && !targetChannels.includes(parentId)) {
return;
}
Deploying the Bot
WSO2's Choreo is a developer-friendly cloud platform that makes deploying applications as easy as clicking a button. Let's walk through deploying our Discord bot as a Webhook component on Choreo.
Step 1: Commit Your Code to GitHub
You already know what to do here :)
Step 2: Create a Webhook Component
- Click "Create Component" in your project.
- Select Component Type: Choose "Webhook" from the component types. You might want to click "View All Component Types" to find this.
-
Connect Your Repository:
- Select "GitHub" as your source
- Authorize Choreo to access your repositories
- Choose the repository containing your bot code
- Setup the Component:
- Set the Build Preset to
Node.js
- Set the Language Version to 20.x.x
- Set the Build Preset to
- Click Create and Deploy.
For more information, refer to Develop a Webhook documentation on Choreo.
Step 3: Configure Environment Variables
This is where we safely store our Discord token and webhook URL:
- In your component, go to the "DevOps" tab from the left menu.
- Click on "Configs & Secrets" in the left sidebar.
- Click + Create.
- Set the type of the Configuration as "Environment Variables" and mark it as a secret.
- Give the configuration a Display Name: Mine is ping-kong-configs.
- Add your environment variables one by one. After adding each, click Add.
- Click Create to create the configutation group and save it.
For more information, refer to Manage Configurations and Secrets documentation on Choreo.
Some Basic Troubleshooting Steps If Things Go Wrong
Bot shows offline?
- Double-check your Discord token
- Verify the bot has proper server permissions
No messages forwarding?
- Confirm your TARGET_CHANNEL_ID is correct
- Make sure "Message Content Intent" is enabled
- Check that the bot can read the target channel
Google Chat webhook failing?
- Verify your webhook URL is still valid
- Check if the webhook was removed from the Chat space
TypeScript compilation errors?
- Run
npm run build
to see detailed error messages - Make sure all dependencies are installed
Wrapping Up
Congratulations! You've just built a communication bridge that would make you a hero at work. Your team can now stay connected across platforms without missing important messages.
This project demonstrates the power of APIs and webhooks for platform integration. With this foundation, you could do more cool stuff like:
- Add bidirectional messaging
- Support more message types (images, reactions, etc.)
- Create bridges to other platforms like Slack or Microsoft Teams
- Add message threading and advanced formatting
The best part? Your team will think you're some kind of integration wizard, when really you just know how to make different APIs play nicely together.
Now go forth and bridge those communication gaps! And remember: with great power comes great responsibility to not spam your teammates with bot messages. π
P.S. - If this helped you become the office hero, consider buying me a coffee or simply add a reaction. If it broke everything... well, that's what version control is for!
Top comments (0)