FunnyQuotes
Tags: devchallenge, twiliochallenge, ai, twilio
Published: 2024-06-20 20:16:58

*This is a submission for the [Twilio Challenge ](https://dev.to/challenges/twilio)*

## What I Built
FunnyQuotes is a site hosted on Cloudflare. Every day, five quotes and five images are generated by AI on the edge by Cloudflare Workers. The AI models used is `@cf/meta/llama-3-8b-instruct` to generate the text and `@cf/stabilityai/stable-diffusion-xl-base-1.0` to generate the images.

## Demo
https://funnyquotes.pages.dev/

## Twilio and AI
FunnyQuotes uses Twilio API to send SMS message to subscribed phones directly from Cloudflare Worker. The user phones, generated images and quotes are saved on Cloudflare KV storage.

## Additional Prize Categories

<!-- Does your submission qualify for any additional prize categories (Twilio Times Two, Impactful Innovators, Entertaining Endeavors)? Please list all that apply.  -->
Entertaining Endeavors category.

<!-- Team Submissions: Please pick one member to publish the submission and credit teammates by listing their DEV usernames directly in the body of the post. -->
Team Submissions:
Diego Cardoso (diegocardoso93)


Take a look at the full project code `_worker.js`:
```
import { Buffer } from 'node:buffer';

var CLOUDFLARE_ACCOUNT_ID;
var CLOUDFLARE_BEARER_TOKEN;
var TWILIO_ACCOUNT_SID;
var TWILIO_AUTH_TOKEN;
var TWILIO_PHONE_NUMBER;

export default {
  async fetch(request, env) {
    CLOUDFLARE_ACCOUNT_ID = env.CLOUDFLARE_ACCOUNT_ID;
    CLOUDFLARE_BEARER_TOKEN = env.CLOUDFLARE_BEARER_TOKEN;
    TWILIO_ACCOUNT_SID = env.TWILIO_ACCOUNT_SID;
    TWILIO_AUTH_TOKEN = env.TWILIO_AUTH_TOKEN;
    TWILIO_PHONE_NUMBER = env.TWILIO_PHONE_NUMBER;

    const url = new URL(request.url);

    if (url.pathname.startsWith('/generate')) {
      const phrases = (await generatePhrases()).split('\n').filter(s => s[0]>0 && s[0]<10);
      for (const i in phrases) {
        await env.FUNNYQUOTES.put(`IMG${i}`, `data:image/png;base64,${new Buffer(await generateImage(phrases[i])).toString('base64')}`);
        await env.FUNNYQUOTES.put(`QUOTE${i}`, phrases[i]);
      }

      return new Response('OK');
    }

    if (url.pathname.startsWith('/notify')) {
      const phones = JSON.parse(await env.FUNNYQUOTES.get(`PHONES`) || '[]');
      let ret = [];
      for (const phone of phones) {
        ret.push(await sendSMS(`Funny Quotes: daily feed. https://funnyquotes.pages.dev`, phone));
      }
      return new Response(JSON.stringify(ret));
    }

    if (url.pathname.startsWith('/subscribe')) {
      const phone = url.searchParams.get('phone') || '';
      if (phone.length < 6) { return; }
      const phones = JSON.parse(await env.FUNNYQUOTES.get(`PHONES`) || '[]');
      if (!phones.includes(phone)) {
        phones.push(phone);
      }
      await env.FUNNYQUOTES.put(`PHONES`, JSON.stringify(phones));
      return new Response(JSON.stringify({success: 1}));
    }

    const img = url.searchParams.get('img') || '';
    if (img) {
      return new Response(JSON.stringify({
        img: await env.FUNNYQUOTES.get(`IMG${img}`),
        quote: await env.FUNNYQUOTES.get(`QUOTE${img}`)
      }), { headers: { 'Content-Type': 'text/json' } });
    }

    return new Response(getTemplate(), { headers: { 'Content-Type': 'text/html' } });
  },
}

async function sendSMS(message, phone) {
  const url = `https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json`;
  const params = new URLSearchParams({
    To: phone,
    From: TWILIO_PHONE_NUMBER,
    Body: message,
  });

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: `Basic ${Buffer.from(`${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}`).toString('base64')}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: params,
  });

  return response.json();
}

async function generatePhrases(q) {
  const { result } = await run("@cf/meta/llama-3-8b-instruct", {
    messages: [
      { role: "system", content: "language: en-us" },
      {
        role: "user",
        content: q || `generate 5 funny motivational phrases for life`,
      },
    ],
  });
  return result.response;
}

async function generateImage(q) {
  return await run("@cf/stabilityai/stable-diffusion-xl-base-1.0", {
    prompt: `no text. oil painted style. "${q || 'random'}"`
  }, 'arrayBuffer');
}

async function run(model, input, returnType) {
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/run/${model}`,
    {
      headers: { Authorization: `Bearer ${CLOUDFLARE_BEARER_TOKEN}` },
      method: "POST",
      body: JSON.stringify(input),
    }
  );
  let result;
  if (returnType == 'arrayBuffer') {
    result = await response.arrayBuffer();
  } else {
    result = await response.json();
  }
  return result;
}


function getTemplate() {
  const TEMPLATE = `<!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Funny Quotes</title>
      <style>
        html, body, * {
          margin: 0;
          box-sizing: content-box;
        }
        .top-fixed {
          position: fixed;
          background: white;
          padding: 16px 0;
          width: 100%;
          margin: auto;
          text-align: center;
          border-bottom: 1px solid #ccc;
        }
        .top-fixed h1 {
          font-size: 26px;
        }
        .bottom-fixed {
          position: fixed;
          bottom: 0;
          width: 100%;
          margin: auto;
          text-align: center;
          background: white;
          padding: 6px 0 10px 0;
          border-top: 1px solid #ccc;
        }
        .bottom-fixed p {
          padding-bottom: 4px;
        }
        .bottom-fixed input, .bottom-fixed button {
          height: 26px;
        }
        .container {
          display: flex;
          flex-direction: column;
          align-items: center;
          padding: 70px 0;
        }
        .card {
          border-radius: 3px;
          border: 1px solid #ccc;
          margin: auto;
          max-width: 512px;
          padding: 10px;
          margin: 10px;
        }
        .card img {
          width: 100%;
        }
        .card p {
          padding-top: 10px;
          font-size: 24px;
          text-align: center;
        }
      </style>
    </head>
    <body>
      <div class="top-fixed">
        <h1 id="title"></h1>
      </div>
      <div class="container">
        <div id="card0" class="card">Loading...</div>
        <div id="card1" class="card">Loading...</div>
        <div id="card2" class="card">Loading...</div>
        <div id="card3" class="card">Loading...</div>
        <div id="card4" class="card">Loading...</div>
      </div>

      <div class="bottom-fixed">
        <p>Subscribe to our daily news quotes notification</p>
        <input id="phone" type="text" placeholder="Enter your phone number..."></input>
        <button onclick="subscribe()">subscribe</button>
      </div>

      <script>
        window.addEventListener('load', async function (event) {
          document.querySelector('#title').innerText = 'Funny Quotes - ' + new Date().toLocaleDateString('en-US');
    
          for (let i=0;i<5;i++){
            const response = await fetch('?img='+i);
            const json = await response.json();
            document.querySelector('#card'+i).innerHTML = '<img src="'+json.img+'" /><p>'+json.quote+'</p>';
          }
        });

        async function subscribe() {
          const phone = document.querySelector('#phone');
          if (!phone.value || phone.value.length < 6) {
            alert('please enter your phone number');
            return;
          }
          const response = await fetch('/subscribe/?phone='+encodeURIComponent(phone.value));
          const json = await response.json();
          if (json.success) {
            alert('You are now subscribed.');
            phone.value = '';
          }
        }
      </script>
    </body>
  </html>
  `;
  return TEMPLATE;
}

```