Adding a comment box with google firebase

1. Why

I never played enough with firebase and felt like a good opportunity. The final result is far from perfect, without optimizations for loading too many comments, spam prevention and dynamically loading.

I had the idea after watching this unfinished series on youtube.

2. Code

2.1. Html

Nothing really special here.

<div id="comments">
    <h2>Comments</h2>
    <form id="new-comment-form">
        <label for="username">Post a new comment</label><br>
        <input type="text" id="comment-name" placeholder="Your name" minlength="4" maxlength="48" required>
        <textarea id="comment-box" placeholder="Your comment here" minlength="10" maxlength="512" rows="5" style="resize: none;" required></textarea>
        <button type="submit" id="post-comment">Post</button>
    </form>
    <div id="comments-section">
        <h4 id="nocomment">Be the first to comment!</h4>
        <ul id="comment-list">
        </ul>
    </div>
</div>
<script type="module" src="/comments.js"></script>

2.2. Css

This could be improved, hire a designer? I wont.

#comments {
    color-scheme: dark;
    margin: 10px;
    display: none;
}

#new-comment-form {
    padding: 5px;
}

#comment-list {
    padding: 5px;
    list-style: none;
}

#comment-box, #post {
    border: none;
    border-radius: 5px;
    boder-color: #3c81ba;
    margin: 10px 0 6px 0;
}

#comments-section {
    margin-top: 10%;
    overflow-y: auto;
    max-height: 500px;
}

#post-comment {
    float: right;
    margin-right: 5%;
    border-radius: 20%;
}

#post-comment:hover{
    background-color: #a20a29;
}


div.comment {
    background-color: #212121;
    border-radius: 15px;
    margin-top: 20px;
    padding-left: 10px;
    padding-bottom: 5px;
}

div.comment span {
    color: #5ea6e1;
}

div.comment p {
    text-align: center;
    text-justify: inter-word;
    margin: 5px 12px;
    background: #373636;
    border: #3c81ba;
    border-width: 2px;
    border-radius: 5px;
    font-size: small;
    border-style: solid;
}

div.comment timestamp {
    float: right;
    padding-right: 50%;
    font-size: small;
}

2.3. Javascript

Now here the magic happens. Ignore the cookie stuff. We initialize a firebase realtime database client as firebase will recommend us itself, choosing the cdn route.

// Import the functions you need from the SDKs you need
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.11/firebase-app.js";
import { getDatabase, ref, set, get, child } from "https://www.gstatic.com/firebasejs/9.6.11/firebase-database.js";

const setupComments = () => {
  if (!isOnPost) {
    return
  }

  const firebaseConfig = {
    apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    authDomain: "blog-comments-xxxxx.firebaseapp.com",
    databaseURL: "https://blog-comments-xxxxx-default-rtdb.firebaseio.com",
    projectId: "blog-comments-xxxxx",
    storageBucket: "blog-comments-xxxxx.appspot.com",
    messagingSenderId: "xxxxxxxxxxxx",
    appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    measurementId: "G-xxxxxxxxxx"
  };

  // Initialize Firebase
  const app = initializeApp(firebaseConfig);

  // Get a reference to the database service
  const database = getDatabase(app);

  // Page path
  const path = window.location.pathname.toString().split("/").slice(0, -1).join("/")

  String.prototype.hash = function() {
    var hash = 0;
    for (var i = 0; i < this.length; i++) {
      var code = this.charCodeAt(i);
      hash = ((hash << 5) - hash) + code;
      hash = hash & hash; // Convert to 32bit integer
    }
    return Math.abs(hash).toString();
  }

  // DB functions
  async function postComment(name, text) {
    const time = Date.now()
    const fullpath = path.hash() + "-" + path.split("/").slice(-1) + "/" + name.hash() + time
    set(ref(database, fullpath), {
      name,
      text,
      time: time,
    });
  }

  function checkType(obj, type) {
    return obj && typeof (obj) === type
  }

  // Fill with comments
  const fullpath = path.hash() + "-" + path.split("/").slice(-1)
  get(child(ref(database), fullpath)).then((snapshot) => {
    if (snapshot.exists()) {
      const comments = snapshot.val();
      for (const cid of Object.keys(comments)) {
        const comment = comments[cid]
        if (checkType(comment.name, "string") && checkType(comment.text, "string") && checkType(comment.time, "number"))
          addCommentHtml(comment)
      }
    }
    document.getElementById("comments").style.display = "block"
  }).catch((error) => {
    console.error(error);
    document.getElementById("comments").style.display = "block"
  })


  function addCommentHtml(commentObj) {
    document.getElementById("nocomment").style.display = "none"

    var li = document.createElement("li");
    const div = document.createElement("div")
    div.classList.add("comment")

    const name = document.createTextNode(commentObj.name);
    const nameSpan = document.createElement("span")
    nameSpan.append(name)

    const comment = document.createTextNode(commentObj.text);
    const commentp = document.createElement("p")
    commentp.append(comment)

    const utctime = commentObj.time
    const timestamp_text = "in " + new Date(utctime).toLocaleDateString() + " " + new Date(utctime).toLocaleTimeString()
    const time = document.createTextNode(timestamp_text);
    const timestamp = document.createElement("timestamp")
    timestamp.append(time)

    div.appendChild(nameSpan);
    div.appendChild(timestamp);
    div.appendChild(commentp);
    li.appendChild(div)
    document.getElementById("comment-list").prepend(li);
  }

  function processUserCommentAdd() {
    const name = document.getElementById("comment-name");
    const text = document.getElementById("comment-box");
    addCommentHtml({ name: name.value, text: text.value, time: Date.now() })
    postComment(name.value, text.value)
    setCookie('username', name.value);
    name.disabled = true
    text.value = ""
  }

  const form = document.getElementById("new-comment-form");
  form.addEventListener("submit", function(e) {
    e.preventDefault()
    processUserCommentAdd()
  });

  document.body.addEventListener('keydown', function(e) {
    if (!(e.key === 'Enter' && (e.metaKey || e.ctrlKey))) return;
    const target = e.target
    if (target.form) {
      e.preventDefault()
      if (form.checkValidity() === false) {
        form.reportValidity();
        return;
      }
      processUserCommentAdd()
    }
  })
}

setupComments();

2.4. Firebase rules

Basically only allow adding new content inside a /cat_id/post_id/, not updating or deleting allowed. The data must contain the fields name, text and time.

Then some type checking is applied and it is ensured that the timestamp is not from the future or older than 15 seconds.

{
  "rules": {
    "$category": {
        ".write": false,
        ".read": true,
        "$postid": {
          ".read": true,
          ".write": "!data.exists()",
          ".validate": "$category.matches(/^\\d+-\\w+$/)",
          "name": {".validate": "newData.isString() && newData.val().length <= 48 && newData.val().length >= 4"},
          "text": {".validate": "newData.isString() && newData.val().length <= 512 && newData.val().length >= 10"},
          "time": {".validate": "newData.isNumber() && newData.val() <= now && newData.val() >= now - 15000"},
          "$other": {".validate":  false }
        }
      }
   }
}

3. What have I learned/noticed

  • Firebase realtime database doesn't seem to be very configurable regarding spam prevention, ddos, etc, at first sight at least.
  • It has authentication built in, which I didn't want to use.
  • So… there is this weird bug if you leave the browser's developers tools open the firebase client wont authenticate. Is that a bug or some sort very bad "anti hacking" thing they try :P
  • Firestore has more methods while the realtime database is simpler and more suitable for small projects. For example I had to give up on having a proof of work ddos prevention because I can't compute a hash on the realtime database rules. There is a very limited scripting you can do there.

You can check bellow my first tests of the this feature.



terminal

Comments


Be the first to comment!

    ﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏

    Adding a comment box with google firebase

    Author: Matheus Fillipe

    Creation Date: 2022-04-15 Fri 00:00

    Modified: 2022-07-15 Fri 02:40

    View this page on github

    Emacs 28.1 (Org mode 9.5.2)