// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; /// Public links only. The board does not fetch, endorse or verify destinations. contract FractoraBoard { struct Link { address author; string url; string title; uint64 createdAt; bool active; } Link[] private links; mapping(address => mapping(bytes32 => bool)) public activeUrl; event LinkPublished(uint256 indexed id, address indexed author, string url, string title); event LinkRemoved(uint256 indexed id, address indexed author); error InvalidUrl(); error InvalidTitle(); error Duplicate(); error NotAuthor(); error Missing(); function publish(string calldata url, string calldata title) external returns (uint256 id) { bytes memory u = bytes(url); bytes memory t = bytes(title); if (u.length < 9 || u.length > 256 || u[0] != 'h' || u[1] != 't' || u[2] != 't' || u[3] != 'p' || u[4] != 's' || u[5] != ':' || u[6] != '/' || u[7] != '/') revert InvalidUrl(); // A prefix check is not a full URL validator or a safety guarantee. for (uint256 i; i < u.length; ++i) if (uint8(u[i]) <= 32 || uint8(u[i]) == 127) revert InvalidUrl(); if (t.length == 0 || t.length > 80) revert InvalidTitle(); bytes32 key = keccak256(u); if (activeUrl[msg.sender][key]) revert Duplicate(); activeUrl[msg.sender][key] = true; id = links.length; links.push(Link(msg.sender, url, title, uint64(block.timestamp), true)); emit LinkPublished(id, msg.sender, url, title); } function remove(uint256 id) external { if (id >= links.length || !links[id].active) revert Missing(); Link storage link = links[id]; if (link.author != msg.sender) revert NotAuthor(); link.active = false; activeUrl[msg.sender][keccak256(bytes(link.url))] = false; emit LinkRemoved(id, msg.sender); } function count() external view returns (uint256) { return links.length; } function get(uint256 id) external view returns (Link memory) { if (id >= links.length) revert Missing(); return links[id]; } }