DEV Community

Cover image for 🚀 Top 4 ways to send notifications about new stars ⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
Eric Allam for Trigger.dev

Posted on • Originally published at trigger.dev

🚀 Top 4 ways to send notifications about new stars ⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️

TL;DR

In the last article, I discussed creating a GitHub stars monitor.
In this article, I want to show you how you can be informed about new stars daily.
We will learn:

  • How to build a generic system to create and use providers.
  • How to use the providers to send notifications.
  • Different use cases of using different providers.

Notifications


Your background job platform 🔌

Trigger.dev is an open-source library that enables you to create and monitor long-running jobs for your app with NextJS, Remix, Astro, and so many more!

 

GiveUsStars

Please help us with a star 🥹.
It would help us to create more articles like this 💖

Star the Trigger.dev repository ⭐️


Let’s set it up 🔥

We are going to create different providers to inform us when there are new stars. We will set up Email, SMS, Slack, and Discord notifications.

Our goal is to make it simple enough for every contributor to contribute more providers in the future.

Each provider will have a different set of parameters, some only API keys and some phone numbers, depending on the providers.

To validate those keys, let’s install zod; it’s a great library to define schemas and check data against them.

You can start by running:

npm install zod --save
Enter fullscreen mode Exit fullscreen mode

Once that is done, create a new folder called providers and then a new file inside called register.provider.ts.

here is the code of the file:

import {Schema} from "zod";

export function registerProvider<T>(
    name: string,
    options: {active: boolean},
    validation: Schema<T>,
    run: (libName: string, stars: number, values: T) => Promise<void>
) {
    // if not active, we can just pass an empty function, nothing will run
    if (!options.active) {
        return () => {};
    }

    // will validate and remove unnecessary values (Security wise)
    const env = validation.parse(process.env);

    // return the function we will run at the end of the job
    return async (libName: string, stars: number) => {
        console.log(`Running provider ${name}`);
        await run(libName, stars, env as T);
        console.log(`Finished running provider ${name}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

It’s not a lot of code, but it might be a little bit complex.

We are starting by creating a new function called registerProvider. That function gets a generic type T, which is basically our required environment variables.

Then we have 4 more parameters:

  • name - that can be any of Twilio, Discord, Slack, or Resend.
  • options - currently, one parameter is the provider active or not?
  • validation - here, we pass the zod schema of our required parameters in our .env file.
  • run - That actually functions to send the notifications. Pay attention that the parameters that are being passed into it are the library name, the number of stars, and the environment variables that we specified in validation

Then we have the actual function:

First, we check if the provider is active or not. If not, we send an empty function.

Then, we validate and extract the variables we specify in our schema. If the variables are missing zod will send an error and will not let the application run.

Lastly, we return a function that gets the library name and the number of stars and triggers the notification.

Inside our providers folder, create a new file called providers.ts and add the following code inside:

export const Providers = [];
Enter fullscreen mode Exit fullscreen mode

Later, we will add all our providers there.


Modify TriggerDev jobs

This article is a continuation of the previous article on creating a GitHub stars monitor.

Edit the file jobs/sync.stars.ts and add the following code to the bottom of the file:

const triggerNotification = client.defineJob({
    id: "trigger-notification",
    name: "Trigger Notification",
    version: "0.0.1",
    trigger: invokeTrigger({
      schema: z.object({
        stars: z.number(),
        library: z.string(),
        providerNumber: z.number(),
      })
    }),
    run: async (payload, io, ctx) => {
      await io.runTask("trigger-notification", async () => {
        return Providers[payload.providerNumber](payload.library, payload.stars);
      });
    }
});
Enter fullscreen mode Exit fullscreen mode

This job gets the number of stars, library name, and provider number and triggers the notification for a specific provider from the previously defined providers.

Now, let’s go ahead and modify getStars at the end of the function add the following code:

for (let i = 0; i < Providers.length; i++) {
      await triggerNotification.invoke(payload.name + '-' + i, {
          library: payload.name,
          stars: stargazers_count - payload.previousStarCount,
          providerNumber: i,
      });
  }
Enter fullscreen mode Exit fullscreen mode

This will trigger a notification for every library.

The full page code:

import { cronTrigger, invokeTrigger } from "@trigger.dev/sdk";
import { client } from "@/trigger";
import { prisma } from "../../helper/prisma";
import axios from "axios";
import { z } from "zod";
import {Providers} from "@/providers/providers";

// Your first job
// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline.
client.defineJob({
  id: "sync-stars",
  name: "Sync Stars Daily",
  version: "0.0.1",
  // Run a cron every day at 23:00 AM
  trigger: cronTrigger({
    cron: "0 23 * * *",
  }),
  run: async (payload, io, ctx) => {
    const repos = await io.runTask("get-stars", async () => {
      // get all libraries and current amount of stars
      return await prisma.repository.groupBy({
        by: ["name"],
        _sum: {
          stars: true,
        },
      });
    });

    //loop through all repos and invoke the Job that gets the latest stars
    for (const repo of repos) {
      await getStars.invoke(repo.name, {
        name: repo.name,
        previousStarCount: repo?._sum?.stars || 0,
      });
    }
  },
});

const getStars = client.defineJob({
  id: "get-latest-stars",
  name: "Get latest stars",
  version: "0.0.1",
  // Run a cron every day at 23:00 AM
  trigger: invokeTrigger({
    schema: z.object({
      name: z.string(),
      previousStarCount: z.number(),
    }),
  }),
  run: async (payload, io, ctx) => {
    const stargazers_count = await io.runTask("get-stars", async () => {
      const {data} = await axios.get(`https://api.github.com/repos/${payload.name}`, {
        headers: {
          authorization: `token ${process.env.TOKEN}`,
        },
      });
      return data.stargazers_count as number;
    });

    await io.runTask("upsert-stars", async () => {
      await prisma.repository.upsert({
        where: {
          name_day_month_year: {
            name: payload.name, month: new Date().getMonth() + 1, year: new Date().getFullYear(), day: new Date().getDate(),
          },
        }, update: {
          stars: stargazers_count - payload.previousStarCount,
        }, create: {
          name: payload.name, stars: stargazers_count - payload.previousStarCount, month: new Date().getMonth() + 1, year: new Date().getFullYear(), day: new Date().getDate(),
        },
      });
    });

    for (let i = 0; i < Providers.length; i++) {
        await triggerNotification.invoke(payload.name + '-' + i, {
            library: payload.name,
            stars: stargazers_count - payload.previousStarCount,
            providerNumber: i,
        });
    }
  },
});

const triggerNotification = client.defineJob({
    id: "trigger-notification",
    name: "Trigger Notification",
    version: "0.0.1",
    trigger: invokeTrigger({
      schema: z.object({
        stars: z.number(),
        library: z.string(),
        providerNumber: z.number(),
      })
    }),
    run: async (payload, io, ctx) => {
      await io.runTask("trigger-notification", async () => {
        return Providers[payload.providerNumber](payload.library, payload.stars);
      });
    }
});
Enter fullscreen mode Exit fullscreen mode

Now, the fun part 🎉

Let’s go ahead and create our providers!

First create a new folder called providers/lists


1. Discord

Discord

Create a new file called discord.provider.ts and add the following code:

import {object, string} from "zod";
import {registerProvider} from "@/providers/register.provider";
import axios from "axios";

export const DiscordProvider = registerProvider(
    "discord",
    {active: true},
    object({
        DISCORD_WEBHOOK_URL: string(),
    }),
    async (libName, stars, values) => {
        await axios.post(values.DISCORD_WEBHOOK_URL, {content: `The library ${libName} has ${stars} new stars!`});
    }
);
Enter fullscreen mode Exit fullscreen mode

As you can see, we are using the registerProvider to create a new provider called DiscordProvider

  • We set the name to discord
  • We set it to be active
  • We specify that we need an environment variable called DISCORD_WEBHOOK_URL.
  • We use a simple post command with Axios to add the information to the check.

To get DISCORD_WEBHOOK_URL:

  1. Go to your Discord server
  2. Click edit on one of the channels
  3. Go to Integrations
  4. Click Create Webhook
  5. Click on the created webhook, then click Copy webhook URL

Edit our .env file on our root project and add

SLACK_WEBHOOK_URL=<your copied url>
Enter fullscreen mode Exit fullscreen mode

Spidy


2. Slack

Slack

Create a new file called slack.provider.ts and add the following code:

import {object, string} from "zod";
import {registerProvider} from "@/providers/register.provider";
import axios from "axios";

export const SlackProvider = registerProvider(
    "slack",
    {active: true},
    object({
        SLACK_WEBHOOK_URL: string(),
    }),
    async (libName, stars, values) => {
        await axios.post(values.SLACK_WEBHOOK_URL, {text: `The library ${libName} has ${stars} new stars!`});
    }
);
Enter fullscreen mode Exit fullscreen mode

As you can see, we are using the registerProvider to create a new provider called SlackProvider

  • We set the name to slack
  • We set it to be active
  • We specify that we need an environment variable called SLACK_WEBHOOK_URL.
  • We use a simple post command with Axios to add the information to the check.

To get SLACK_WEBHOOK_URL:

  1. Create a new Slack app by using this URL: https://api.slack.com/apps?new_app=1
  2. Select the first option: “From scratch”
  3. Give an app name (any) and Slack the workspace you would like to add the notifications too. Click Create App.
  4. In “Add features and functionalities,” click Incoming hook
  5. In Activate Incoming Webhooks, change it to “On”.
  6. Click on “Add New Webhook to Workspace”.
  7. Select the channel you want and click “Allow”.
  8. Copy the webhook URL.

Edit our .env file on our root project and add

SLACK_WEBHOOK_URL=<your copied url>
Enter fullscreen mode Exit fullscreen mode

SlackBot


3. Email

Email

You can use different kinds of email providers. For example, we will use Resend to send emails.

For that, let’s install resend on our project:

npm install resend --save
Enter fullscreen mode Exit fullscreen mode

Create a new file called resend.provider.ts and add the following code:

import {object, string} from "zod";
import {registerProvider} from "@/providers/register.provider";
import axios from "axios";
import { Resend } from 'resend';

export const ResendProvider = registerProvider(
    "resend",
    {active: true},
    object({
        RESEND_API_KEY: string(),
    }),
    async (libName, stars, values) => {
        const resend = new Resend(values.RESEND_API_KEY);
        await resend.emails.send({
            from: "Eric Allam <eric@trigger.dev>",
            to: ['eric@trigger.dev'],
            subject: 'New GitHub stars',
            html: `The library ${libName} has ${stars} new stars!`,
        });
    }
);
Enter fullscreen mode Exit fullscreen mode

As you can see, we are using the registerProvider to create a new provider called ResendProvider

  • We set the name to resend
  • We set it to be active
  • We specify that we need an environment variable called RESEND_API_KEY.
  • We use the Resend library to send an email to ourselves with the new number of stars.

To get RESEND_API_KEY:

  1. Create a new account at: https://resend.com
  2. Go to API Keys or use this URL https://resend.com/api-keys
  3. Click “+ Create API Key,” add the Key name, choose “Sending access” and use the default “All Domains”. Click Add.
  4. Copy the API Key.

Edit our .env file on our root project and add

RESEND_API_KEY=<your API key>
Enter fullscreen mode Exit fullscreen mode

Eric Allam


4. SMS

Twilio

SMS are a little bit more complex as they require multiple variables.

For that, let’s install Twilio on our project:

npm install twilio --save
Enter fullscreen mode Exit fullscreen mode

Create a new file called twilio.provider.ts and add the following code:

import {object, string} from "zod";
import {registerProvider} from "@/providers/register.provider";
import axios from "axios";
import client from 'twilio';

export const TwilioProvider = registerProvider(
    "twilio",
    {active: true},
    object({
        TWILIO_SID: string(),
        TWILIO_AUTH_TOKEN: string(),
        TWILIO_FROM_NUMBER: string(),
        TWILIO_TO_NUMBER: string(),
    }),
    async (libName, stars, values) => {
        const twilio = client(values.TWILIO_SID, values.TWILIO_AUTH_TOKEN);
        await twilio.messages.create({
            body: `The library ${libName} has ${stars} new stars!`,
            from: values.TWILIO_FROM_NUMBER,
            to: values.TWILIO_TO_NUMBER,
        });
    }
);
Enter fullscreen mode Exit fullscreen mode

As you can see, we are using the registerProvider to create a new provider called TwilioProvider

  • We set the name to twilio
  • We set it to be active
  • We specify that we need environment variables: TWILIO_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER and TWILIO_TO_NUMBER
  • We use the Twilio create function to send an SMS.

To get TWILIO_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER and TWILIO_TO_NUMBER

  1. Create a new account at https://twilio.com
  2. Mark that you want to use it to send SMSs.
  3. Click “Get a phone number”
  4. Copy the “Account SID”, “Auth Token” and “My Twilio Phone Number”

Edit our .env file on our root project and add

TWILIO_SID=<your SID key>
TWILIO_AUTH_TOKEN=<your AUTH TOKEN key>
TWILIO_FROM_NUMBER=<your FROM number>
TWILIO_TO_NUMBER=<your TO number>
Enter fullscreen mode Exit fullscreen mode

TwilioSMSs


Create new providers

As you can see, now it’s super easy to create providers.

You can also use the open-source community to create new providers since they only need to create one new file inside the providers/list directory.

The last thing to do is edit your providers.ts file and add all your providers.

import {DiscordProvider} from "@/providers/list/discord.provider";
import {ResendProvider} from "@/providers/list/resend.provider";
import {SlackProvider} from "@/providers/list/slack.provider";
import {TwilioProvider} from "@/providers/list/twilio.provider";

export const Providers = [
    DiscordProvider,
    ResendProvider,
    SlackProvider,
    TwilioProvider,
];
Enter fullscreen mode Exit fullscreen mode

Feel free to create more providers for push notifications, web push notifications, in-app notifications, etc.

And you are done 🥳


Let's connect! 🔌

As an open-source developer, you're invited to join our community to contribute and engage with maintainers. Don't hesitate to visit our GitHub repository to contribute and create issues related to Trigger.dev.

The source for this tutorial is available here:

https://github.com/triggerdotdev/blog/tree/main/stars-monitor-notifications

Thank you for reading!

Top comments (9)

Collapse
 
daveparr profile image
Dave Parr

This is an interesting idea. When a user stars a repo, what do you think it signifies? If a repo has lots of stars what do you interpret that to mean? Do you think it's quality of code or do you think it's more about how common the problem is for other GitHub users?

I'm curious because I started my own project using GitHub stars as well, and I'm trying to understand more about why users star repos and how they use them afterwards :)

Collapse
 
srbhr profile image
Saurabh Rai

Hey @daveparr, I know why this matters as someone with an OSS Project with 3.3K Stars.
It's more like an outreach for you. The people who star your repo, bookmark it. They are notified of the releases, discussions, issues, and more. And this comes into their GitHub feed. Also, if someone uses a filter to search for specific topics like resume machine-learning, or search engine it filters by the stars. The more the stars, the more you rank on top.
That's all. Like why anyone should subscribe on YouTube and press the bell icon. It's the same with GH Stars; they matter, but to an extent, that's all.

Collapse
 
matijasos profile image
Matija Sosic

Great banner! :)

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Great article, I'm starting to use Trigger.dev quite a bit and I love the ease of use.

Collapse
 
nevodavid profile image
Nevo David

I love how TriggerDev handling background jobs!

Collapse
 
generator_garimu profile image
Garimu Alonso

This is an interesting article, never thought of using to create something like github star monitor. I may create one for Github forks.

Collapse
 
srbhr profile image
Saurabh Rai

Erik (@maverickdotdev) never fails to surprise us with awesome & practical use cases for Trigger.dev.

Collapse
 
xavierdev profile image
Lucille Xavier

Just wanted to say that this was the best written guide I've seen in a long time.

Collapse
 
glntn101 profile image
Luke

Good job, thank you!