mirror of
https://github.com/CompeyDev/fxtwitter-docker.git
synced 2025-04-05 10:30:55 +01:00
Add community notes, author actions
This commit is contained in:
parent
e2bb948f7d
commit
2062109692
4 changed files with 119 additions and 11 deletions
|
@ -127,6 +127,14 @@ export const buildAPITwitterStatus = async (
|
||||||
apiStatus.is_note_tweet = false;
|
apiStatus.is_note_tweet = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.birdwatch_pivot?.subtitle?.text) {
|
||||||
|
/* We can't automatically replace links because API doesn't give full URLs, only t.co versions :( */
|
||||||
|
apiStatus.community_note = {
|
||||||
|
text: unescapeText(status.birdwatch_pivot?.subtitle?.text ?? ''),
|
||||||
|
entities: status.birdwatch_pivot.subtitle?.entities ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (status.legacy.lang !== 'unk') {
|
if (status.legacy.lang !== 'unk') {
|
||||||
apiStatus.lang = status.legacy.lang;
|
apiStatus.lang = status.legacy.lang;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -4,6 +4,12 @@ import { getSocialTextIV } from '../helpers/socialproof';
|
||||||
import { sanitizeText } from '../helpers/utils';
|
import { sanitizeText } from '../helpers/utils';
|
||||||
import { Strings } from '../strings';
|
import { Strings } from '../strings';
|
||||||
|
|
||||||
|
enum AuthorActionType {
|
||||||
|
Reply = 'Reply',
|
||||||
|
Original = 'Original',
|
||||||
|
FollowUp = 'FollowUp'
|
||||||
|
}
|
||||||
|
|
||||||
const populateUserLinks = (status: APIStatus, text: string): string => {
|
const populateUserLinks = (status: APIStatus, text: string): string => {
|
||||||
/* TODO: Maybe we can add username splices to our API so only genuinely valid users are linked? */
|
/* TODO: Maybe we can add username splices to our API so only genuinely valid users are linked? */
|
||||||
text.match(/@(\w{1,15})/g)?.forEach(match => {
|
text.match(/@(\w{1,15})/g)?.forEach(match => {
|
||||||
|
@ -117,8 +123,10 @@ const truncateSocialCount = (count: number): string => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateInlineAuthorHeader = (status: APIStatus, author: APIUser): string => {
|
const generateInlineAuthorHeader = (status: APIStatus, author: APIUser, authorActionType: AuthorActionType | null): string => {
|
||||||
return `<i><a href="${status.url}">Reply</a> from <b>${author.name}</b> (<a href="${author.url}">@${author.screen_name}</a>):</i>`;
|
return `<h4><i><a href="${status.url}">{AuthorAction}</a> from <b>${author.name}</b> (<a href="${author.url}">@${author.screen_name}</a>):</i></h4>`.format({
|
||||||
|
AuthorAction: authorActionType === AuthorActionType.Reply ? 'Reply' : authorActionType === AuthorActionType.Original ? 'Original' : 'Follow-up'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,7 +144,7 @@ const generateStatusFooter = (status: APIStatus, isQuote = false, author: APIUse
|
||||||
`.format({
|
`.format({
|
||||||
socialText: getSocialTextIV(status as APITwitterStatus) || '',
|
socialText: getSocialTextIV(status as APITwitterStatus) || '',
|
||||||
viewOriginal: !isQuote
|
viewOriginal: !isQuote
|
||||||
? `<a href="${status.url}">View original post</a>`
|
? `<a href="${status.url}">View full thread</a>`
|
||||||
: notApplicableComment,
|
: notApplicableComment,
|
||||||
aboutSection: isQuote
|
aboutSection: isQuote
|
||||||
? ''
|
? ''
|
||||||
|
@ -166,7 +174,48 @@ const generateStatusFooter = (status: APIStatus, isQuote = false, author: APIUse
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateStatus = (status: APIStatus, author: APIUser, isQuote = false, differentAuthor = false): string => {
|
const generateCommunityNote = (status: APITwitterStatus): string => {
|
||||||
|
if (status.community_note) {
|
||||||
|
const note = status.community_note;
|
||||||
|
const entities = note.entities;
|
||||||
|
entities.sort((a, b) => a.fromIndex - b.fromIndex); // sort entities by fromIndex
|
||||||
|
|
||||||
|
let lastToIndex = 0;
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
entities.forEach(entity => {
|
||||||
|
if (entity?.ref?.type !== 'TimelineUrl') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const fromIndex = entity.fromIndex;
|
||||||
|
const toIndex = entity.toIndex;
|
||||||
|
const url = entity.ref.url;
|
||||||
|
|
||||||
|
// Add the text before the link
|
||||||
|
result += note.text.substring(lastToIndex, fromIndex);
|
||||||
|
|
||||||
|
// Add the link
|
||||||
|
result += `<a href="${url}">${note.text.substring(fromIndex, toIndex)}</a> `;
|
||||||
|
|
||||||
|
lastToIndex = toIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the remaining text after the last link
|
||||||
|
result = `<table>
|
||||||
|
<thead>
|
||||||
|
<th><b>Readers added context they thought people might want to know</b></th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<th>${result}</th>
|
||||||
|
</tbody>
|
||||||
|
</table>`;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateStatus = (status: APIStatus, author: APIUser, isQuote = false, authorActionType: AuthorActionType | null): string => {
|
||||||
let text = paragraphify(sanitizeText(status.text), isQuote);
|
let text = paragraphify(sanitizeText(status.text), isQuote);
|
||||||
text = htmlifyLinks(text);
|
text = htmlifyLinks(text);
|
||||||
text = htmlifyHashtags(text);
|
text = htmlifyHashtags(text);
|
||||||
|
@ -181,11 +230,13 @@ const generateStatus = (status: APIStatus, author: APIUser, isQuote = false, dif
|
||||||
<!-- Translated text (if applicable) -->
|
<!-- Translated text (if applicable) -->
|
||||||
${translatedText ? translatedText : notApplicableComment}
|
${translatedText ? translatedText : notApplicableComment}
|
||||||
<!-- Inline author (if applicable) -->
|
<!-- Inline author (if applicable) -->
|
||||||
${differentAuthor ? generateInlineAuthorHeader(status, author) : ''}
|
${authorActionType ? generateInlineAuthorHeader(status, author, authorActionType) : ''}
|
||||||
<!-- Embed Status text -->
|
<!-- Embed Status text -->
|
||||||
${text}
|
${text}
|
||||||
|
<!-- Embed Community Note -->
|
||||||
|
${generateCommunityNote(status as APITwitterStatus)}
|
||||||
<!-- Embedded quote status -->
|
<!-- Embedded quote status -->
|
||||||
${!isQuote && status.quote ? generateStatus(status.quote, author, true) : notApplicableComment}
|
${!isQuote && status.quote ? generateStatus(status.quote, author, true, null) : notApplicableComment}
|
||||||
`.format({
|
`.format({
|
||||||
quoteHeader: isQuote
|
quoteHeader: isQuote
|
||||||
? `<h4><a href="${status.url}">Quoting</a> ${author.name} (<a href="${Constants.TWITTER_ROOT}/${author.screen_name}">@${author.screen_name}</a>)</h4>`
|
? `<h4><a href="${status.url}">Quoting</a> ${author.name} (<a href="${Constants.TWITTER_ROOT}/${author.screen_name}">@${author.screen_name}</a>)</h4>`
|
||||||
|
@ -199,6 +250,7 @@ export const renderInstantView = (properties: RenderProperties): ResponseInstruc
|
||||||
const instructions: ResponseInstructions = { addHeaders: [] };
|
const instructions: ResponseInstructions = { addHeaders: [] };
|
||||||
|
|
||||||
let previousThreadPieceAuthor: string | null = null;
|
let previousThreadPieceAuthor: string | null = null;
|
||||||
|
let originalAuthor: string | null = null;
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
throw new Error('Status is undefined');
|
throw new Error('Status is undefined');
|
||||||
|
@ -228,20 +280,42 @@ export const renderInstantView = (properties: RenderProperties): ResponseInstruc
|
||||||
flags?.archive
|
flags?.archive
|
||||||
? `${Constants.BRANDING_NAME} archive`
|
? `${Constants.BRANDING_NAME} archive`
|
||||||
: 'If you can see this, your browser is doing something weird with your user agent.'
|
: 'If you can see this, your browser is doing something weird with your user agent.'
|
||||||
} <a href="${status.url}">View original post</a>
|
} <a href="${status.url}">View full thread</a>
|
||||||
</section>
|
</section>
|
||||||
<article>
|
<article>
|
||||||
<sub><a href="${status.url}">View original</a></sub>
|
<sub><a href="${status.url}">View full thread</a></sub>
|
||||||
<h1>${status.author.name} (@${status.author.screen_name})</h1>
|
<h1>${status.author.name} (@${status.author.screen_name})</h1>
|
||||||
|
|
||||||
${thread?.thread?.map((status) => {
|
${thread?.thread?.map((status) => {
|
||||||
|
console.log('previousThreadPieceAuthor', previousThreadPieceAuthor)
|
||||||
|
if (originalAuthor === null) {
|
||||||
|
originalAuthor = status.author?.id;
|
||||||
|
}
|
||||||
const differentAuthor = thread?.author?.id !== status.author?.id || (previousThreadPieceAuthor !== null && previousThreadPieceAuthor !== status.author?.id);
|
const differentAuthor = thread?.author?.id !== status.author?.id || (previousThreadPieceAuthor !== null && previousThreadPieceAuthor !== status.author?.id);
|
||||||
|
const isOriginal = thread?.author?.id !== status.author?.id && previousThreadPieceAuthor === null;
|
||||||
|
const isFollowup = thread?.author?.id === status.author?.id && previousThreadPieceAuthor !== null && previousThreadPieceAuthor !== thread?.author?.id && originalAuthor === status.author?.id;
|
||||||
|
console.log('differentAuthor', differentAuthor)
|
||||||
|
console.log('isOriginal', isOriginal)
|
||||||
|
console.log('isFollowup', isFollowup)
|
||||||
|
|
||||||
|
let authorAction = null;
|
||||||
|
|
||||||
|
if (differentAuthor) {
|
||||||
|
if (isFollowup) {
|
||||||
|
authorAction = AuthorActionType.FollowUp;
|
||||||
|
} else if (isOriginal) {
|
||||||
|
authorAction = AuthorActionType.Original;
|
||||||
|
} else if (previousThreadPieceAuthor !== status.author?.id) {
|
||||||
|
authorAction = AuthorActionType.Reply;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
previousThreadPieceAuthor = status.author?.id;
|
previousThreadPieceAuthor = status.author?.id;
|
||||||
|
|
||||||
return generateStatus(status, status.author ?? thread?.author, false, differentAuthor)
|
return generateStatus(status, status.author ?? thread?.author, false, authorAction)
|
||||||
}).join('')}
|
}).join('')}
|
||||||
${generateStatusFooter(status, false, thread?.author ?? status.author)}
|
${generateStatusFooter(status, false, thread?.author ?? status.author)}
|
||||||
<br>${`<a href="${status.url}">View original post</a>`}
|
<br>${`<a href="${status.url}">View full thread</a>`}
|
||||||
</article>`;
|
</article>`;
|
||||||
|
|
||||||
return instructions;
|
return instructions;
|
||||||
|
|
6
src/types/types.d.ts
vendored
6
src/types/types.d.ts
vendored
|
@ -148,11 +148,17 @@ interface APIStatus {
|
||||||
embed_card: 'tweet' | 'summary' | 'summary_large_image' | 'player';
|
embed_card: 'tweet' | 'summary' | 'summary_large_image' | 'player';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface APITwitterCommunityNote {
|
||||||
|
text: string;
|
||||||
|
entities: BirdwatchEntity[];
|
||||||
|
}
|
||||||
|
|
||||||
interface APITwitterStatus extends APIStatus {
|
interface APITwitterStatus extends APIStatus {
|
||||||
views?: number | null;
|
views?: number | null;
|
||||||
translation?: APITranslate;
|
translation?: APITranslate;
|
||||||
|
|
||||||
is_note_tweet: boolean;
|
is_note_tweet: boolean;
|
||||||
|
community_note: APITwitterCommunityNote | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APIUser {
|
interface APIUser {
|
||||||
|
|
22
src/types/vendor/twitter.d.ts
vendored
22
src/types/vendor/twitter.d.ts
vendored
|
@ -351,13 +351,33 @@ type GraphQLTwitterStatusLegacy = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BirdwatchEntity = {
|
||||||
|
fromIndex: number, // 119
|
||||||
|
toIndex: number, // 154
|
||||||
|
ref: {
|
||||||
|
type: "TimelineUrl",
|
||||||
|
url: string, // https://t.co/jxvVatCVCz
|
||||||
|
urlType: "ExternalUrl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type GraphQLTwitterStatus = {
|
type GraphQLTwitterStatus = {
|
||||||
// Workaround
|
// Workaround
|
||||||
result: GraphQLTwitterStatus;
|
result: GraphQLTwitterStatus;
|
||||||
__typename: 'Tweet' | 'TweetWithVisibilityResults' | 'TweetUnavailable';
|
__typename: 'Tweet' | 'TweetWithVisibilityResults' | 'TweetUnavailable';
|
||||||
reason: string; // used for errors
|
reason: string; // used for errors
|
||||||
rest_id: string; // "1674824189176590336",
|
rest_id: string; // "1674824189176590336",
|
||||||
has_birdwatch_notes: false;
|
has_birdwatch_notes: boolean;
|
||||||
|
birdwatch_pivot: {
|
||||||
|
destinationUrl: string, // https://twitter.com/i/birdwatch/n/1784594925926973714
|
||||||
|
note: {
|
||||||
|
rest_id: string // 1784594925926973714
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
text: string, // "This screenshot is from Sonic 1\n\ninfo.sonicretro.org/Sonic_the_Hedg…"
|
||||||
|
entities: BirdwatchEntity[]
|
||||||
|
},
|
||||||
|
}
|
||||||
core: {
|
core: {
|
||||||
user_results: {
|
user_results: {
|
||||||
result: GraphQLUser;
|
result: GraphQLUser;
|
||||||
|
|
Loading…
Add table
Reference in a new issue