Andrew hai 11 horas
pai
achega
6f80af8f16

+ 1 - 0
.idea/sqldialects.xml

@@ -2,6 +2,7 @@
 <project version="4">
   <component name="SqlDialectMappings">
     <file url="file://$PROJECT_DIR$/app/Controllers/VideoController.php" dialect="GenericSQL" />
+    <file url="file://$PROJECT_DIR$/app/Types/DatabaseObjects/ContactTracker.php" dialect="GenericSQL" />
     <file url="file://$PROJECT_DIR$/app/Types/DatabaseObjects/Follow.php" dialect="GenericSQL" />
   </component>
 </project>

+ 477 - 450
app/Controllers/AccountController.php

@@ -21,447 +21,474 @@ use Pecee\SimpleRouter\SimpleRouter;
 
 class AccountController implements IRouteController
 {
-	public static function getToken(): string
-	{
-		$username = input("username");
-		$password = password_hash(input("password"), PASSWORD_DEFAULT);
-
-		$account = new Account(
-			username: $username,
-			password: $password,
-			picture_hash: "default",
-			verified: false
-		);
-
-		if ($account->Exists()) { // Account already exists
-			$account->Load();
-			if (password_verify($password, $account->password)) throw new UnauthenticatedException($account->id, 401);
-			$session = new Session(account_id: $account->id);
-		} else { // Create a new account
-			$session = new Session(account_id: $account->Save());
-		}
-
-		CORSHelper();
-		$token = $session->Save();
-		$session->Load();
-		return api_json([
-			"token" => $token,
-			"auth_date" => $session->date_authenticated
-		]);
-	}
-
-	public static function getAccount($id): string
-	{
-		if (!signed_in(request())) {
-			$usernames = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeUsernames"));
-
-			CORSHelper();
-			return api_json([
-				"id" => $id,
-				"password" => "password1234",
-				"username" => $usernames[rand(0, count($usernames) - 1)],
-				"verified" => rand(1, 10) > 8,
-				"bio" => "SIGN IN to see this EPIC content!!!",
-				"followers" => rand(5, 38203),
-				"following" => rand(5, 6243),
-				"myself" => false,
-				"links" => [],
-				"socialCredit" => rand(-20223, 20223),
+    public static function getToken(): string
+    {
+        $username = input("username");
+        $password = password_hash(input("password"), PASSWORD_DEFAULT);
+
+        $account = new Account(
+            username: $username,
+            password: $password,
+            picture_hash: "default",
+            verified: false
+        );
+
+        $client_ip = get_client_ip();
+        if ($account->Exists()) { // Account already exists
+            $account->Load();
+            if (password_verify($password, $account->password)) throw new UnauthenticatedException($account->id, 401);
+            $session = new Session(account_id: $account->id, ip: $client_ip);
+
+            Logger::Info("Logged in account ID $account->id with the IP address $client_ip.");
+        } else { // Create a new account
+            $account_id = $account->Save();
+            $session = new Session(account_id: $account_id, ip: $client_ip);
+            Logger::Info("Created & logged in account ID $account_id with the IP address $client_ip.");
+        }
+
+        CORSHelper();
+        $token = $session->Save();
+        $session->Load();
+        return api_json([
+            "token" => $token,
+            "auth_date" => $session->date_authenticated
+        ]);
+    }
+
+    public static function getAccount($id): string
+    {
+        $client_ip = get_client_ip();
+        Logger::Info("Getting account ID $id information for the IP address $client_ip.");
+
+        if (!signed_in(request())) {
+            $usernames = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeUsernames"));
+
+            CORSHelper();
+            return api_json([
+                "id" => $id,
+                "password" => "password1234",
+                "username" => $usernames[rand(0, count($usernames) - 1)],
+                "verified" => rand(1, 10) > 8,
+                "bio" => "SIGN IN to see this EPIC content!!!",
+                "followers" => rand(5, 38203),
+                "following" => rand(5, 6243),
+                "myself" => false,
+                "links" => [],
+                "socialCredit" => rand(-20223, 20223),
                 "followed" => false
-			]);
-		}
+            ]);
+        }
 
-		if ($id === "myself") $id = get_token_id(request());
-		Logger::Debug($id);
-		Logger::Debug("Getting account id ($id).");
+        if ($id === "myself") $id = get_token_id(request());
+        Logger::Debug("Getting account id ($id).");
 
-		$account = new Account(id: $id);
-		$account->Load();
+        $account = new Account(id: $id);
+        $account->Load();
 
-		$followers = count(new Follow(followee_id: $id)->LoadMany());
-		$following = count(new Follow(follower_id: $id)->LoadMany());
+        $followers = count(new Follow(followee_id: $id)->LoadMany());
+        $following = count(new Follow(follower_id: $id)->LoadMany());
 
-		try {
-			$links = new Link(account_id: $id)->LoadMany();
-		} catch (Exception $e) {
-			$links = [];
-		}
+        try {
+            $links = new Link(account_id: $id)->LoadMany();
+        } catch (Exception $e) {
+            $links = [];
+        }
 
         $followed = new Follow(followee_id: $id, follower_id: get_token_id(request()))->Exists();
 
-		CORSHelper();
-
-		return api_json([
-			"id" => $id,
-			"username" => $account->username,
-			"verified" => $account->verified,
-			"bio" => $account->bio,
-			"followers" => $followers,
-			"following" => $following,
-			"myself" => $id == get_token_id(request()),
-			"links" => $links,
-			"socialCredit" => $account->social_credit,
+        CORSHelper();
+
+        return api_json([
+            "id" => $id,
+            "username" => $account->username,
+            "verified" => $account->verified,
+            "bio" => $account->bio,
+            "followers" => $followers,
+            "following" => $following,
+            "myself" => $id == get_token_id(request()),
+            "links" => $links,
+            "socialCredit" => $account->social_credit,
             "followed" => $followed
-		]);
-	}
-
-	public static function getVideos($id): string
-	{
-		if(!signed_in(request())) {
-			$titles = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeTitles"));
-			$descriptions = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeDescriptions"));
-			$usernames = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeUsernames"));
-
-			$feed = [];
-
-			for($i = 0; $i < rand(6, 20); $i++) {
-				$feed[] = [
-					"id" => rand(1, 10),
-					"title" => $titles[rand(0, count($titles) - 1)],
-					"description" => $descriptions[rand(0, count($descriptions) - 1)],
-					"likes" => rand(20000, 37020),
-					"dislikes" =>  rand(2, 12343),
-					"comments" => rand(2, 1029),
-					"shares" => rand(2, 200000),
-					"author" => [
-						"id" => rand(2, 59),
-						"verified" => rand(1, 10) > 8,
-						"username" => $usernames[rand(0, count($usernames) - 1)],
-					],
-				];
-			}
-
-			CORSHelper();
-			return api_json($feed);
-		}
-		if ($id === "myself") $id = get_token_id(request());
-		$account = new Account(id: $id);
-		$video_number = intval(input("video") ?? "0");
-		$data = Hajeebtok::$Database->Query("SELECT * FROM videos WHERE author_id = :author_id", ["author_id" => $account->id]);
-		if (empty($data)) throw new VideoNotFoundException(0, 404);
-
-		$videos = [];
-		$valid_video = false;
-		foreach ($data as $video) {
-			if($video_number === 0) $valid_video = true;
-			if($video["id"] === $video_number) $valid_video = true;
-			if(!$valid_video) continue;
-
-			$account = new Account($video["author_id"]);
-			$account->Load();
-
-			$videos[] = ["id" => $video["id"],
-				"title" => $video["title"],
-				"description" => $video["description"],
-				"likes" => $video["likes"],
-				"dislikes" => $video["dislikes"],
-				"comments" => Hajeebtok::$Database->Single("SELECT COUNT(*) FROM comments WHERE video_id = :id", ["id" => $video["id"]]),
-				"shares" => Hajeebtok::$Database->Single("SELECT COUNT(*) FROM messages INNER JOIN videos ON messages.video_id = videos.id WHERE videos.id = :id", ["id" => $video["id"]]),
-				"author" => [
-					"id" => $video["author_id"],
-					"verified" => $account->verified,
-					"username" => $account->username,
-				],
-			];
-		}
-
-		CORSHelper();
-		return api_json($videos);
-	}
-
-	public static function getPicture($id): string
-	{
-		$signed_in = signed_in(request());
-		if ($id === "myself") $id = get_token_id(request());
-
-		if ($signed_in) {
-			try {
-				$account = new Account(id: $id);
-				$account->Load();
-
-				$picture_path = APP_ROOT . "/usercontent/pictures/$account->picture_hash.png";
-			} catch (Exception $e) {
-				$picture_path = APP_ROOT . "/usercontent/pictures/not_found.png";
-			}
-		} else {
-			// this is hardcoded because i dont care
-			$picture_path = APP_ROOT . "/usercontent/pictures/premium_" . rand(1, 57) . ".png";
-		}
-
-		$mimeTypes = new MimeTypes();
-
-		$picture_contents = file_get_contents($picture_path);
-		$picture_size = filesize($picture_path);
-
-		$mime = $mimeTypes->getMimeType(pathinfo($picture_path, PATHINFO_EXTENSION));
-
-		$response = response();
-		$response->header("Content-Type: $mime");
-		$response->header("Content-Length: $picture_size");
-		$response->header("Cache-Control: max-age=1800, public");
-
-		return $picture_contents;
-	}
-
-	public static function getAvailablePremiumProfilePictures(): string
-	{
-		$pictures = [];
-		for ($i = 1; $i <= 47; $i++) {
-			$pictures[] = $i;
-		}
-
-		CORSHelper();
-		return api_json($pictures);
-	}
-
-	public static function getPremiumProfilePicture($id): string
-	{
-		$picture_path = APP_ROOT . "/usercontent/pictures/premium_$id.png";
-
-		$mime_types = new MimeTypes();
-
-		$picture_contents = file_get_contents($picture_path);
-		$picture_size = filesize($picture_path);
-
-		$mime = $mime_types->getMimeType(pathinfo($picture_path, PATHINFO_EXTENSION));
-
-		$response = response();
-		$response->header("Content-Type: $mime");
-		$response->header("Content-Length: $picture_size");
-		$response->header("Cache-Control: max-age=3600, public");
-
-		return $picture_contents;
-	}
-
-	public static function updateAccount(): string
-	{
-		if (!signed_in(request())) throw new UnauthenticatedException(0, 401);
-		$id = get_token_id(request());
-
-		$picture_hash = input("picture_hash");
-		$picture = request()->getInputHandler()->file("picture");
-		$bio = input("bio");
-
-		$valid_picture_hash_list = [
-			"default",
-			"premium_1",
-			"premium_2",
-			"premium_3",
-			"premium_4",
-			"premium_5",
-			"premium_6",
-			"premium_7",
-			"premium_8",
-			"premium_9",
-			"premium_10",
-			"premium_11",
-			"premium_12",
-			"premium_13",
-			"premium_14",
-			"premium_15",
-			"premium_16",
-			"premium_17",
-			"premium_18",
-			"premium_19",
-			"premium_20",
-			"premium_21",
-			"premium_22",
-			"premium_23",
-			"premium_24",
-			"premium_25",
-			"premium_26",
-			"premium_27",
-			"premium_28",
-			"premium_29",
-			"premium_30",
-			"premium_31",
-			"premium_32",
-			"premium_33",
-			"premium_34",
-			"premium_35",
-			"premium_36",
-			"premium_37",
-			"premium_38",
-			"premium_39",
-			"premium_40",
-			"premium_41",
-			"premium_42",
-			"premium_43",
-			"premium_44",
-			"premium_45",
-			"premium_46",
-			"premium_47",
-			"premium_48",
-			"premium_49",
-			"premium_50",
-			"premium_51",
-			"premium_52",
-			"premium_53",
-			"premium_54",
-			"premium_55",
-			"premium_56",
-			"premium_57"
-		];
-		if (empty($picture) && !empty($picture_hash)) {
-			if (!in_array($picture_hash, $valid_picture_hash_list)) throw new SecurityFaultException("Attempt to path trace on /update endpoint.", 400);
-		} else if (!empty($picture) && empty($picture_hash)) {
-			$picture_hash = hash("sha256", $picture);
-			$picture_path = APP_ROOT . "/usercontent/pictures/$picture_hash.png";
-
-			$size = getimagesize($picture);
-			$crop = min($size[0], $size[1]);
-			$image_contents = file_get_contents($picture);
-			$image_string = imagecreatefromstring($image_contents);
-			$cropped_image = imagecrop($image_string, [
-				"x" => 0,
-				"y" => 0,
-				"width" => $crop,
-				"height" => $crop
-			]);
-			imagepng($cropped_image, $picture_path); // save image and crop and turn into png (i love php)
-		} else if (!empty($picture) && !empty($picture_hash)) {
-			throw new InvalidRequestException(400);
-		}
-
-		$old_account = new Account(id: $id);
-		$old_account->Load();
-		$new_account = new Account(
-			id: $id,
-			username: $old_account->username,
-			password: $old_account->password,
-			picture_hash: $picture_hash ?? $old_account->picture_hash,
-			verified: $old_account->verified,
-			bio: $bio ?? $old_account->bio,
+        ]);
+    }
+
+    public static function getVideos($id): string
+    {
+        $client_ip = get_client_ip();
+        Logger::Info("Getting videos of the account ID $id for the IP address $client_ip.");
+
+        if (!signed_in(request())) {
+            $titles = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeTitles"));
+            $descriptions = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeDescriptions"));
+            $usernames = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeUsernames"));
+
+            $feed = [];
+
+            for ($i = 0; $i < rand(6, 20); $i++) {
+                $feed[] = [
+                    "id" => rand(1, 10),
+                    "title" => $titles[rand(0, count($titles) - 1)],
+                    "description" => $descriptions[rand(0, count($descriptions) - 1)],
+                    "likes" => rand(20000, 37020),
+                    "dislikes" => rand(2, 12343),
+                    "comments" => rand(2, 1029),
+                    "shares" => rand(2, 200000),
+                    "author" => [
+                        "id" => rand(2, 59),
+                        "verified" => rand(1, 10) > 8,
+                        "username" => $usernames[rand(0, count($usernames) - 1)],
+                    ],
+                ];
+            }
+
+            CORSHelper();
+            return api_json($feed);
+        }
+        if ($id === "myself") $id = get_token_id(request());
+        $account = new Account(id: $id);
+        $video_number = intval(input("video") ?? "0");
+        $data = Hajeebtok::$Database->Query("SELECT * FROM videos WHERE author_id = :author_id", ["author_id" => $account->id]);
+        if (empty($data)) throw new VideoNotFoundException(0, 404);
+
+        $videos = [];
+        $valid_video = false;
+        foreach ($data as $video) {
+            if ($video_number === 0) $valid_video = true;
+            if ($video["id"] === $video_number) $valid_video = true;
+            if (!$valid_video) continue;
+
+            $account = new Account($video["author_id"]);
+            $account->Load();
+
+            $videos[] = ["id" => $video["id"],
+                "title" => $video["title"],
+                "description" => $video["description"],
+                "likes" => $video["likes"],
+                "dislikes" => $video["dislikes"],
+                "comments" => Hajeebtok::$Database->Single("SELECT COUNT(*) FROM comments WHERE video_id = :id", ["id" => $video["id"]]),
+                "shares" => Hajeebtok::$Database->Single("SELECT COUNT(*) FROM messages INNER JOIN videos ON messages.video_id = videos.id WHERE videos.id = :id", ["id" => $video["id"]]),
+                "author" => [
+                    "id" => $video["author_id"],
+                    "verified" => $account->verified,
+                    "username" => $account->username,
+                ],
+            ];
+        }
+
+        CORSHelper();
+        return api_json($videos);
+    }
+
+    public static function getPicture($id): string
+    {
+        $signed_in = signed_in(request());
+        if ($id === "myself") $id = get_token_id(request());
+
+        if ($signed_in) {
+            try {
+                $account = new Account(id: $id);
+                $account->Load();
+
+                $picture_path = APP_ROOT . "/usercontent/pictures/$account->picture_hash.png";
+            } catch (Exception $e) {
+                $picture_path = APP_ROOT . "/usercontent/pictures/not_found.png";
+            }
+        } else {
+            // this is hardcoded because i dont care
+            $picture_path = APP_ROOT . "/usercontent/pictures/premium_" . rand(1, 57) . ".png";
+        }
+
+        $mimeTypes = new MimeTypes();
+
+        $picture_contents = file_get_contents($picture_path);
+        $picture_size = filesize($picture_path);
+
+        $mime = $mimeTypes->getMimeType(pathinfo($picture_path, PATHINFO_EXTENSION));
+
+        $response = response();
+        $response->header("Content-Type: $mime");
+        $response->header("Content-Length: $picture_size");
+        $response->header("Cache-Control: max-age=1800, public");
+
+        return $picture_contents;
+    }
+
+    public static function getAvailablePremiumProfilePictures(): string
+    {
+        $pictures = [];
+        for ($i = 1; $i <= 47; $i++) {
+            $pictures[] = $i;
+        }
+
+        CORSHelper();
+        return api_json($pictures);
+    }
+
+    public static function getPremiumProfilePicture($id): string
+    {
+        $picture_path = APP_ROOT . "/usercontent/pictures/premium_$id.png";
+
+        $mime_types = new MimeTypes();
+
+        $picture_contents = file_get_contents($picture_path);
+        $picture_size = filesize($picture_path);
+
+        $mime = $mime_types->getMimeType(pathinfo($picture_path, PATHINFO_EXTENSION));
+
+        $response = response();
+        $response->header("Content-Type: $mime");
+        $response->header("Content-Length: $picture_size");
+        $response->header("Cache-Control: max-age=3600, public");
+
+        return $picture_contents;
+    }
+
+    public static function updateAccount(): string
+    {
+        if (!signed_in(request())) throw new UnauthenticatedException(0, 401);
+        $id = get_token_id(request());
+        $client_ip = get_client_ip();
+
+        Logger::Info("Updating account id $id with the IP address $client_ip.");
+
+        $picture_hash = input("picture_hash");
+        $picture = request()->getInputHandler()->file("picture");
+        $bio = input("bio");
+
+        $valid_picture_hash_list = [
+            "default",
+            "premium_1",
+            "premium_2",
+            "premium_3",
+            "premium_4",
+            "premium_5",
+            "premium_6",
+            "premium_7",
+            "premium_8",
+            "premium_9",
+            "premium_10",
+            "premium_11",
+            "premium_12",
+            "premium_13",
+            "premium_14",
+            "premium_15",
+            "premium_16",
+            "premium_17",
+            "premium_18",
+            "premium_19",
+            "premium_20",
+            "premium_21",
+            "premium_22",
+            "premium_23",
+            "premium_24",
+            "premium_25",
+            "premium_26",
+            "premium_27",
+            "premium_28",
+            "premium_29",
+            "premium_30",
+            "premium_31",
+            "premium_32",
+            "premium_33",
+            "premium_34",
+            "premium_35",
+            "premium_36",
+            "premium_37",
+            "premium_38",
+            "premium_39",
+            "premium_40",
+            "premium_41",
+            "premium_42",
+            "premium_43",
+            "premium_44",
+            "premium_45",
+            "premium_46",
+            "premium_47",
+            "premium_48",
+            "premium_49",
+            "premium_50",
+            "premium_51",
+            "premium_52",
+            "premium_53",
+            "premium_54",
+            "premium_55",
+            "premium_56",
+            "premium_57"
+        ];
+        if (empty($picture) && !empty($picture_hash)) {
+            if (!in_array($picture_hash, $valid_picture_hash_list)) throw new SecurityFaultException("Attempt to path trace on /update endpoint.", 400);
+        } else if (!empty($picture) && empty($picture_hash)) {
+            $picture_hash = hash("sha256", $picture);
+            $picture_path = APP_ROOT . "/usercontent/pictures/$picture_hash.png";
+
+            $size = getimagesize($picture);
+            $crop = min($size[0], $size[1]);
+            $image_contents = file_get_contents($picture);
+            $image_string = imagecreatefromstring($image_contents);
+            $cropped_image = imagecrop($image_string, [
+                "x" => 0,
+                "y" => 0,
+                "width" => $crop,
+                "height" => $crop
+            ]);
+            imagepng($cropped_image, $picture_path); // save image and crop and turn into png (i love php)
+        } else if (!empty($picture) && !empty($picture_hash)) {
+            throw new InvalidRequestException(400);
+        }
+
+        $old_account = new Account(id: $id);
+        $old_account->Load();
+        $new_account = new Account(
+            id: $id,
+            username: $old_account->username,
+            password: $old_account->password,
+            picture_hash: $picture_hash ?? $old_account->picture_hash,
+            verified: $old_account->verified,
+            bio: $bio ?? $old_account->bio,
             social_credit: $old_account->social_credit,
-		);
-
-		CORSHelper();
-		$new_account->Update();
-		return api_json([
-			"id" => $new_account->id,
-			"username" => $new_account->username,
-			"pictureHash" => in_array($new_account->picture_hash, $valid_picture_hash_list) ? $new_account->picture_hash : null,
-			"bio" => $new_account->bio,
-			"verified" => $new_account->verified,
+        );
+
+        CORSHelper();
+        $new_account->Update();
+        return api_json([
+            "id" => $new_account->id,
+            "username" => $new_account->username,
+            "pictureHash" => in_array($new_account->picture_hash, $valid_picture_hash_list) ? $new_account->picture_hash : null,
+            "bio" => $new_account->bio,
+            "verified" => $new_account->verified,
             "social_credit" => $old_account->social_credit,
-		]);
-	}
-
-	public static function addLink(): string
-	{
-		if (!signed_in(request())) throw new UnauthenticatedException(0, 401);
-		$account_id = get_token_id(request());
-		$link = input("link");
-		$link_type = input("linkType");
-
-		$link_enum_type = LinkEnum::tryFrom($link_type);
-		// todo: add enum type filtering
-
-		$link = new Link(account_id: $account_id, type: $link_enum_type, url: $link);
-		$link->Save();
-		return api_json([
-			"id" => $link->id,
-			"url" => $link->url,
-			"type" => $link->type->value,
-			"account_id" => $link->account_id
-		]);
-	}
-
-	public static function getFollowers($id): string
-	{
-		if ($id === "myself") $id = get_token_id(request());
-		$followers = new Follow(followee_id: $id)->LoadMany();
-		if (empty($followers)) throw new FollowNotFoundException(0, 404);
-		for ($i = 0; $i < count($followers); $i++) {
-			$follower = $followers[$i]["follower_id"];
-			$account = new Account(id: $follower);
-			$account->Load();
-
-			$followers[$i] = [
-				"followee_id" => (int)$id,
-				"follower_id" => $follower,
-				"username" => $account->username,
-				"verified" => $account->verified
-			];
-		}
-		return api_json($followers);
-	}
-
-	public static function getFollowing($id): string
-	{
-		if ($id === "myself") $id = get_token_id(request());
-		$following = new Follow(follower_id: $id)->LoadMany();
-		if (empty($following)) throw new FollowNotFoundException(0, 404);
-		for ($i = 0; $i < count($following); $i++) {
-			$followee = $following[$i]["followee_id"];
-			$account = new Account(id: $followee);
-			$account->Load();
-
-			$following[$i] = [
-				"followee_id" => $followee,
-				"follower_id" => (int)$id,
-				"username" => $account->username,
-				"verified" => $account->verified
-			];
-		}
-		return api_json($following);
-	}
-
-	public static function search(): string {
-		$signed_in = signed_in(request());
-		if(!$signed_in) {
-			$data = [];
-			$usernames = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeUsernames"));
-
-			for($i = 0; $i < rand(1, 13); $i++) {
-				$data[] = [
-					"id" => rand(2, 10232),
-					"username" => $usernames[rand(0, count($usernames) - 1)],
-					"socialCredit" => rand(-2032, 1209312),
-					"following" => rand(0, 10232),
-					"followers" => rand(0, 10232),
-					"bio" => "SIGN IN to see this EPIC content!!",
-					"verified" => rand(1, 10) > 8,
-				];
-			}
-
-			CORSHelper();
-			return api_json($data);
-		}
-
-		$query = input("query");
-		$account = new Account(username: $query);
-		$data = $account->LoadMany();
-
-		$accounts = [];
-		foreach($data as $account) {
-			$followers = count(new Follow(followee_id: $account["id"])->LoadMany());
-			$following = count(new Follow(follower_id: $account["id"])->LoadMany());
-
-			$accounts[] = [
-				"id" => $account["id"],
-				"username" => $account["username"],
-				"socialCredit" => $account["social_credit"],
-				"following" => $following,
-				"followers" => $followers,
-				"bio" => $account["bio"],
-				"verified" => $account["verified"] === 1,
-			];
-		}
-
-		CORSHelper();
-		return api_json($accounts);
-	}
-
-    public static function follow($id): string {
+        ]);
+    }
+
+    public static function addLink(): string
+    {
+        if (!signed_in(request())) throw new UnauthenticatedException(0, 401);
+        $account_id = get_token_id(request());
+        $client_ip = get_client_ip();
+
+        $link = input("link");
+        $link_type = input("linkType");
+        Logger::Info("Adding link ($link) for the account Id $account_id with the IP address $client_ip.");
+
+        $link_enum_type = LinkEnum::tryFrom($link_type);
+        // todo: add enum type filtering
+
+        $link = new Link(account_id: $account_id, type: $link_enum_type, url: $link);
+        $link->Save();
+        return api_json([
+            "id" => $link->id,
+            "url" => $link->url,
+            "type" => $link->type->value,
+            "account_id" => $link->account_id
+        ]);
+    }
+
+    // function preserved just in case i need it
+    public static function getFollowers($id): string
+    {
+        if ($id === "myself") $id = get_token_id(request());
+        $followers = new Follow(followee_id: $id)->LoadMany();
+        if (empty($followers)) throw new FollowNotFoundException(0, 404);
+        for ($i = 0; $i < count($followers); $i++) {
+            $follower = $followers[$i]["follower_id"];
+            $account = new Account(id: $follower);
+            $account->Load();
+
+            $followers[$i] = [
+                "followee_id" => (int)$id,
+                "follower_id" => $follower,
+                "username" => $account->username,
+                "verified" => $account->verified
+            ];
+        }
+        return api_json($followers);
+    }
+
+    // function preserved just in case i need it
+    public static function getFollowing($id): string
+    {
+        if ($id === "myself") $id = get_token_id(request());
+        $following = new Follow(follower_id: $id)->LoadMany();
+        if (empty($following)) throw new FollowNotFoundException(0, 404);
+        for ($i = 0; $i < count($following); $i++) {
+            $followee = $following[$i]["followee_id"];
+            $account = new Account(id: $followee);
+            $account->Load();
+
+            $following[$i] = [
+                "followee_id" => $followee,
+                "follower_id" => (int)$id,
+                "username" => $account->username,
+                "verified" => $account->verified
+            ];
+        }
+        return api_json($following);
+    }
+
+    public static function search(): string
+    {
         $signed_in = signed_in(request());
-        if(!$signed_in) throw new UnauthenticatedException(0, 401);
+        $client_ip = get_client_ip();
+        $query = input("query");
+        Logger::Info("Searching accounts with query ($query) for IP address $client_ip.");
+
+
+        if (!$signed_in) {
+            $data = [];
+            $usernames = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeUsernames"));
+
+            for ($i = 0; $i < rand(1, 13); $i++) {
+                $data[] = [
+                    "id" => rand(2, 10232),
+                    "username" => $usernames[rand(0, count($usernames) - 1)],
+                    "socialCredit" => rand(-2032, 1209312),
+                    "following" => rand(0, 10232),
+                    "followers" => rand(0, 10232),
+                    "bio" => "SIGN IN to see this EPIC content!!",
+                    "verified" => rand(1, 10) > 8,
+                ];
+            }
+
+            CORSHelper();
+            return api_json($data);
+        }
+
+        $account = new Account(username: $query);
+        $data = $account->LoadMany();
+
+        $accounts = [];
+        foreach ($data as $account) {
+            $followers = count(new Follow(followee_id: $account["id"])->LoadMany());
+            $following = count(new Follow(follower_id: $account["id"])->LoadMany());
+
+            $accounts[] = [
+                "id" => $account["id"],
+                "username" => $account["username"],
+                "socialCredit" => $account["social_credit"],
+                "following" => $following,
+                "followers" => $followers,
+                "bio" => $account["bio"],
+                "verified" => $account["verified"] === 1,
+            ];
+        }
+
+        CORSHelper();
+        return api_json($accounts);
+    }
+
+    public static function follow($id): string
+    {
+        $signed_in = signed_in(request());
+        if (!$signed_in) throw new UnauthenticatedException(0, 401);
+
+        $client_ip = get_client_ip();
         $follower_id = get_token_id(request());
+        Logger::Info("Following account Id $id for account Id $follower_id with the IP address $client_ip.");
 
         $message = "Successful";
         $successful = true;
         $follow = new Follow(follower_id: $follower_id, followee_id: $id);
-        if($follow->Exists()) {
+        if ($follow->Exists()) {
             $successful = false;
             $message = "You already follow this account...";
         } else {
@@ -475,31 +502,31 @@ class AccountController implements IRouteController
         ]);
     }
 
-	public static function RegisterRoutes(): void
-	{
-		SimpleRouter::group([
-			"prefix" => "/account/",
-		], function () {
-			SimpleRouter::get("/availablePremiumProfilePictures", [AccountController::class, "getAvailablePremiumProfilePictures"]);
-			SimpleRouter::get("/getPremiumProfilePicture/{id}", [AccountController::class, "getPremiumProfilePicture"]);
-			SimpleRouter::get("/{id}/get", [AccountController::class, "getAccount"]);
-			SimpleRouter::get("/{id}/videos", [AccountController::class, "getVideos"]);
-			SimpleRouter::get("/{id}/picture", [AccountController::class, "getPicture"]);
-			//SimpleRouter::get("/{id}/followers", [AccountController::class, "getFollowers"]);
-			//SimpleRouter::get("/{id}/following", [AccountController::class, "getFollowing"]);
+    public static function RegisterRoutes(): void
+    {
+        SimpleRouter::group([
+            "prefix" => "/account/",
+        ], function () {
+            SimpleRouter::get("/availablePremiumProfilePictures", [AccountController::class, "getAvailablePremiumProfilePictures"]);
+            SimpleRouter::get("/getPremiumProfilePicture/{id}", [AccountController::class, "getPremiumProfilePicture"]);
+            SimpleRouter::get("/{id}/get", [AccountController::class, "getAccount"]);
+            SimpleRouter::get("/{id}/videos", [AccountController::class, "getVideos"]);
+            SimpleRouter::get("/{id}/picture", [AccountController::class, "getPicture"]);
+            //SimpleRouter::get("/{id}/followers", [AccountController::class, "getFollowers"]);
+            //SimpleRouter::get("/{id}/following", [AccountController::class, "getFollowing"]);
             SimpleRouter::post("/{id}/follow", [AccountController::class, "follow"]);
-			SimpleRouter::post("/token", [AccountController::class, "getToken"]);
-			SimpleRouter::post("/update", [AccountController::class, "updateAccount"]);
-			SimpleRouter::post("/addLink", [AccountController::class, "addLink"]);
-			SimpleRouter::post("/search", [AccountController::class, "search"]);
-			SimpleRouter::options("/update", "CORSHelper");
-			SimpleRouter::options("/token", "CORSHelper");
-			SimpleRouter::options("/{id}/get", "CORSHelper");
-			SimpleRouter::options("/{id}/videos", "CORSHelper");
-			SimpleRouter::options("/update", "CORSHelper");
-			SimpleRouter::options("/availablePremiumProfilePictures", "CORSHelper");
-			SimpleRouter::options("/getPremiumProfilePicture/{id}","CORSHelper");
-			SimpleRouter::options("/search", "CORSHelper");
-		});
-	}
+            SimpleRouter::post("/token", [AccountController::class, "getToken"]);
+            SimpleRouter::post("/update", [AccountController::class, "updateAccount"]);
+            SimpleRouter::post("/addLink", [AccountController::class, "addLink"]);
+            SimpleRouter::post("/search", [AccountController::class, "search"]);
+            SimpleRouter::options("/update", "CORSHelper");
+            SimpleRouter::options("/token", "CORSHelper");
+            SimpleRouter::options("/{id}/get", "CORSHelper");
+            SimpleRouter::options("/{id}/videos", "CORSHelper");
+            SimpleRouter::options("/update", "CORSHelper");
+            SimpleRouter::options("/availablePremiumProfilePictures", "CORSHelper");
+            SimpleRouter::options("/getPremiumProfilePicture/{id}", "CORSHelper");
+            SimpleRouter::options("/search", "CORSHelper");
+        });
+    }
 }

+ 28 - 3
app/Controllers/HomeController.php

@@ -2,7 +2,10 @@
 
 namespace app\Controllers;
 
+use app\Exceptions\InvalidRequestException;
 use app\Exceptions\UnauthenticatedException;
+use app\Logger;
+use app\Types\DatabaseObjects\ContactTracker;
 use app\Types\WebhookMessage;
 use Pecee\SimpleRouter\SimpleRouter;
 use app\Interfaces\IRouteController;
@@ -16,13 +19,31 @@ class HomeController implements IRouteController
 	}
 
     public static function contact(): string {
-        $signed_in = signed_in(request());
-        if(!$signed_in) throw new UnauthenticatedException(0, 401);
         $message = input("message");
         $email = input("email");
         $name = input("name");
 
-        $webhook = new WebhookMessage();
+        // sanity checks
+        if(empty($message) || empty($email) || empty($name)) throw new InvalidRequestException(400);
+        if(strlen($message) > 2000 || strlen($email) > 100 || strlen($name) > 100) throw new InvalidRequestException(400);
+
+        $client_ip = get_client_ip();
+        $contact = new ContactTracker(ip: $client_ip);
+        if($contact->Exists()) {
+            $contact->Load();
+            Logger::Debug(time());
+            Logger::Debug($contact->date_sent);
+
+            Logger::Debug(strval(time() - $contact->date_sent));
+            if(time() - $contact->date_sent < Hajeebtok::$Config->GetByDotKey("Instance.ContactTimeout")) {
+                throw new InvalidRequestException(429); // didnt wait atleast 1 hour :-1:
+            }
+        }
+
+        $contact = new ContactTracker(name: $name, email: $email, message: $message, ip: $client_ip);
+        $contact->Save();
+
+        $webhook = new WebhookMessage(url: Hajeebtok::$Config->GetByDotKey("Instance.PrivateDiscordWebhookURL"));
         $webhook->SetContent("this is ground control to major tom");
         $webhook->AddEmbed([
             "description" => $message,
@@ -31,6 +52,9 @@ class HomeController implements IRouteController
             "footer" => [
                 "text" => $email,
             ],
+            "author" => [
+                "name" => $client_ip,
+            ],
             "color" => 16711680 // red
         ]);
         $webhook->Send();
@@ -49,6 +73,7 @@ class HomeController implements IRouteController
 		], function () {
 			SimpleRouter::get("/", [HomeController::class, "redirect"]);
             SimpleRouter::post("/contact", [HomeController::class, "contact"]);
+            SimpleRouter::options("/contact", "CORSHelper");
 		});
 	}
 }

+ 30 - 5
app/Controllers/VideoController.php

@@ -10,6 +10,7 @@ use app\Exceptions\UnauthenticatedException;
 use app\Exceptions\VideoNotFoundException;
 use app\Hajeebtok;
 use app\Types\DatabaseObjects\View;
+use app\Types\DatabaseObjects\ViewTracker;
 use Cassandra\Exception\UnauthorizedException;
 use Exception;
 use app\Exceptions\SecurityFaultException;
@@ -31,6 +32,19 @@ class VideoController implements IRouteController
 			$video_information = new Video($id);
 			$video_information->Load();
 			$video_path = APP_ROOT . "/usercontent/videos/$id/video.mp4";
+
+            $client_ip = get_client_ip();
+            $view = new ViewTracker(ip: $client_ip); // view tracking is ip based for now.
+            if($view->Exists()) {
+                $view->Load(); // load last view
+                if(time() - $view->tracking_start > Hajeebtok::$Config->GetByDotKey("Service.ViewDuration")) { // X seconds passed since last video get request
+                    $view = new View(video_id: $id, account_id: get_token_id(request()));
+                    $view->Save();
+                }
+            }
+
+            $view = new ViewTracker(ip: $client_ip, video_id: $id, account_id: get_token_id(request()));
+            $view->Save();
 		}
 		else { // not signed in
 			$rand = rand(1, 47);
@@ -38,7 +52,6 @@ class VideoController implements IRouteController
 		}
 
 		CORSHelper();
-		Logger::Debug($video_path);
 		if (file_exists($video_path)) {
 			$mime_types = new MimeTypes();
 			$video_contents = file_get_contents($video_path);
@@ -111,6 +124,10 @@ class VideoController implements IRouteController
 	public static function search(): string
 	{
 		$signed_in = signed_in(request());
+        $query = input("query");
+        $client_ip = get_client_ip();
+        Logger::Info("Searching query ($query) for ip address $client_ip.");
+
 		if(!$signed_in) {
 			$data = [];
 			$titles = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeTitles"));
@@ -137,7 +154,6 @@ class VideoController implements IRouteController
 			return api_json($data);
 		}
 
-		$query = input("query");
 		$video = new Video(title: $query);
 		$videos = $video->LoadMany();
 
@@ -210,6 +226,9 @@ class VideoController implements IRouteController
 		$limit = intval(Hajeebtok::$Config->GetByDotKey("Service.VideoFeedLimit"));
 
 		$signed_in = signed_in(request());
+        $client_ip = get_client_ip();
+        Logger::Info("Getting feed for ip address $client_ip.");
+
 		if (!$signed_in) {
 			$titles = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeTitles"));
 			$descriptions = explode(",", Hajeebtok::$Config->GetByDotKey("Service.FakeDescriptions"));
@@ -276,15 +295,18 @@ class VideoController implements IRouteController
 	{
 		if (!signed_in(request())) throw new UnauthenticatedException(0, 401);
 
+        $client_ip = get_client_ip();
+
 		$video_file = input()->file("video");
 		$title = input("title");
 		$description = input("description");
 		$author_id = get_token_id(request());
 		if (empty($video_file) || empty($title) || empty($author_id)) throw new InvalidRequestException(0, 400);
-
 		if (!file_exists($video_file->getTmpName()))
 			throw new InvalidRequestException(400);
 
+        Logger::Info("Uploading video for account Id $author_id with the IP address $client_ip.");
+
 		// save database object
 		$video = new Video(title: $title, description: $description, author_id: $author_id);
 		$video->Save();
@@ -302,9 +324,9 @@ class VideoController implements IRouteController
 
 		// move video
 		mkdir($video_folder);
-		Logger::Debug($video_file->getFilename() . " -> " . $video_folder . "/video.mp4");
 		$video_file->move($video_folder . "/video.mp4");
 
+        Logger::Info("Successfully uploaded video Id $video->id for account Id $author_id with the IP address $client_ip.");
 		CORSHelper();
 		return api_json([
 			"id" => $video->id,
@@ -326,6 +348,10 @@ class VideoController implements IRouteController
 		$limit = intval(Hajeebtok::$Config->GetByDotKey("Service.VideoFeedLimit"));
 
 		$signed_in = signed_in(request());
+        $client_ip = get_client_ip();
+
+        Logger::Info("Getting explore feed for IP address $client_ip.");
+
 		if (!$signed_in) {
 			$feed = [];
 			for($i = 0; $i < $limit; $i++) {
@@ -361,7 +387,6 @@ class VideoController implements IRouteController
 		foreach ($videos as $video) {
 			if ($video_num === 0) $valid_video = true;
 			if ($video["id"] == $video_num) $valid_video = true;
-			Logger::Debug($video["id"] . " -> " . $video["title"]);
 			if (!$valid_video) continue;
 
 			$account = new Account($video["author_id"]);

+ 16 - 0
app/Exceptions/ContactTrackerNotFoundException.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\Exceptions;
+
+use app\Logger;
+use \Exception;
+use \Throwable;
+
+class ContactTrackerNotFoundException extends Exception
+{
+	public function __construct(int $id, int $code = 0, ?Throwable $previous = null)
+	{
+		Logger::Error("Couldn't find contact id ($id)");
+		parent::__construct("Couldn't find contact id ($id)", $code, $previous);
+	}
+}

+ 16 - 0
app/Exceptions/ViewTrackerNotFoundException.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\Exceptions;
+
+use app\Logger;
+use \Exception;
+use \Throwable;
+
+class ViewTrackerNotFoundException extends Exception
+{
+	public function __construct(int $id, int $code = 0, ?Throwable $previous = null)
+	{
+		Logger::Error("Couldn't find view tracker id ($id)");
+		parent::__construct("Couldn't find view tracker id ($id)", $code, $previous);
+	}
+}

+ 4 - 3
app/Hajeebtok.php

@@ -23,10 +23,11 @@ class Hajeebtok {
 	public static Config $Config;
 
 	public static function InitializeApp() {
-		Logger::SetMinLevel(LogLevel::DBUG);
-		Logger::SetMaxLogSizeKB(5000);
+        Logger::SetMinLevel(LogLevel::DBUG);
+		Logger::SetMaxLogSizeKB(1000);
 
 		self::$Config = new Config();
+        date_default_timezone_set(self::$Config->GetByDotKey("Instance.Timezone"));
 
 		self::$Database = new MariaDBDatabase([
 			"Host" => self::$Config->GetByDotKey("Database.Host"),
@@ -44,7 +45,7 @@ class Hajeebtok {
 		HomeController::RegisterRoutes();
         AccountController::RegisterRoutes();
 		
-		Logger::Info("Hajeebtok application initialized.");
+		//Logger::Info("Hajeebtok application initialized.");
 
         SimpleRouter::error(function (Request $request, Exception $error) {
             Logger::Error("Route fault: $error");

+ 20 - 0
app/Helpers.php

@@ -132,4 +132,24 @@ function CORSHelper(): void
     $response->header("Access-Control-Allow-Origin: http://localhost:5173");
     $response->header("Access-Control-Allow-Headers: Authorization, Content-Type");
     $response->header("Access-Control-Allow-Credentials: true");
+}
+
+// Function to get the client IP address
+function get_client_ip() {
+    $ipaddress = '';
+    if (isset($_SERVER['HTTP_CLIENT_IP']))
+        $ipaddress = $_SERVER['HTTP_CLIENT_IP'];
+    else if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
+        $ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
+    else if(isset($_SERVER['HTTP_X_FORWARDED']))
+        $ipaddress = $_SERVER['HTTP_X_FORWARDED'];
+    else if(isset($_SERVER['HTTP_FORWARDED_FOR']))
+        $ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
+    else if(isset($_SERVER['HTTP_FORWARDED']))
+        $ipaddress = $_SERVER['HTTP_FORWARDED'];
+    else if(isset($_SERVER['REMOTE_ADDR']))
+        $ipaddress = $_SERVER['REMOTE_ADDR'];
+    else
+        $ipaddress = 'UNKNOWN';
+    return $ipaddress;
 }

+ 2 - 2
app/Logger.php

@@ -16,7 +16,7 @@ namespace app;
 class Logger
 {
     private const LOG_FILE = "rajesh.log";
-    private const DEFAULT_MAX_SIZE_KB = 5000; // 5MB default max size
+    private const DEFAULT_MAX_SIZE_KB = 1000; // 1MB default max size
 
     private static ?string $logPath = null;
     private static LogLevel $minLevel = LogLevel::DBUG;
@@ -43,7 +43,7 @@ class Logger
         //     return $data;
         // });
 
-        self::Info("Logger initialized. Log file: " . self::$logPath);
+        //self::Info("Logger initialized. Log file: " . self::$logPath);
     }
 
     /**

+ 2 - 2
app/Types/DatabaseObjects/Account.php

@@ -79,13 +79,13 @@ class Account implements IDatabaseObject
 	{
 		if (empty($this->id)) { // search by username
             $data = Hajeebtok::$Database->Row("SELECT * FROM accounts WHERE username = :username", ["username" => $this->username]);
+            if (empty($data)) throw new AccountNotFoundException($this->username, 404);
         } else {
             // search by id
             $data = Hajeebtok::$Database->Row("SELECT * FROM accounts WHERE id = :id", ["id" => $this->id]);
+            if (empty($data)) throw new AccountNotFoundException($this->id, 404);
         }
 
-		if (empty($data)) throw new AccountNotFoundException($this->id, 404);
-
         $this->id = $data["id"];
 		$this->username = $data["username"];
 		$this->password = $data["password"];

+ 117 - 0
app/Types/DatabaseObjects/ContactTracker.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace app\Types\DatabaseObjects;
+
+use app\Exceptions\AccountNotFoundException;
+use app\Exceptions\ContactTrackerNotFoundException;
+use app\Interfaces\IDatabaseObject;
+use app\Hajeebtok;
+use app\Logger;
+use app\Exceptions\SecurityFaultException;
+
+class ContactTracker implements IDatabaseObject
+{
+
+	public private(set) ?int $id;
+	public private(set) ?string $name;
+	public private(set) ?string $email;
+	public private(set) ?string $message;
+	public private(set) ?string $ip;
+    public private(set) ?int $date_sent;
+
+	public function __construct(?int $id = null, ?string $name = null, ?string $email = null, ?string $message = null, ?string $ip = null, ?int $date_sent = null)
+	{
+		$this->id = $id;
+        $this->name = $name;
+        $this->email = $email;
+        $this->message = $message;
+        $this->ip = $ip;
+        $this->date_sent = $date_sent;
+	}
+
+	/**
+	 * Creates the table for the object type in the database.
+	 */
+	public static function CreateTable(): void
+	{
+		throw new SecurityFaultException("Attempt to create table on contact tracker object.");
+	}
+
+	/**
+	 * Drops the table for the object type from the database.
+	 */
+	public static function DropTable(): void
+	{
+		throw new SecurityFaultException("Attempt to drop table on contact tracker object.");
+	}
+	
+	/**
+	 * Saves the object to the database.
+	 */
+	public function Save(): int
+	{
+		Hajeebtok::$Database->Query("INSERT INTO contact_tracking (name, email, message, ip) VALUES (:name, :email, :message, :ip)", [
+            "name" => $this->name,
+            "email" => $this->email,
+            "message" => $this->message,
+            "ip" => $this->ip
+		]);
+		$id = Hajeebtok::$Database->LastInsertId();
+		Logger::Debug("Saved contact id ($id).");
+        return $id;
+	}
+
+	/**
+	 * Deletes the object from the database.
+	 */
+	public function Delete()
+	{
+		Hajeebtok::$Database->Query("DELETE FROM contact_tracking WHERE id = :id", ["id" => $this->id]);
+	}
+
+	/**
+	 * Loads the object from the database.
+	 */
+	public function Load(): void
+    {
+        if(empty($this->id)) { // search via ip
+            $data = Hajeebtok::$Database->Row("SELECT * FROM contact_tracking WHERE ip = :ip", ["ip" => $this->ip]);
+            if (empty($data)) throw new ContactTrackerNotFoundException($this->ip, 404);
+        } else {
+            $data = Hajeebtok::$Database->Row("SELECT * FROM contact_tracking WHERE id = :id", ["id" => $this->id]);
+            if (empty($data)) throw new ContactTrackerNotFoundException($this->id, 404);
+        }
+
+        $this->id = $data["id"];
+		$this->name = $data["name"];
+        $this->email = $data["email"];
+        $this->message = $data["message"];
+        $this->ip = $data["ip"];
+        $this->date_sent = strtotime($data["date_sent"]);
+	}
+    
+    public function LoadMany(): array {
+        if(!empty($this->email) && empty($this->name) && empty($this->ip)) {
+            $query = "SELECT * FROM contact_tracking WHERE email = :email";
+            $array = ["email" => $this->email];
+        } else if (empty($this->email) && !empty($this->name) && empty($this->ip)) {
+            $query = "SELECT * FROM contact_tracking WHERE name = :name";
+            $array = ["name" => $this->name];
+        } else if (empty($this->email) && empty($this->name) && !empty($this->ip)) {
+            $query = "SELECT * FROM contact_tracking WHERE ip = :ip";
+            $array = ["ip" => $this->ip];
+        } else {
+            throw new ContactTrackerNotFoundException(0, 404);
+        }
+
+        $data = Hajeebtok::$Database->Query($query, $array);
+        if(empty($data)) throw new ContactTrackerNotFoundException(0, 404);
+        return $data;
+    }
+
+    public function Exists(): bool {
+        if(empty($this->ip)) throw new ContactTrackerNotFoundException(0, 400);
+        $data = Hajeebtok::$Database->Query("SELECT * FROM contact_tracking WHERE ip = :ip", ["ip" => $this->ip]);
+        return !empty($data);
+    }
+}

+ 16 - 6
app/Types/DatabaseObjects/Session.php

@@ -15,11 +15,12 @@ class Session implements IDatabaseObject
 	public private(set) ?string $token;
     public private(set) ?int $account_id;
     public private(set) ?int $date_authenticated;
+    public private(set) ?string $ip;
 
     /**
      * @throws SecurityFaultException
      */
-    public function __construct(?string $token = null, ?int $account_id = null, ?int $date_authenticated = null)
+    public function __construct(?string $token = null, ?int $account_id = null, ?int $date_authenticated = null, ?string $ip = null)
 	{
         try {
             $this->token = bin2hex(random_bytes(32));
@@ -28,6 +29,7 @@ class Session implements IDatabaseObject
         }
         $this->account_id = $account_id;
         $this->date_authenticated = $date_authenticated;
+        $this->ip = $ip;
 	}
 
 	/**
@@ -51,9 +53,10 @@ class Session implements IDatabaseObject
 	 */
 	public function Save(): string
 	{
-		Hajeebtok::$Database->Query("INSERT INTO sessions (token, account_id) VALUES (:token, :account_id);", [
+		Hajeebtok::$Database->Query("INSERT INTO sessions (token, account_id, ip) VALUES (:token, :account_id, :ip);", [
             "token" => $this->token,
-            "account_id" => $this->account_id
+            "account_id" => $this->account_id,
+            "ip" => $this->ip
         ]);
         
         $token = Hajeebtok::$Database->Row("SELECT token FROM sessions WHERE account_id = :account_id", ["account_id" => $this->account_id]);
@@ -88,12 +91,19 @@ class Session implements IDatabaseObject
         $this->token = $data["token"];
 		$this->account_id = $data["account_id"];
         $this->date_authenticated = strtotime($data["date_authenticated"]);
+        $this->ip = $data["ip"];
 	}
 
     public function LoadMany(): array {
-        if(empty($this->account_id)) throw new SessionNotFoundException(0, 404);
-        $data = Hajeebtok::$Database->Query("SELECT * FROM sessions WHERE account_id = :account_id", ["account_id" => $this->account_id]);
-        if(empty($data)) throw new SessionNotFoundException($this->account_id, 404);
+        if(empty($this->account_id)) { // search via ip
+            if(empty($this->ip)) throw new SessionNotFoundException(0, 404);
+            $data = Hajeebtok::$Database->Query("SELECT * FROM sessions WHERE ip = :ip", ["ip" => $this->ip]);
+            if(empty($data)) throw new SessionNotFoundException($this->ip, 404);
+        } else { // search via account_id
+            if(empty($this->account_id)) throw new SessionNotFoundException(0, 404);
+            $data = Hajeebtok::$Database->Query("SELECT * FROM sessions WHERE account_id = :account_id", ["account_id" => $this->account_id]);
+            if(empty($data)) throw new SessionNotFoundException($this->account_id, 404);
+        }
 
         return $data;
     }

+ 0 - 1
app/Types/DatabaseObjects/Video.php

@@ -79,7 +79,6 @@ class Video implements IDatabaseObject
 		if($this->id === null) throw new VideoNotFoundException(0, 404);
 		$data = Hajeebtok::$Database->Row("SELECT * FROM videos WHERE id = :id", ["id" => $this->id]);
 		if(empty($data)) throw new VideoNotFoundException($this->id, 404);
-		Logger::Debug("$data");
 
 		$this->title = $data["title"];
 		$this->description = $data["description"];

+ 0 - 1
app/Types/DatabaseObjects/View.php

@@ -67,7 +67,6 @@ class View implements IDatabaseObject
 	}
 
     public function LoadMany(): array {
-        Logger::Debug("{$this->account_id}");
         if(!empty($this->account_id)) {
             $array =  ["account_id" => $this->account_id];
             $query = "SELECT * FROM views WHERE account_id = :account_id";

+ 95 - 0
app/Types/DatabaseObjects/ViewTracker.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace app\Types\DatabaseObjects;
+
+use app\Exceptions\AccountNotFoundException;
+use app\Exceptions\ViewTrackerNotFoundException;
+use app\Interfaces\IDatabaseObject;
+use app\Hajeebtok;
+use app\Logger;
+use app\Exceptions\SecurityFaultException;
+
+class ViewTracker implements IDatabaseObject
+{
+
+    public private(set) ?int $id;
+    public private(set) ?int $video_id;
+    public private(set) ?int $tracking_start;
+    public private(set) ?string $ip;
+    public private(set) ?int $account_id;
+
+    public function __construct(?int $id = null, ?int $video_id = null, ?int $tracking_start = null, ?string $ip = null, ?int $account_id = null)
+    {
+        $this->id = $id;
+        $this->video_id = $video_id;
+        $this->tracking_start = $tracking_start;
+        $this->ip = $ip;
+        $this->account_id = $account_id;
+    }
+
+    /**
+     * Creates the table for the object type in the database.
+     */
+    public static function CreateTable(): void
+    {
+        throw new SecurityFaultException("Attempt to create table on view tracker object.");
+    }
+
+    /**
+     * Drops the table for the object type from the database.
+     */
+    public static function DropTable(): void
+    {
+        throw new SecurityFaultException("Attempt to drop table on view tracker object.");
+    }
+
+    /**
+     * Saves the object to the database.
+     */
+    public function Save(): int
+    {
+        Hajeebtok::$Database->Query("INSERT INTO view_tracking (video_id, ip, account_id) VALUES (:video_id, :ip, :account_id)", [
+            "video_id" => $this->video_id,
+            "ip" => $this->ip,
+            "account_id" => $this->account_id
+        ]);
+        $id = Hajeebtok::$Database->LastInsertId();
+        Logger::Info("Saving view tracker for account Id $this->account_id of IP address $this->ip, for video Id $this->video_id.");
+        return $id;
+    }
+
+    /**
+     * Deletes the object from the database.
+     */
+    public function Delete()
+    {
+        Hajeebtok::$Database->Query("DELETE FROM view_tracking WHERE id = :id", ["id" => $this->id]);
+    }
+
+    /**
+     * Loads the object from the database.
+     */
+    public function Load()
+    {
+        if (empty($this->ip)) throw new ViewTrackerNotFoundException(0, 404);
+        $data = Hajeebtok::$Database->Row("SELECT * FROM view_tracking WHERE ip = :ip", ["ip" => $this->ip]);
+        if (empty($data)) throw new ViewTrackerNotFoundException($this->ip, 404);
+
+        $this->id = $data["id"];
+        $this->video_id = $data["video_id"];
+        $this->tracking_start = strtotime($data["tracking_start"]);
+        $this->ip = $data["ip"];
+        $this->account_id = $data["account_id"];
+    }
+
+    public function LoadMany() {
+        // not implemented
+    }
+
+    public function Exists(): bool
+    {
+        if (empty($this->ip)) throw new ViewTrackerNotFoundException(0, 404);
+        $data = Hajeebtok::$Database->Row("SELECT * FROM view_tracking WHERE ip = :ip", ["ip" => $this->ip]);
+        return !empty($data);
+    }
+}