Code a Chatting Application

in #tutorial8 years ago (edited)

Today I'm going to show you how to code an API that lets you send messages (PHP). Please upvote if you enjoyed reading my tutorial. If people show interest in this project I'll post a frontend tutorial on how to view and send messages via a website.

Demo: https://hexmsg.daltonedwards.me

Database Setup

Before we get started we need to set up our database. Run the SQL command below.

CREATE TABLE IF NOT EXISTS channels (
channel VARCHAR(20) NOT NULL UNIQUE PRIMARY KEY,
messages MEDIUMTEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS senders (
ip VARCHAR(45) NOT NULL UNIQUE PRIMARY KEY,
banned TINYINT(1) NOT NULL,
time INT(10) NOT NULL,
messages MEDIUMTEXT NOT NULL
);

Code Setup

Create a new PHP file and place this code at the top. We use this to access the database. I named my file send.php.

<?php
$servername = "";
$username   = "";
$password   = "";
$dbname     = "";
$dbError    = false;
try {
    $conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch (PDOException $e) {
    $dbError = true;
}

Functions

Below the database code paste this channel_exists function. We use this later on in our code.

function channel_exists($channel)
{
    global $conn;
    $channelQuery = $conn->prepare("SELECT channel FROM channels WHERE channel = :channel LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $channelQuery->execute(array(
        ":channel" => $channel
    ));
    $count = count($channelQuery->fetchAll());
    if ($count > 0) {
        return true;
    } else {
        return false;
    }
}

Header

Below the channel_exists function paste this code. It sets the proper content type for our API, contains variables we use throughout our code, and even defines our swear word blocking mechanism (thanks Stack Overlow).

header("Content-type: application/json;charset=utf-8");
$ip         = $_SERVER["REMOTE_ADDR"];
$channel    = strtolower($_POST["channel"]);
$message    = trim($_POST["message"]);
$time       = time();
$badWords   = array(
    "cuck",
    "libtard"
);
$matches    = array();
$matchFound = preg_match_all("/\b(" . implode($badWords, "|") . ")\b/i", $message, $matches);

Errors

Paste this code below everything else. It's arguably the most important part of our code. It handles all of the validation.

if ($dbError) {
    $error[] = "database error";
}
if ($dbError === false) {
    $getSenderQuery = $conn->prepare("SELECT sender FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $getSenderQuery->execute(array(
        ":ip" => $ip
    ));
    $sender        = $getSenderQuery->fetchColumn();
    $bannedIpQuery = $conn->prepare("SELECT banned FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $bannedIpQuery->execute(array(
        ":ip" => $ip
    ));
    $bannedIpStatus = $bannedIpQuery->fetchColumn();
    if ($bannedIpStatus == 1) {
        $error[] = "banned";
    }
    if (empty($channel)) {
        $error[] = "empty channel";
    }
    if (!empty($channel) && !channel_exists($channel)) {
        $error[] = "nonexistent channel";
    }
    $coolDownQuery = $conn->prepare("SELECT time FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $coolDownQuery->execute(array(
        ":ip" => $ip
    ));
    $lastSent = $coolDownQuery->fetchColumn();
    if ($lastSent + 5 > $time) {
        $error[] = "messaging cooldown";
    }
    $duplicateQuery = $conn->prepare("SELECT messages FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $duplicateQuery->execute(array(
        ":ip" => $ip
    ));
    $latestMessagesJson     = $duplicateQuery->fetchColumn();
    $latestMessages         = json_decode($latestMessagesJson, true);
    $latestMessagesReversed = array_reverse($latestMessages);
    $lastMessage            = $latestMessagesReversed[0]["message"];
    if (strtolower($message) === strtolower($lastMessage)) {
        $error[] = "duplicate message";
    }
    if (empty($message)) {
        $error[] = "empty message";
    }
    if ($matchFound) {
        $error[] = "message blocked";
    }
    if (strlen($message) > 200) {
        $error[] = "message maxlength";
    }
}
if (!isset($error)) {
    $error[] = false;
}

Possible Errors

errorexplanation
database errorcan't connect to database
bannedbanned and can't send messages
empty channelchannel parameter empty or missing
nonexistent channelthis channel doesn't exist
messaging cooldownchatting too fast and message wasn't sent. default cooldown is 5 seconds
duplicate messagethis message was previously sent
empty messagemessage parameter empty or missing
message blockedcontains a blacklisted word
message maxlengthmessage too long. default maxlength is 200 characters

Update Database

Paste this code below everything else. This code updates the database! Super important!

By default it only saves the 100 latest messages in a channel. It also saves 100 user messages in the senders table, so that you can see what users are sending and see if they are behaving.

if ($error[0] === false) {
    $updateTracking = $conn->prepare("INSERT INTO senders (ip, banned, time, messages) VALUES(:ip, :banned, :time, :messages)
    ON DUPLICATE KEY UPDATE time= :time2, messages= :messages2");
    $updateTracking->bindParam(":ip", $ip);
    $banned = 0;
    $updateTracking->bindParam(":banned", $banned);
    $trackingMessagesQuery = $conn->prepare("SELECT messages FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $trackingMessagesQuery->execute(array(
        ":ip" => $ip
    ));
    $trackingMessagesJson   = $trackingMessagesQuery->fetchColumn();
    $trackingMessagesData   = json_decode($trackingMessagesJson);
    $trackingMessagesData[] = array(
        "message" => $message
    );
    if (count($trackingMessagesData) > 100) {
        array_shift($trackingMessagesData);
    }
    $newTrackingMessages = json_encode($trackingMessagesData, JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK);
    $updateTracking->bindParam(":time", $time);
    $updateTracking->bindParam(":messages", $newTrackingMessages);
    $updateTracking->bindParam(":time2", $time);
    $updateTracking->bindParam(":messages2", $newTrackingMessages);
    $updateTracking->execute();
    if ($sender === false) {
        $getSenderQuery = $conn->prepare("SELECT sender FROM senders WHERE ip = :ip LIMIT 1", array(
            PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
        ));
        $getSenderQuery->execute(array(
            ":ip" => $ip
        ));
        $sender = $getSenderQuery->fetchColumn();
    }
    $messagesQuery = $conn->prepare("SELECT messages FROM channels WHERE channel = :channel LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $messagesQuery->execute(array(
        ":channel" => $channel
    ));
    $json   = $messagesQuery->fetchColumn();
    $data   = json_decode($json);
    $data[] = array(
        "sender" => $sender,
        "message" => $message
    );
    if (count($data) > 100) {
        array_shift($data);
    }
    $updateMessages = $conn->prepare("UPDATE channels SET messages = ? WHERE channel = ?");
    $newMessages    = json_encode($data, JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK);
    $updateMessages->execute(array(
        $newMessages,
        $channel
    ));
}

API JSON Output (And Close Database Connection)

Paste this code below everything else!

if (count($error) === 1) {
    echo json_encode(array(
        "error" => $error[0]
    ), JSON_PRETTY_PRINT);
} else {
    echo json_encode(array(
        "error" => $error
    ), JSON_PRETTY_PRINT);
}
$conn = null;

All Code

<?php
$servername = "";
$username   = "";
$password   = "";
$dbname     = "";
$dbError    = false;
try {
    $conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch (PDOException $e) {
    $dbError = true;
}
function channel_exists($channel)
{
    global $conn;
    $channelQuery = $conn->prepare("SELECT channel FROM channels WHERE channel = :channel LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $channelQuery->execute(array(
        ":channel" => $channel
    ));
    $count = count($channelQuery->fetchAll());
    if ($count > 0) {
        return true;
    } else {
        return false;
    }
}
header("Content-type: application/json;charset=utf-8");
$ip         = $_SERVER["HTTP_CF_CONNECTING_IP"];
$channel    = strtolower($_POST["channel"]);
$message    = trim($_POST["message"]);
$time       = time();
$badWords   = array(
    "cuck",
    "libtard"
);
$matches    = array();
$matchFound = preg_match_all("/\b(" . implode($badWords, "|") . ")\b/i", $message, $matches);
if ($dbError) {
    $error[] = "database error";
}
if ($dbError === false) {
    $getSenderQuery = $conn->prepare("SELECT sender FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $getSenderQuery->execute(array(
        ":ip" => $ip
    ));
    $sender        = $getSenderQuery->fetchColumn();
    $bannedIpQuery = $conn->prepare("SELECT banned FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $bannedIpQuery->execute(array(
        ":ip" => $ip
    ));
    $bannedIpStatus = $bannedIpQuery->fetchColumn();
    if ($bannedIpStatus == 1) {
        $error[] = "banned";
    }
    if (empty($channel)) {
        $error[] = "empty channel";
    }
    if (!empty($channel) && !channel_exists($channel)) {
        $error[] = "nonexistent channel";
    }
    $coolDownQuery = $conn->prepare("SELECT time FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $coolDownQuery->execute(array(
        ":ip" => $ip
    ));
    $lastSent = $coolDownQuery->fetchColumn();
    if ($lastSent + 5 > $time) {
        $error[] = "messaging cooldown";
    }
    $duplicateQuery = $conn->prepare("SELECT messages FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $duplicateQuery->execute(array(
        ":ip" => $ip
    ));
    $latestMessagesJson     = $duplicateQuery->fetchColumn();
    $latestMessages         = json_decode($latestMessagesJson, true);
    $latestMessagesReversed = array_reverse($latestMessages);
    $lastMessage            = $latestMessagesReversed[0]["message"];
    if (strtolower($message) === strtolower($lastMessage)) {
        $error[] = "duplicate message";
    }
    if (empty($message)) {
        $error[] = "empty message";
    }
    if ($matchFound) {
        $error[] = "message blocked";
    }
    if (strlen($message) > 200) {
        $error[] = "message maxlength";
    }
}
if (!isset($error)) {
    $error[] = false;
}
if ($error[0] === false) {
    $updateTracking = $conn->prepare("INSERT INTO senders (ip, banned, time, messages) VALUES(:ip, :banned, :time, :messages)
    ON DUPLICATE KEY UPDATE time= :time2, messages= :messages2");
    $updateTracking->bindParam(":ip", $ip);
    $banned = 0;
    $updateTracking->bindParam(":banned", $banned);
    $trackingMessagesQuery = $conn->prepare("SELECT messages FROM senders WHERE ip = :ip LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $trackingMessagesQuery->execute(array(
        ":ip" => $ip
    ));
    $trackingMessagesJson   = $trackingMessagesQuery->fetchColumn();
    $trackingMessagesData   = json_decode($trackingMessagesJson);
    $trackingMessagesData[] = array(
        "message" => $message
    );
    if (count($trackingMessagesData) > 100) {
        array_shift($trackingMessagesData);
    }
    $newTrackingMessages = json_encode($trackingMessagesData, JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK);
    $updateTracking->bindParam(":time", $time);
    $updateTracking->bindParam(":messages", $newTrackingMessages);
    $updateTracking->bindParam(":time2", $time);
    $updateTracking->bindParam(":messages2", $newTrackingMessages);
    $updateTracking->execute();
    if ($sender === false) {
        $getSenderQuery = $conn->prepare("SELECT sender FROM senders WHERE ip = :ip LIMIT 1", array(
            PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
        ));
        $getSenderQuery->execute(array(
            ":ip" => $ip
        ));
        $sender = $getSenderQuery->fetchColumn();
    }
    $messagesQuery = $conn->prepare("SELECT messages FROM channels WHERE channel = :channel LIMIT 1", array(
        PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY
    ));
    $messagesQuery->execute(array(
        ":channel" => $channel
    ));
    $json   = $messagesQuery->fetchColumn();
    $data   = json_decode($json);
    $data[] = array(
        "sender" => $sender,
        "message" => $message
    );
    if (count($data) > 100) {
        array_shift($data);
    }
    $updateMessages = $conn->prepare("UPDATE channels SET messages = ? WHERE channel = ?");
    $newMessages    = json_encode($data, JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK);
    $updateMessages->execute(array(
        $newMessages,
        $channel
    ));
}
if (count($error) === 1) {
    echo json_encode(array(
        "error" => $error[0]
    ), JSON_PRETTY_PRINT);
} else {
    echo json_encode(array(
        "error" => $error
    ), JSON_PRETTY_PRINT);
}
$conn = null;
?>

Wrapping Up

To create a channel just add a new item to your database. Enter the channel name and leave the messages column blank.

To send messages POST to the API like channel=whatever&message=hi