Files
Alcea 84d9affe28 Add files via upload
Fix VideoBsky upload
2026-02-08 13:41:29 +01:00

1253 lines
40 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Bluesky Poster</title>
<a href="javascript:(function() { var queryString = window.location.search; window.location.href = 'mtdreact.html' + queryString; })();">
React
</a>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
h2 {
color: #0085ff;
border-bottom: 2px solid #0085ff;
padding-bottom: 10px;
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
label {
display: block;
margin: 10px 0 5px;
font-weight: bold;
}
input[type="text"], input[type="password"], textarea {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
font-family: inherit;
}
button {
background-color: #0085ff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 10px 0;
}
button:hover {
background-color: #0066cc;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#duplicateWarning {
color: red;
font-weight: bold;
margin: 10px 0;
display: none;
padding: 10px;
background-color: #ffeeee;
border-radius: 4px;
}
.textboxes-container {
display: flex;
gap: 20px;
margin-bottom: 15px;
}
.textbox-wrapper {
flex: 1;
}
.textbox-wrapper h3 {
margin-top: 0;
color: #0085ff;
}
#hiddenText {
opacity: 0.3;
background-color: #f9f9f9;
cursor: not-allowed;
}
.char-counter {
text-align: right;
font-size: 0.9em;
color: #666;
margin-top: 5px;
}
.char-counter.error {
color: #dc3545;
}
.post-preview {
border: 1px solid #ddd;
padding: 15px;
margin: 15px 0;
background: #f9f9f9;
border-radius: 4px;
min-height: 60px;
}
.hashtag {
color: #0085ff;
font-weight: bold;
}
.link {
color: #0066cc;
text-decoration: underline;
}
.media-section {
margin: 20px 0;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}
.media-controls {
display: flex;
gap: 10px;
margin-top: 10px;
}
.media-preview {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.media-preview img, .media-preview video {
max-width: 100px;
max-height: 100px;
object-fit: cover;
border-radius: 4px;
}
.media-item {
background: white;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
.remove-media-btn {
background: #dc3545;
padding: 5px 10px;
font-size: 0.9em;
margin-top: 5px;
}
.features-section {
margin: 15px 0;
padding: 15px;
background: #fff3cd;
border-radius: 4px;
border: 1px solid #ffeaa7;
}
.feature-item {
margin: 10px 0;
}
.handle-display {
background: #e7f3ff;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
font-weight: bold;
}
.hidden {
display: none !important;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #fff;
border-top: 2px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.info-box {
background: #e7f3ff;
border-left: 4px solid #0085ff;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
#jsonPreview {
border: 1px solid #999;
background: #f0f0f0;
padding: 10px;
margin-top: 15px;
font-family: monospace;
font-size: 0.9em;
white-space: pre-wrap;
min-height: 100px;
}
#debugLog {
border: 1px solid #ccc;
background: #f9f9f9;
padding: 10px;
margin-top: 15px;
font-family: monospace;
font-size: 0.9em;
max-height: 250px;
overflow-y: auto;
white-space: pre-wrap;
}
details {
margin: 15px 0;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
}
summary {
cursor: pointer;
font-weight: bold;
}
.duration-warning {
color: #dc3545;
font-size: 0.9em;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="card">
<h2>Bluesky Poster</h2>
<details>
<summary>Troubleshooting</summary>
<p>This Bluesky poster works entirely in your browser. No server-side code is involved for authentication or posting.</p>
<p><strong>Security:</strong> Your credentials are sent only to Bluesky's official API at bsky.social</p>
<p><strong>Media:</strong> Supports images (JPEG, PNG, GIF, WebP) and videos (MP4, WebM, max 2 minutes, 20MB each)</p>
</details>
<div id="authStatus" class="info-box hidden"></div>
<!-- Login Section -->
<div id="loginSection">
<div class="info-box">
<strong>Security Note:</strong> This runs entirely in your browser. Credentials are only sent to Bluesky's official API.
</div>
<form id="loginForm">
<label for="handle">Bluesky Handle</label>
<input type="text" id="handle" name="handle" placeholder="username.bsky.social" required />
<label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Enter your Bluesky password" required />
<div style="margin-top: 15px;">
<button type="submit" id="loginBtn">
<span id="loginText">Login to Bluesky</span>
<span id="loginLoading" class="loading hidden"></span>
</button>
</div>
</form>
</div>
<!-- Post Section -->
<div id="postSection" class="hidden">
<div id="handleDisplay" class="handle-display">
Logged in as: <span id="currentHandle"></span>
</div>
<div class="textboxes-container">
<div class="textbox-wrapper">
<h3>Post Content (Will be sent to Bluesky):</h3>
<textarea id="mainText" rows="10" placeholder="Edit content here - this gets sent to Bluesky"></textarea>
</div>
<div class="textbox-wrapper">
<h3>Hidden Content (For preview & duplicate check - Never changes):</h3>
<textarea id="hiddenText" rows="10" readonly></textarea>
</div>
</div>
<div class="char-counter" id="charCounter">0/300</div>
<div class="media-section">
<h4>Media Upload</h4>
<div class="info-box">
<p><strong>Supported formats:</strong> JPEG, PNG, GIF, WebP (images) • MP4, WebM (videos, max 2 minutes)</p>
<p><strong>Max size:</strong> 20MB per file • Max 4 files</p>
</div>
<div class="media-controls">
<input type="file" id="mediaInput" accept="image/*,video/mp4,video/webm,video/quicktime" multiple />
<button type="button" id="addMediaBtn">Add Media</button>
<button type="button" id="clearMediaBtn">Clear All Media</button>
</div>
<div id="mediaPreview" class="media-preview"></div>
<div id="mediaList" class="media-list"></div>
</div>
<div class="features-section">
<h4>Post Features:</h4>
<div class="feature-item">
<label>
<input type="checkbox" id="autoDetectHashtags" checked />
Auto-detect #hashtags (as Bluesky facets)
</label>
</div>
<div class="feature-item">
<label>
<input type="checkbox" id="autoDetectLinks" checked />
Auto-detect links (as Bluesky facets)
</label>
</div>
</div>
<div class="post-preview" id="postPreview">Preview will appear here...</div>
<div id="duplicateWarning">This content has already been posted recently!</div>
<div style="margin-top: 20px;">
<button type="button" id="logoutBtn" style="background-color: #6c757d; margin-right: 10px;">Logout</button>
<button type="button" id="postButton" onclick="postToBluesky()">
<span id="postTextBtn">Post to Bluesky</span>
<span id="postLoading" class="loading hidden"></span>
</button>
</div>
</div>
</div>
<div class="card">
<h3>📦 JSON that will be saved:</h3>
<div id="jsonPreview">{}</div>
</div>
<div class="card">
<h3>📦 Past JSON from Local Storage</h3>
<button onclick="loadLocalStoragePosts()">Load Local Storage Items</button>
<button onclick="clearLocalStoragePosts()" style="background-color: #dc3545; margin-left: 10px;">Clear Storage</button>
<div id="localStoragePosts" style="margin-top: 15px; max-height: 300px; overflow-y: auto;"></div>
</div>
<div class="card">
<h3>🪵 Debug Log</h3>
<div id="debugLog"><strong>Debug Log:</strong><br /></div>
</div>
<script>
const PDS_HOST = "https://bsky.social";
const MAX_POST_LENGTH = 300;
const MAX_MEDIA_SIZE = 20971520; // 20MB (Bluesky's actual limit)
const MAX_MEDIA_COUNT = 4;
const MAX_VIDEO_DURATION = 120; // 2 minutes in seconds
const SUPPORTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
const SUPPORTED_VIDEO_TYPES = ["video/mp4", "video/webm", "video/quicktime"];
const SUPPORTED_MEDIA_TYPES = [...SUPPORTED_IMAGE_TYPES, ...SUPPORTED_VIDEO_TYPES];
let state = {
accessJwt: null,
refreshJwt: null,
did: null,
handle: null,
repo: null,
};
let mediaFiles = [];
let alceaId = null;
let uploadedMediaIds = [];
// DOM elements
const authStatus = document.getElementById("authStatus");
const loginSection = document.getElementById("loginSection");
const postSection = document.getElementById("postSection");
const handleDisplay = document.getElementById("handleDisplay");
const currentHandleSpan = document.getElementById("currentHandle");
const jsonPreview = document.getElementById("jsonPreview");
const duplicateWarning = document.getElementById("duplicateWarning");
const postButton = document.getElementById("postButton");
const mainText = document.getElementById("mainText");
const hiddenText = document.getElementById("hiddenText");
function logToDom(message) {
const logDiv = document.getElementById("debugLog");
const timestamp = new Date().toISOString();
logDiv.innerHTML += `[${timestamp}] ${message}\n`;
logDiv.scrollTop = logDiv.scrollHeight;
}
function showMessage(message, type = "info") {
authStatus.textContent = message;
authStatus.className = `info-box ${type}`;
authStatus.classList.remove("hidden");
logToDom(`${type.toUpperCase()}: ${message}`);
if (type === "success") {
setTimeout(() => {
authStatus.classList.add("hidden");
}, 5000);
}
}
function showLoading(elementId, show) {
const textElement = document.getElementById(elementId + "Text");
const loadingElement = document.getElementById(elementId + "Loading");
const buttonElement = document.getElementById(elementId + "Btn") || document.getElementById("postButton");
if (textElement) textElement.classList.toggle("hidden", show);
if (loadingElement) loadingElement.classList.toggle("hidden", !show);
if (buttonElement) buttonElement.disabled = show;
}
function detectFacets(text) {
const facets = [];
const autoDetectHashtags = document.getElementById("autoDetectHashtags").checked;
const autoDetectLinks = document.getElementById("autoDetectLinks").checked;
if (autoDetectHashtags) {
const hashtagRegex = /#(\w+)/g;
let match;
while ((match = hashtagRegex.exec(text)) !== null) {
const start = match.index;
const end = start + match[0].length;
facets.push({
index: {
byteStart: start,
byteEnd: end,
},
features: [
{
$type: "app.bsky.richtext.facet#tag",
tag: match[1],
},
],
});
}
}
if (autoDetectLinks) {
const urlRegex = /(https?:\/\/[^\s<>"'()]+[^\s<>"'().,;:])/gi;
let match;
while ((match = urlRegex.exec(text)) !== null) {
const start = match.index;
const end = start + match[0].length;
try {
new URL(match[0]);
facets.push({
index: {
byteStart: start,
byteEnd: end,
},
features: [
{
$type: "app.bsky.richtext.facet#link",
uri: match[0],
},
],
});
} catch (e) {
// Invalid URL, skip
}
}
}
facets.sort((a, b) => a.index.byteStart - b.index.byteStart);
return facets;
}
function updateCharCounter() {
const textarea = document.getElementById("mainText");
const counter = document.getElementById("charCounter");
const postBtn = document.getElementById("postButton");
const length = textarea.value.length;
counter.textContent = `${length}/${MAX_POST_LENGTH}`;
if (length > MAX_POST_LENGTH) {
counter.classList.add("error");
if (postBtn) postBtn.disabled = true;
} else {
counter.classList.remove("error");
if (postBtn) postBtn.disabled = length === 0 && mediaFiles.length === 0;
}
updatePreview();
}
function updatePreview() {
const preview = document.getElementById("postPreview");
// ALWAYS use hiddenText for preview generation
const previewText = hiddenText.value.trim();
const sendText = mainText.value.trim();
if (!previewText && mediaFiles.length === 0) {
preview.innerHTML = "Preview will appear here...";
return;
}
let html = previewText || "";
// Convert newlines to <br> for preview
html = html.replace(/\n/g, "<br>");
// Convert URLs to links for preview display
const urlRegex = /(https?:\/\/[^\s<>"'()]+[^\s<>"'().,;:])/gi;
let match;
const urlMatches = [];
let newText = html;
while ((match = urlRegex.exec(html)) !== null) {
urlMatches.push({
url: match[0],
index: match.index,
originalMatch: match[0]
});
}
for (let i = urlMatches.length - 1; i >= 0; i--) {
const { url, index, originalMatch } = urlMatches[i];
let displayUrl = url;
let trailingPunctuation = '';
const lastChar = url.slice(-1);
if (/[.,;:]$/.test(lastChar)) {
displayUrl = url.slice(0, -1);
trailingPunctuation = lastChar;
}
const link = `<a href="${displayUrl}" target="_blank" rel="noopener noreferrer">${displayUrl}</a>${trailingPunctuation}`;
newText = newText.substring(0, index) +
link +
newText.substring(index + originalMatch.length);
}
html = newText;
// Show note if texts differ
if (previewText !== sendText) {
html = `<div style="background: #fff3cd; padding: 5px; margin-bottom: 10px; border-radius: 3px; border: 1px solid #ffeaa7;">
<small>⚠️ Preview shows hidden text. Actual post will send: "${sendText.substring(0, 50)}${sendText.length > 50 ? '...' : ''}"</small>
</div>` + html;
}
if (mediaFiles.length > 0) {
html += `<br><br><strong>Media:</strong> ${mediaFiles.length} file(s) attached`;
mediaFiles.forEach((file, index) => {
const mediaType = file.type.startsWith("image/") ? "Image" : "Video";
html += `<br>• ${file.name} (${mediaType}, ${(file.size / (1024 * 1024)).toFixed(2)}MB)`;
if (file.altText) {
html += ` - Alt: ${file.altText}`;
}
if (file.duration) {
html += ` - Duration: ${Math.round(file.duration)}s`;
if (file.duration > MAX_VIDEO_DURATION) {
html += ` <span class="duration-warning">(Exceeds ${MAX_VIDEO_DURATION}s limit!)</span>`;
}
}
});
}
preview.innerHTML = html;
}
function updateMediaPreview() {
const mediaPreviewDiv = document.getElementById("mediaPreview");
const mediaListDiv = document.getElementById("mediaList");
mediaListDiv.innerHTML = "";
mediaPreviewDiv.innerHTML = "";
if (mediaFiles.length === 0) {
mediaPreviewDiv.innerHTML = "<em>No media files added</em>";
return;
}
let html = "";
mediaFiles.forEach((file, index) => {
const altText = file.altText || file.name;
const sizeMB = (file.size / (1024 * 1024)).toFixed(2);
const mediaType = file.type.startsWith("image/") ? "Image" : "Video";
const durationInfo = file.duration ? ` - ${Math.round(file.duration)}s` : '';
html += `
<div class="media-item">
<strong>${index + 1}. ${file.name}</strong> (${mediaType}, ${sizeMB}MB${durationInfo})
${file.duration && file.duration > MAX_VIDEO_DURATION ?
`<div class="duration-warning">⚠️ Video exceeds ${MAX_VIDEO_DURATION}s limit!</div>` : ''}
<div>
<label>Alt Text:</label>
<input type="text" class="media-alt-text" data-index="${index}" value="${altText}" placeholder="Describe this media for accessibility" />
<button type="button" class="remove-media-btn" data-index="${index}">Remove</button>
</div>
</div>
`;
if (file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement("img");
img.src = e.target.result;
img.style.maxWidth = "100px";
img.style.maxHeight = "100px";
img.style.margin = "5px";
img.style.objectFit = "cover";
mediaPreviewDiv.appendChild(img);
};
reader.readAsDataURL(file);
} else if (file.type.startsWith("video/")) {
const reader = new FileReader();
reader.onload = (e) => {
const video = document.createElement("video");
video.src = e.target.result;
video.style.maxWidth = "100px";
video.style.maxHeight = "100px";
video.style.margin = "5px";
video.controls = true;
video.muted = true;
video.preload = "metadata";
video.addEventListener('loadedmetadata', () => {
file.duration = video.duration;
updatePreview();
});
mediaPreviewDiv.appendChild(video);
};
reader.readAsDataURL(file);
}
});
mediaListDiv.innerHTML = html;
document.querySelectorAll(".media-alt-text").forEach((input) => {
input.addEventListener("input", (e) => {
const index = parseInt(e.target.dataset.index);
mediaFiles[index].altText = e.target.value.trim();
updatePreview();
});
});
document.querySelectorAll(".remove-media-btn").forEach((button) => {
button.addEventListener("click", (e) => {
const index = parseInt(e.target.dataset.index);
mediaFiles.splice(index, 1);
updateMediaPreview();
updatePreview();
updateCharCounter();
});
});
}
function handleMediaUpload() {
const files = document.getElementById("mediaInput").files;
if (!files || files.length === 0) return;
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!SUPPORTED_MEDIA_TYPES.includes(file.type)) {
showMessage(`Unsupported file type: ${file.type}. Supported: JPEG, PNG, GIF, WebP, MP4, WebM`, "error");
continue;
}
if (file.size > MAX_MEDIA_SIZE) {
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
const maxSizeMB = (MAX_MEDIA_SIZE / (1024 * 1024)).toFixed(2);
showMessage(
`File ${file.name} is too large (${fileSizeMB}MB). Maximum size is ${maxSizeMB}MB`,
"error"
);
continue;
}
if (mediaFiles.length >= MAX_MEDIA_COUNT) {
showMessage(`Maximum ${MAX_MEDIA_COUNT} media files allowed per post`, "error");
break;
}
file.altText = file.name;
mediaFiles.push(file);
// Check video duration if it's a video
if (file.type.startsWith("video/")) {
checkVideoDuration(file);
}
}
document.getElementById("mediaInput").value = "";
updateMediaPreview();
updatePreview();
updateCharCounter();
}
function checkVideoDuration(videoFile) {
const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = function() {
videoFile.duration = video.duration;
if (video.duration > MAX_VIDEO_DURATION) {
showMessage(`Video ${videoFile.name} exceeds 2-minute limit (${Math.round(video.duration)}s)`, "error");
}
updatePreview();
};
video.onerror = function() {
// Couldn't load metadata, will check during upload
videoFile.duration = null;
};
video.src = URL.createObjectURL(videoFile);
}
function clearAllMedia() {
mediaFiles = [];
uploadedMediaIds = [];
document.getElementById("mediaInput").value = "";
updateMediaPreview();
updatePreview();
updateCharCounter();
showMessage("All media files cleared", "success");
}
// ================= DUPLICATE CHECKING LOGIC =================
async function checkForDuplicates() {
try {
const response = await fetch('/other/extra/scripts/fakesocialmedia/0ld/data_akkoma.json?t=' + Date.now());
const recentPosts = await response.json();
// ALWAYS use hiddenText for duplicate checking (first 60 chars)
const currentText = hiddenText.value.trim();
const previewText = currentText.slice(0, 60);
logToDom(`Checking against ${recentPosts.length} recent posts...`);
const latestPosts = recentPosts.slice(0, 5);
let duplicateFound = false;
for (const post of latestPosts) {
if (currentText.includes(post.preview) || post.preview.includes(previewText)) {
duplicateFound = true;
logToDom(`⚠️ Duplicate found: ${post.preview} (posted at ${post.date})`);
break;
}
}
if (duplicateFound) {
postButton.disabled = true;
duplicateWarning.style.display = 'block';
logToDom("Post button disabled due to duplicate content");
} else {
postButton.disabled = false;
duplicateWarning.style.display = 'none';
logToDom("No duplicates found - posting enabled");
}
} catch (error) {
logToDom("Error checking for duplicates: " + error.message);
}
}
// ============================================================
async function blueskyCreateSession(identifier, password) {
try {
const response = await fetch(`${PDS_HOST}/xrpc/com.atproto.server.createSession`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
identifier: identifier,
password: password,
}),
});
const data = await response.json();
if (response.ok) {
return {
success: true,
accessJwt: data.accessJwt,
refreshJwt: data.refreshJwt,
did: data.did,
handle: data.handle,
};
} else {
return {
success: false,
error: data.message || `Login failed (HTTP ${response.status})`,
};
}
} catch (error) {
return {
success: false,
error: "Network error: " + error.message,
};
}
}
async function uploadBlob(accessJwt, file) {
try {
const response = await fetch(`${PDS_HOST}/xrpc/com.atproto.repo.uploadBlob`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessJwt}`,
"Content-Type": file.type,
},
body: file,
});
const data = await response.json();
if (response.ok) {
return {
success: true,
blob: data.blob,
};
} else {
return {
success: false,
error: data.message || `Upload failed (HTTP ${response.status})`,
};
}
} catch (error) {
return {
success: false,
error: "Network error: " + error.message,
};
}
}
async function blueskyCreatePost(accessJwt, repo, text, mediaFiles = []) {
try {
const postData = {
repo: repo,
collection: "app.bsky.feed.post",
record: {
text: text,
createdAt: new Date().toISOString(),
$type: "app.bsky.feed.post",
},
};
// Add facets for hashtags and links (from the text being sent)
const facets = detectFacets(text);
if (facets.length > 0) {
postData.record.facets = facets;
}
// Handle media embeddings
if (mediaFiles.length > 0) {
const embed = {
$type: "app.bsky.embed.images",
images: [],
};
for (const file of mediaFiles) {
const uploadResult = await uploadBlob(accessJwt, file);
if (!uploadResult.success) {
return {
success: false,
error: `Failed to upload ${file.name}: ${uploadResult.error}`,
};
}
embed.images.push({
image: uploadResult.blob,
alt: file.altText || file.name,
});
// Store the blob reference for JSON
uploadedMediaIds.push(uploadResult.blob.ref['$link'] || uploadResult.blob.cid || 'unknown');
}
postData.record.embed = embed;
}
const response = await fetch(`${PDS_HOST}/xrpc/com.atproto.repo.createRecord`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessJwt}`,
},
body: JSON.stringify(postData),
});
const data = await response.json();
if (response.ok) {
return {
success: true,
uri: data.uri,
cid: data.cid,
facetsCount: facets.length,
};
} else {
return {
success: false,
error: data.message || `Post failed (HTTP ${response.status})`,
};
}
} catch (error) {
return {
success: false,
error: "Network error: " + error.message,
};
}
}
async function handleLogin(event) {
event.preventDefault();
const handle = document.getElementById("handle").value.trim();
const password = document.getElementById("password").value.trim();
if (!handle || !password) {
showMessage("Please enter both handle and password", "error");
return;
}
showLoading("login", true);
const result = await blueskyCreateSession(handle, password);
if (result.success) {
state.accessJwt = result.accessJwt;
state.refreshJwt = result.refreshJwt;
state.did = result.did;
state.handle = result.handle;
state.repo = result.did;
currentHandleSpan.textContent = result.handle;
handleDisplay.classList.remove("hidden");
loginSection.classList.add("hidden");
postSection.classList.remove("hidden");
// Load latest •acws post and check for duplicates
await loadLatestAcwsPost();
showMessage(`✅ Successfully logged in as ${result.handle}`, "success");
// Clear sensitive URL parameters
if (window.history.replaceState) {
const url = new URL(window.location);
if (url.searchParams.has("pass")) {
url.searchParams.delete("pass");
url.searchParams.delete("user");
window.history.replaceState({}, "", url);
showMessage("Password removed from URL for security", "info");
}
}
} else {
showMessage(`${result.error}`, "error");
}
showLoading("login", false);
}
async function loadLatestAcwsPost() {
try {
// First fetch data_alcea.json to get the total count for ID generation
const countResponse = await fetch('data_alcea.json?t=' + Date.now());
const countData = await countResponse.json();
const totalCount = countData.length;
// Then fetch data_part_alcea.json for the actual content
const url = 'data_part_alcea.json?t=' + Date.now();
const res = await fetch(url);
const data = await res.json();
let latest = null;
for (let i = 0; i < data.length; i++) {
const entry = data[i];
const dateKey = Object.keys(entry)[0];
const valueObj = entry[dateKey];
if (valueObj && typeof valueObj.value === "string" && valueObj.value.includes("•acws")) {
latest = valueObj.value;
// Use totalCount from data_alcea.json for ID generation
alceaId = totalCount - i; // Reverse the order
break;
}
}
if (latest) {
// Set BOTH textareas to the same initial value
mainText.value = latest;
hiddenText.value = latest;
logToDom(`Loaded latest •acws entry (ID: ${alceaId}, based on data_alcea.json count)`);
updateCharCounter();
await checkForDuplicates();
} else {
logToDom("No •acws entry found in data_part_alcea.json");
}
} catch (err) {
logToDom("Error loading JSON files: " + err.message);
}
}
async function postToBluesky() {
// ALWAYS use mainText for sending to Bluesky
const postText = mainText.value.trim();
// ALWAYS use hiddenText for preview in JSON
const preview = hiddenText.value.slice(0, 60);
if (!postText && mediaFiles.length === 0) {
showMessage("Please enter post content or add media", "error");
return;
}
if (postText.length > MAX_POST_LENGTH) {
showMessage(`Post exceeds ${MAX_POST_LENGTH} characters`, "error");
return;
}
// Check video durations before posting
for (const file of mediaFiles) {
if (file.type.startsWith("video/") && file.duration && file.duration > MAX_VIDEO_DURATION) {
showMessage(`Video ${file.name} exceeds 2-minute limit (${Math.round(file.duration)}s)`, "error");
return;
}
}
logToDom("Starting Bluesky post process...");
showLoading("post", true);
try {
// Clear previous media IDs
uploadedMediaIds = [];
const result = await blueskyCreatePost(state.accessJwt, state.did, postText, mediaFiles);
if (result.success) {
const now = new Date().toISOString();
// Convert Bluesky URI to web URL
const webUrl = result.uri
.replace('at://', 'https://bsky.app/profile/')
.replace('/app.bsky.feed.post/', '/post/');
const logObject = {
date: now,
url: webUrl,
preview: preview, // ALWAYS from hiddenText
mode: "bluesky",
mediaType: "text/plain",
isReply: false,
replyTo: null,
replyLocalId: null,
mentionUsed: null,
visibility: "public",
alceaId: alceaId,
mediaCount: mediaFiles.length,
mediaIds: uploadedMediaIds
};
jsonPreview.textContent = JSON.stringify(logObject, null, 2);
showMessage(`✅ Posted to Bluesky!`, "success");
logToDom(`✅ Posted. URL: ${webUrl} | Media: ${mediaFiles.length} file(s) | Facets: ${result.facetsCount || 0} | alceaId: ${alceaId}`);
logToDom(`📝 Sent text: "${postText.substring(0, 100)}${postText.length > 100 ? '...' : ''}"`);
logToDom(`📋 Preview from hidden text: "${preview}"`);
// Save to server (same endpoint as before)
await fetch("/other/extra/scripts/fakesocialmedia/0ld/save_post.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(logObject),
});
logToDom("📁 JSON sent to save_post.php");
// Save to localStorage
try {
const existingPosts = JSON.parse(localStorage.getItem('blueskyPosts') || '[]');
existingPosts.push(logObject);
localStorage.setItem('blueskyPosts', JSON.stringify(existingPosts));
logToDom("💾 JSON appended to localStorage");
} catch (storageError) {
logToDom("⚠️ Could not save to localStorage: " + storageError.message);
}
// Clear ONLY mainText (the editable one)
mainText.value = "";
// Keep hiddenText unchanged for future reference
updateCharCounter();
updateMediaPreview();
// Auto-load the updated local storage
setTimeout(loadLocalStoragePosts, 500);
} else {
showMessage(`${result.error}`, "error");
}
} catch (err) {
logToDom("❌ Error: " + err.message);
showMessage("❌ Error posting to Bluesky: " + err.message, "error");
}
showLoading("post", false);
}
function handleLogout() {
state = {
accessJwt: null,
refreshJwt: null,
did: null,
handle: null,
repo: null,
};
mediaFiles = [];
uploadedMediaIds = [];
alceaId = null;
loginSection.classList.remove("hidden");
postSection.classList.add("hidden");
handleDisplay.classList.add("hidden");
document.getElementById("loginForm").reset();
mainText.value = "";
hiddenText.value = "";
updateMediaPreview();
updateCharCounter();
duplicateWarning.style.display = 'none';
showMessage("Logged out successfully", "success");
}
function loadLocalStoragePosts() {
try {
const posts = JSON.parse(localStorage.getItem('blueskyPosts') || '[]');
const container = document.getElementById('localStoragePosts');
if (posts.length === 0) {
container.innerHTML = '<div class="info-box">No posts found in local storage</div>';
logToDom("No posts found in local storage");
return;
}
let html = `<p>Found ${posts.length} posts in local storage:</p>`;
posts.forEach((post, index) => {
const mediaInfo = post.mediaCount > 0 ? `📷 ${post.mediaCount} media file(s)` : 'No media';
html += `
<div class="media-item" style="border: 1px solid #ddd; padding: 10px; margin: 10px 0; background: white;">
<strong>#${index + 1}:</strong> ${post.date}<br>
<strong>Preview:</strong> ${post.preview}<br>
<strong>URL:</strong> <a href="${post.url}" target="_blank">${post.url.substring(0, 50)}...</a><br>
<strong>Mode:</strong> ${post.mode}<br>
<strong>Media:</strong> ${mediaInfo}<br>
<strong>alceaId:</strong> ${post.alceaId}<br>
<button onclick="viewPostDetails(${index})" style="margin-top: 5px; padding: 3px 8px; font-size: 0.8em;">View Details</button>
<div id="postDetails-${index}" style="display: none; margin-top: 10px; padding: 10px; background: #f9f9f9;">
<pre>${JSON.stringify(post, null, 2)}</pre>
</div>
</div>
`;
});
container.innerHTML = html;
logToDom(`Loaded ${posts.length} posts from local storage`);
} catch (error) {
logToDom("Error loading local storage posts: " + error.message);
}
}
function viewPostDetails(index) {
const detailsElement = document.getElementById(`postDetails-${index}`);
if (detailsElement.style.display === 'none') {
detailsElement.style.display = 'block';
} else {
detailsElement.style.display = 'none';
}
}
function clearLocalStoragePosts() {
if (confirm("Are you sure you want to clear all posts from local storage?")) {
localStorage.removeItem('blueskyPosts');
document.getElementById('localStoragePosts').innerHTML =
'<div class="info-box">Local storage cleared</div>';
logToDom("Local storage cleared");
}
}
function init() {
// Set up event listeners
document.getElementById("loginForm").addEventListener("submit", handleLogin);
document.getElementById("logoutBtn").addEventListener("click", handleLogout);
// Update preview when mainText changes
mainText.addEventListener("input", function() {
updateCharCounter();
updatePreview();
// DON'T update hiddenText - it stays unchanged
});
document.getElementById("mediaInput").addEventListener("change", handleMediaUpload);
document.getElementById("addMediaBtn").addEventListener("click", () => document.getElementById("mediaInput").click());
document.getElementById("clearMediaBtn").addEventListener("click", clearAllMedia);
document.getElementById("autoDetectHashtags").addEventListener("change", updatePreview);
document.getElementById("autoDetectLinks").addEventListener("change", updatePreview);
// Check URL parameters for auto-login
const urlParams = new URLSearchParams(window.location.search);
const handle = urlParams.get("user");
const password = urlParams.get("pass");
if (handle) document.getElementById("handle").value = handle;
if (password) document.getElementById("password").value = password;
updateCharCounter();
updatePreview();
logToDom("Bluesky Poster initialized");
}
document.addEventListener("DOMContentLoaded", init);
</script>
</body>
</html>