Code a Chatting Application
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
error | explanation |
---|---|
database error | can't connect to database |
banned | banned and can't send messages |
empty channel | channel parameter empty or missing |
nonexistent channel | this channel doesn't exist |
messaging cooldown | chatting too fast and message wasn't sent. default cooldown is 5 seconds |
duplicate message | this message was previously sent |
empty message | message parameter empty or missing |
message blocked | contains a blacklisted word |
message maxlength | message 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
cool