Skip to content

Commit 84e9e66

Browse files
authored
feat: Add support for spoilers (#60)
* feat: add support for spoilers * add text parser * added more safeguards
1 parent 9289483 commit 84e9e66

File tree

5 files changed

+115
-1
lines changed

5 files changed

+115
-1
lines changed

public/scripts/text-parser.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
* This source code is licensed under the license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
// Export for use in other files
9+
if (typeof module !== 'undefined' && module.exports) {
10+
module.exports = { extractSpoilerInfo };
11+
}
12+
13+
/**
14+
* Extracts spoiler text and their positions from a string with **spoiler** markers
15+
* @param {string} text - Input text with **spoiler** markers
16+
* @returns {Object} - Object containing spoiler info and cleaned text
17+
*/
18+
function extractSpoilerInfo(text) {
19+
const spoilerMarker = '**spoiler**';
20+
const textEntities = [];
21+
let cleanedText = '';
22+
let currentPosition = 0;
23+
let searchPosition = 0;
24+
25+
while (currentPosition < text.length && searchPosition < text.length) {
26+
// Find the next spoiler start marker
27+
const startMarkerIndex = text.indexOf(spoilerMarker, searchPosition);
28+
29+
if (startMarkerIndex === -1) {
30+
// No more spoiler markers, add remaining text
31+
cleanedText += text.substring(searchPosition);
32+
break;
33+
}
34+
35+
// Add text before the spoiler marker to cleaned text
36+
const textBeforeMarker = text.substring(searchPosition, startMarkerIndex);
37+
cleanedText += textBeforeMarker;
38+
currentPosition += textBeforeMarker.length;
39+
40+
// Find the closing spoiler marker
41+
const contentStartIndex = startMarkerIndex + spoilerMarker.length;
42+
const endMarkerIndex = text.indexOf(spoilerMarker, contentStartIndex);
43+
44+
if (endMarkerIndex === -1) {
45+
// No closing marker found, treat remaining text as normal
46+
cleanedText += text.substring(startMarkerIndex);
47+
break;
48+
}
49+
50+
// Extract spoiler content
51+
const spoilerContent = text.substring(contentStartIndex, endMarkerIndex);
52+
53+
// Record spoiler info
54+
textEntities.push({
55+
entity_type: "SPOILER",
56+
text: spoilerContent,
57+
offset: currentPosition.toString(),
58+
length: spoilerContent.length.toString(),
59+
});
60+
61+
// Add spoiler content to cleaned text
62+
cleanedText += spoilerContent;
63+
currentPosition += spoilerContent.length;
64+
65+
// Move search position past the end marker
66+
searchPosition = endMarkerIndex + spoilerMarker.length;
67+
}
68+
69+
return {
70+
textEntities: textEntities,
71+
text: cleanedText
72+
};
73+
}

public/scripts/upload.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@
77

88
async function updateMediaType(attachmentsCount, attachmentListElem) {
99
const mediaTypeElem = document.getElementById('media-type');
10+
const spoilerMediaControl = document.querySelector('.spoiler-media-control');
1011

1112
let mediaTypeDesc;
1213
if (attachmentsCount === 0) {
1314
mediaTypeDesc = 'Text 📝';
15+
spoilerMediaControl.style.display = 'none';
1416
} else if (attachmentsCount === 1) {
1517
const singleAttachmentType =
1618
attachmentListElem.querySelector('select').value;
1719
if (singleAttachmentType === 'Image') mediaTypeDesc = 'Image 🖼️';
1820
else mediaTypeDesc = 'Video 🎬';
21+
spoilerMediaControl.style.display = 'block';
1922
} else {
2023
mediaTypeDesc = 'Carousel 🎠';
24+
spoilerMediaControl.style.display = 'block';
2125
}
2226

2327
mediaTypeElem.innerText = mediaTypeDesc;

src/index.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ const fs = require('fs');
1515
const { URLSearchParams, URL } = require('url');
1616
const multer = require('multer');
1717
const { DateTime } = require('luxon');
18+
const textParser = require('../public/scripts/text-parser.js');
1819

20+
const spoilerMarker = "**spoiler**";;
1921
const app = express();
2022
const upload = multer();
2123

@@ -58,6 +60,8 @@ const FIELD__THREADS_BIOGRAPHY = 'threads_biography';
5860
const FIELD__THREADS_PROFILE_PICTURE_URL = 'threads_profile_picture_url';
5961
const FIELD__USERNAME = 'username';
6062
const FIELD__VIEWS = 'views';
63+
const FIELD_IS_SPOILER_MEDIA = 'is_spoiler_media';
64+
const FIELD_TEXT_ENTITES = 'text_entities';
6165

6266
const MEDIA_TYPE__CAROUSEL = 'CAROUSEL';
6367
const MEDIA_TYPE__IMAGE = 'IMAGE';
@@ -94,6 +98,8 @@ const PARAMS__SEARCH_TYPE = 'search_type';
9498
const PARAMS__TEXT = 'text';
9599
const PARAMS__TOPIC_TAG = 'topic_tag';
96100
const PARAMS__USERNAME = 'username';
101+
const PARAMS_IS_SPOILER_MEDIA = 'is_spoiler_media';
102+
const PARAMS_TEXT_ENTITES = 'text_entities';
97103

98104
// Read variables from environment
99105
require('dotenv').config();
@@ -442,15 +448,24 @@ app.post('/upload', upload.array(), async (req, res) => {
442448
pollOptionC,
443449
pollOptionD,
444450
quotePostId,
451+
spoilerMedia,
445452
} = req.body;
446453

447454
const params = {
448-
[PARAMS__TEXT]: text,
449455
[PARAMS__REPLY_CONTROL]: replyControl,
450456
[PARAMS__REPLY_TO_ID]: replyToId,
451457
[PARAMS__LINK_ATTACHMENT]: linkAttachment,
452458
};
453459

460+
if (text.includes(spoilerMarker)) {
461+
parsedInput = textParser.extractSpoilerInfo(text);
462+
processedText = parsedInput.text;
463+
textEntites = parsedInput.textEntities;
464+
params[PARAMS__TEXT] = processedText;
465+
params[PARAMS_TEXT_ENTITES] = JSON.stringify(textEntites);
466+
} else {
467+
params[PARAMS__TEXT] = text;
468+
}
454469
if (
455470
topicTag.length >= 1 &&
456471
topicTag.length <= 50 &&
@@ -460,6 +475,10 @@ app.post('/upload', upload.array(), async (req, res) => {
460475
params[PARAMS__TOPIC_TAG] = topicTag;
461476
}
462477

478+
if (spoilerMedia) {
479+
params[PARAMS_IS_SPOILER_MEDIA] = true;
480+
}
481+
463482
if (pollOptionA && pollOptionB) {
464483
const pollAttachment = JSON.stringify({
465484
option_a: pollOptionA,
@@ -658,6 +677,8 @@ app.get('/threads/:threadId', loggedInUserChecker, async (req, res) => {
658677
FIELD__IS_QUOTE_POST,
659678
FIELD__QUOTED_POST,
660679
FIELD__REPOSTED_POST,
680+
FIELD_IS_SPOILER_MEDIA,
681+
FIELD_TEXT_ENTITES,
661682
].join(','),
662683
},
663684
req.session.access_token

views/thread.pug

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ block content
2323
tr
2424
td Text
2525
td #{text}
26+
tr
27+
td Text Entities
28+
td #{text_entities}
2629
tr
2730
td Topic Tag
2831
td #{topic_tag}
@@ -33,6 +36,9 @@ block content
3336
td Media URL
3437
td
3538
a(href=media_url target='_blank') #{media_url}
39+
tr
40+
td Is Spoiler Media Post
41+
td #{is_spoiler_media}
3642
tr
3743
td GIF URL
3844
td

views/upload.pug

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ block content
3535
textarea {
3636
text-align: center;
3737
padding: 5px 0;
38+
border-radius: 20px;
39+
height: 100px;
3840
}
3941
img {
4042
margin-bottom: 15px;
@@ -53,6 +55,9 @@ block content
5355
#quote-post-id {
5456
width: 200px;
5557
}
58+
.spoiler-media-control {
59+
display: none;
60+
}
5661

5762
if (replyToId !== undefined)
5863
p(style='color: gray') Replying to #{replyToId}
@@ -67,6 +72,11 @@ block content
6772
img#attachments-button(src='/img/attachment.png')
6873
div.attachment-controls
6974
div#attachments-list.attachments
75+
div.spoiler-media-control
76+
label(for='spoilerMedia')
77+
| Mark Media as Spoiler &nbsp;&nbsp;
78+
input(type='checkbox' name='spoilerMedia' value='true')
79+
br
7080

7181
label(for="reply-control") Who Can Reply
7282
select#reply-control(name='replyControl' hint="Reply Control")

0 commit comments

Comments
 (0)