DEV Community

Cover image for How to Build a Discord to Google Chat Bridge Bot (And Why Your Team Will Love You)
Suvin Nimnaka
Suvin Nimnaka

Posted on

How to Build a Discord to Google Chat Bridge Bot (And Why Your Team Will Love You)

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.

Discord and Google Chat integration

Image credits: Masters of AI & Automation

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:

  1. Head over to the Discord Developer Portal
  2. Click "New Application" and give it a name (I went with "PingKong" - King Kong, but for pinging across apps.)
  3. Navigate to the "Bot" tab and click "Add Bot".
  4. Important: Enable "Message Content Intent" - this is like giving your bot permission to actually read what people are saying.
  5. Copy that bot token (keep it secret, keep it safe).
  6. In OAuth2 URL Generator, select "bot" scope and these permissions:
    • Read Messages/View Channels
    • Send Messages
    • Read Message History
  7. 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:

  1. Open Google Chat and find your Chat Space.
  2. Click the space name β†’ "Apps & integrations"
  3. Click "Add webhooks"
  4. Give it a name.
  5. Add an avatar if you're feeling fancy.
  6. Hit "Save" and copy that webhook URL

Step 3: Project Setup

Time to create our project structure:

  1. Create a new directory for the project home.
mkdir discord-to-gchat-bot
cd discord-to-gchat-bot
Enter fullscreen mode Exit fullscreen mode
  1. Initialize a new Node project.
npm init -y
Enter fullscreen mode Exit fullscreen mode
  1. Install our dependencies.
npm install discord.js axios dotenv typescript @types/node
npm install -D typescript ts-node
Enter fullscreen mode Exit fullscreen mode
  1. Set up TypeScript.
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
                                };
                            };
                        }[];
                    };
                }[];
            }[];
        };
    }[];
}
Enter fullscreen mode Exit fullscreen mode

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
    ],
});
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Monitor Multiple Channels

Replace single channel monitoring with multiple:

const targetChannels = env.TARGET_CHANNEL_ID.split(',');
if (!targetChannels.includes(channelId) && !targetChannels.includes(parentId)) {
    return;
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Click "Create Component" in your project.
  2. Select Component Type: Choose "Webhook" from the component types. You might want to click "View All Component Types" to find this.
  3. Connect Your Repository:
    • Select "GitHub" as your source
    • Authorize Choreo to access your repositories
    • Choose the repository containing your bot code
  4. Setup the Component:
    • Set the Build Preset to Node.js
    • Set the Language Version to 20.x.x
  5. Click Create and Deploy.

For more information, refer to Develop a Webhook documentation on Choreo.

Creating a Choreo Webhook Component

Step 3: Configure Environment Variables

This is where we safely store our Discord token and webhook URL:

  1. In your component, go to the "DevOps" tab from the left menu.
  2. Click on "Configs & Secrets" in the left sidebar.
  3. Click + Create.
  4. Set the type of the Configuration as "Environment Variables" and mark it as a secret.
  5. Give the configuration a Display Name: Mine is ping-kong-configs.
  6. Add your environment variables one by one. After adding each, click Add.
  7. Click Create to create the configutation group and save it.

Creating a Choreo Configuration Group

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!

Buy Me A Coffee

Top comments (0)