DEV Community

Cover image for Nyla the Cat: Email Boss Battler
Ansell Maximilian
Ansell Maximilian

Posted on

Nyla the Cat: Email Boss Battler

This is a submission for the Nylas Challenge: Galaxy Brain.

Quick Links

What I Built and Why

Getting bored checking emails the regular way? Nyla is here to save the day!

Do you not like checking/reading your emails every day? Nyla can help you with that. After playing this browser game, you're either going to enjoy your email checking process from now on OR start appreciating what you had before.

Either way, this game will make you enjoy reading your boring emails.

Nyla the Cat Game

Nyla the Cat is a browser-based video game centered around Nyla. A feline warrior in the Kingdom of Felinia, which has been overrun by boring corporate rules. The cats in Felinia used to be able to express themselves freely in their emails; now with the advent of corporations and Standard Operating Procedures (SPOs), emails have become boring.

Nyla is here to save the day.

How It Works

In this game, you'll play Nyla herself. You will choose which unread email you want to read from your inbox. Unfortunately, it's not as easy anymore. To read the chosen email, you'll now have to fight the Legendary Email Boss. It's holding onto your email hostage!

After defeating this Email Boss, you will not only be able to view the original contents of the email; you'll also be able to "Purrify" the email to turn it into cat-speak to make it more interesting. Using AI technologies, the original email body will take on a more cat-like, non-boring form filled with puns like "Purrfect", etc.

After each successful battle, you'll also gain XP. This XP will be used to level up your Nyla. While leveling up your Nyla, you can upgrades one of her stats such as Attack, Health, and Speed.

There are also the "Trinket System". Each day, you'll be able to spin for a random Trinket, which are ancient artifacts from Nyla's world. You can equip these Trinkets for a boost of Nyla's stats.

Finally, you can compare your Nylas to your contacts. See which of your friends have the highest leveled Nylas (the cat).

All of this was possible using Nylas APIs. Check it out here.

Inspiration

I was super interested in the Galaxy Brain Prompt and how I could turn Nylas' available APIs to make a game.

I decided to theme the game around "Cats" because Nylas sounds like "nyan", which is associated with cats? It's a stretch, but I liked it. So I created the character Nyla.

I also thought this would be a good chance to learn some new skills. I had ZERO experience in making games and even less with pixel art, so excuse the terrible art. However, I think I really did improve in those areas at the end of this project.

I was also inspired by one of my favorite games "Hollow Knight". This game is kind of like that. You can keep progressing and improving your character. You can also get enhancements from items!

Demo

Video


Video Demo

Gameplay, Flow, and Features

Home Page

Guest Home Page

Logging In with Nyla

Logging in With Google

Home page when user is logged in

Battle

Choose Email to Read

choosing email to read

Set Up Email Boss

set up boss parameters

Fight

the actual gameplay battle

Finally Read Your Email!

reading the email

Your Nyla and Trinkets

Nyla

nyla profile

Trinkets

trinkets list

spinning for a new trinket

Leaderboard/Friends List

leaderboard

Code

Github repo

Check out the Github repo for the code here.

Development

In this section I will be talking about the more technical side such as technology stacks, how Nylas was integrated within the project, etc. Feel free to skip this part if you're not interested.

I want to share how I did things to open things for suggestion first and foremost. But I also want knowledge-share in detail how I did things regarding code and hopefully readers will gain something new once they finish reading this article.

Technology Stack

  • NextJS: I used NextJS to cover the front end of the application as well as to provide a secure place to call the needed Nylas APIs. As Nylas APIs require me to provide the app secret as a bearer token, I needed the secure environment that NextJS route handlers provide.
  • Appwrite: I used Appwrite's database to store two important things. User grants and Player Nylas. User grants will allow the logged in user to fetch their grant id based on their session token.
  • Nylas: Nylas is the core of this game, both as the prompt for this challenge and as the service that directly powers the main features.
  • Google's Gemini: I used Google's Gemini AI to "Purrify" emails to rewrite emails in "cat speak". For example, "Hello, the weather is perfect!" could turn into "Meow-llo, the weather is purr-fect!".

How Nylas was Used

Authentication

Before I did anything with one of the many APIs Nylas provides, I had to authenticate my users.

I had a lot of trouble the first time with this, so I wanted to share a detailed walkthrough of how I did it with NextJS.

I'm going to assume you've already created a Nyla application and received its client id and secret. Don't forget to add a callback URL to handle the recording of the grant id.

I was confused about this, but remember that this callback url is what Nylas will redirect to after a successful authentication, so this should be done in a server environment. NextJS' route handler is perfect for this.

Here's a step by step:

  1. Create a login button to display to unauthenticated users.
function NylasLoginButton() {
  const router = useRouter();

  const handle = () => {
    const baseUrl = "https://api.eu.nylas.com/v3/connect/auth";
    const clientId = "<APP_CLIENT_ID>";
    const callbackURL = "<BASE_URL>/api/nylas/callback";

    const responseType = "code";
    const provider = "google";
    const accessType = "online";

    const authUrl = `${baseUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent(
      callbackURL
    )}&response_type=${responseType}&provider=${provider}&access_type=${accessType}`;
    3;

    router.push(authUrl);
  };
  return (
    <button className="text-4xl hover:scale-105" onClick={handle}>
      Login
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

What this button does basically is redirecting the user using router.push() to https://api.eu.nylas.com/v3/connect/auth, providing it with the necessary query parameters. In this case, we give it our app client id, the callback url we want Nylas to redirect to, the response type which we set to "code", and the provider (I'm using Google).

Nylas will then take care of the OAuth authentication with Google. Once you've granted Nylas the necessary permissions, Nylas will finally redirect to the callback URL you've defined as the query parameters.

Next, I set up a route handler in accordance to the callback I've defined (/api/nylas/callback). So, I make a file app/api/nylas/callback/route.ts. Here are the contents:

export async function GET(req: NextRequest) {
  const searchParams = req.nextUrl.searchParams;

  const code = searchParams.get("code");

  if (!code) {
    return NextResponse.json(
      { error: "Authorization failed or was canceled." },
      { status: 400 }
    );
  }

  try {
    console.log("CODE", code);
    const tokenResponse = await fetch(
      "https://api.eu.nylas.com/v3/connect/token",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          code: code,
          client_id: process.env.NEXT_PUBLIC_NYLAS_CLIENT_ID,
          client_secret: process.env.NYLAS_API_KEY,
          redirect_uri: `${String(
            process.env.NEXT_PUBLIC_BASE_URL
          )}/api/nylas/callback`,
          grant_type: "authorization_code",
          code_verifier: "nylas",
        }),
      }
    );

    if (!tokenResponse.ok) {
      return NextResponse.json(
        { error: "Failed to exchange code for token." },
        { status: 500 }
      );
    }

    const tokenData = await tokenResponse.json();

    let grantRecord: GrantRecord | null = null;

    try {
      const res = await databases.listDocuments(
        config.dbId,
        config.grantCollectionId,
        [Query.equal("email", tokenData.email)]
      );

      if (res.total < 1) {
        throw new Error("No grant saved.");
      } else {
        grantRecord = res.documents[0] as GrantRecord;
      }
      console.log("FOUND CREATED");
    } catch (error) {
      try {
        grantRecord = (await databases.createDocument(
          config.dbId,
          config.grantCollectionId,
          ID.unique(),
          {
            grant_id: tokenData.grant_id,
            access_token: tokenData.access_token,
            refresh_token: tokenData.refresh_token,
            email: tokenData.email,
            scope: tokenData.scope,
            token_type: tokenData.token_type,
            id_token: tokenData.id_token,
          }
        )) as GrantRecord;
      } catch (error) {
        if (error instanceof Error) console.log(error.message);
        return NextResponse.json(
          { error: "An unexpected error occurred." },
          { status: 500 }
        );
      }
    }

    // creating player Nyla
    let playerNyla: PlayerNyla | null = null;

    try {
      const res = await databases.listDocuments(
        config.dbId,
        config.playerNylaCollectionId,
        [Query.equal("email", tokenData.email)]
      );

      if (res.total < 1) {
        throw new Error("No player Nyla saved.");
      } else {
        playerNyla = res.documents[0] as PlayerNyla;
      }
    } catch (error) {
      try {
        playerNyla = (await databases.createDocument(
          config.dbId,
          config.playerNylaCollectionId,
          grantRecord.$id,
          {
            grant_id: tokenData.grant_id,
            email: tokenData.email,
          }
        )) as PlayerNyla;
      } catch (error) {
        if (error instanceof Error) console.log(error.message);
        return NextResponse.json(
          { error: "An unexpected error occurred." },
          { status: 500 }
        );
      }
    }

    await createSession(grantRecord.$id);

    // Set up a session or redirect the user
    const res = NextResponse.redirect(`${process.env.NEXT_PUBLIC_BASE_URL}`);

    return res;
  } catch (error) {
    console.log("BOTTOM", error);
    if (error instanceof Error) console.log(error.message);
    return NextResponse.json(
      { error: "An unexpected error occurred." },
      { status: 500 }
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

There's a lot of code here, so I'll summarize. First thing to remember is that this URL will only be called by Nylas after you authenticate with a Google account.

Nylas will call this URL with your code like this /api/nylas/callback?code="", so you will get it with the following line const code = searchParams.get("code").

Then, you will use this code to perform a token exchange with the provider (Google). So, I make a POST request to "https://api.eu.nylas.com/v3/connect/token" with a bunch of data as the body JSON, including client id, client secret, and the code we just got. Don't forget to add the matching callback url.

Now tokenData should contain a bunch of things you will need, namely email and grant_id.

After token is received, the code will be pretty specific to my stack, but you can easily adapt it to others. Here I am using Appwrite to store grant ids and Player Nylas (meaning each user's version of Nyla the cat, with their respective xp, level, and upgrades).

First, I check whether a grant id has already been recorded with the user with the particular email from the token, if it has I skip; else, I create a new record with Appwrite's Databases.createDocument(). This record will come in handy later in many places.

Then, I do the same thing with Player Nyla. Create one (if there isn't already one associated with the particular grant record). I use the grant id's document $id as the Player Nyla $id so it will be easier to query for them both in the future.

After grant record has been created/fetched. I create a session for the user and give it the grant record's $id (from Appwrite). I recommend looking at this session tutorial by Vercel here.

Remember that I've given the session the grant record's $id. await createSession(grantRecord.$id). I then redirect the user to the home page.

Then, in server pages, I can check for a user's authenticated status with the following code:

const session = await decrypt(cookies().get("session")?.value);

if(!session?.grantRecordId){
  // user is not authenticated because session doesn't exist
  redirect("/somehwere");
}
Enter fullscreen mode Exit fullscreen mode

If a user is authenticated, I can get their grant record's $id on the server and subsequently use their grant id to fetch relevant data or perform other things through Nyla's APIs. More on this below.

Getting Unread Emails

Here's an example of using the user's session to use Nylas email API to fetch messages.

export async function GET(req: NextRequest) {
  const session = await decrypt(cookies().get("session")?.value);
  const searchParams = req.nextUrl.searchParams;
  const unread = searchParams.get("unread") === "true";
  const limit = Number(searchParams.get("limit"));

  if (!session?.grantRecordId) {
    return NextResponse.json(
      {
        error: true,
        message: "Unauthorized",
      },
      {
        status: 401,
      }
    );
  }

  const grant = (await databases.getDocument(
    config.dbId,
    config.grantCollectionId,
    session.grantRecordId
  )) as GrantRecord;

  const res = await axios.get(
    `https://api.eu.nylas.com/v3/grants/${grant.grant_id}/messages`,
    {
      headers: {
        Accept: "application/json",
        Authorization: `Bearer ${String(process.env.NYLAS_API_KEY)}`,
        "Content-Type": "application/json",
      },
      params: {
        limit: limit ? limit : 1,
        unread: unread ? unread : undefined,
      },
    }
  );

  const latestEmails = res.data as NylasResponse<Email>;

  return NextResponse.json(
    latestEmails.data.map((e) => ({ ...e, grant_id: undefined }))
  );
}
Enter fullscreen mode Exit fullscreen mode

Generally, this will be the basic pattern of using Nylas APIs. You perform a request to https://api.eu.nylas.com/v3/grants/${grant.grant_id}/,replacing grant.grant_id with your grant id, however you fetch it.

In my case, I use the aforementioned grant record's $id from the session to fetch it using Appwrite's Databases.getDocument(). After that, the code gets access to the real grant id in a secure place.

Then I perform a GET request to that base Nylas API url and add /messages with the query parameters limit and unread to fetch the 5 latest unread emails.

Marking Email as Read

Performing PUT request to mark an email with the provided ID as "read". I receive emailId from the list messages api.

await axios.put(
      `https://api.eu.nylas.com/v3/grants/${grantRecord.grant_id}/messages/${emailId}`,
      {
        unread: false,
      },
      {
        headers: {
          Accept: "application/json",
          Authorization: `Bearer ${String(process.env.NYLAS_API_KEY)}`,
          "Content-Type": "application/json",
        },
      }
    );
Enter fullscreen mode Exit fullscreen mode
Getting contacts

I also use the contacts API to get the user's contacts and determine whether or not they have an associated Nyla the Cat in the database. If they do, I display their level.

This is one of my favorite parts of the APIs. I think this will be huge for easily implementing a friends feature with Nylas.

I used the following API https://api.eu.nylas.com/v3/grants/${grant.grant_id}/contacts

Sending Invitations to Play

The final Nylas API I used is for sending invite messages. In the leaderboard/friends page, you can see which of your contacts has/hasn't played Nyla the Cat. If they haven't you can invite them by sending them an email. I used the following API https://api.eu.nylas.com/v3/grants/${grantRecord.grant_id}/messages/send

Your Journey

I really enjoyed my time developing this game. I learned a lot about Oauth, callback URLs, and game development. I know callback URLs seems insignificant, but with third party services, I usually never needed to interact with the concept of callback URLs. So I definitely learned a lot of new things.

I already mention above what I used Nylas for and how I leveraged it to develop Nyla the cat. But, again, one of the APIs that stood out the most to me was the contacts API.

I thought it was cool how easily I could create the friends system on my app. I think it could have a lot of applications. First off, you can share your friend list across multiple applications. Second, you can also use Nylas to add/remove friends without other services like a database. I think this is huge. I love it.

Sending invitation to play using the send message API was also pretty cool.

Top comments (2)

Collapse
 
harshitads44217 profile image
Harshita Sharma D

This is just amazing! Really liked the concept.

Collapse
 
harishkotra profile image
Harish Kotra (he/him)

This is insane! Great job!