DEV Community

Cover image for FunnyQuotes
Diego Cardoso
Diego Cardoso

Posted on

FunnyQuotes

This is a submission for the Twilio Challenge

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

Entertaining Endeavors category.

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;
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)