mirror of
https://github.com/CompeyDev/fxtwitter-docker.git
synced 2025-04-04 18:10:56 +01:00
Polls and external media in API
This commit is contained in:
parent
0cb705eb9c
commit
0e20ea9db4
5 changed files with 133 additions and 77 deletions
102
src/api.ts
102
src/api.ts
|
@ -1,7 +1,66 @@
|
|||
import { renderCard } from './card';
|
||||
import { fetchUsingGuest } from './fetch';
|
||||
import { linkFixer } from './linkFixer';
|
||||
import { colorFromPalette } from './palette';
|
||||
import { translateTweet } from './translate';
|
||||
|
||||
export const statueAPI = async (
|
||||
const populateTweetProperties = async (
|
||||
tweet: TweetPartial,
|
||||
conversation: TimelineBlobPartial,
|
||||
language: string = 'en'
|
||||
): Promise<APITweet> => {
|
||||
let apiTweet = {} as APITweet;
|
||||
|
||||
const user = tweet.user;
|
||||
const screenName = user?.screen_name || '';
|
||||
const name = user?.name || '';
|
||||
|
||||
apiTweet.text = linkFixer(tweet, tweet.full_text);
|
||||
apiTweet.author = {
|
||||
name: name,
|
||||
screen_name: screenName,
|
||||
avatar_url: user?.profile_image_url_https.replace('_normal', '_200x200') || '',
|
||||
avatar_color: (apiTweet.palette = colorFromPalette(
|
||||
tweet.user?.profile_image_extensions_media_color?.palette || []
|
||||
)),
|
||||
banner_url: user?.profile_banner_url || ''
|
||||
};
|
||||
apiTweet.replies = tweet.reply_count;
|
||||
apiTweet.retweets = tweet.retweet_count;
|
||||
apiTweet.likes = tweet.favorite_count;
|
||||
apiTweet.palette = colorFromPalette(
|
||||
tweet.user?.profile_image_extensions_media_color?.palette || []
|
||||
);
|
||||
|
||||
if (tweet.card) {
|
||||
let card = await renderCard(tweet.card);
|
||||
if (card.external_media) {
|
||||
apiTweet.media = apiTweet.media || {};
|
||||
apiTweet.media.external = card.external_media;
|
||||
}
|
||||
if (card.poll) {
|
||||
apiTweet.poll = card.poll;
|
||||
}
|
||||
}
|
||||
|
||||
/* If a language is specified, let's try translating it! */
|
||||
if (typeof language === 'string' && language.length === 2 && language !== tweet.lang) {
|
||||
let translateAPI = await translateTweet(
|
||||
tweet,
|
||||
conversation.guestToken || '',
|
||||
language
|
||||
);
|
||||
apiTweet.translation = {
|
||||
text: translateAPI?.translation || '',
|
||||
source_lang: translateAPI?.sourceLanguage || '',
|
||||
target_lang: translateAPI?.destinationLanguage || ''
|
||||
};
|
||||
}
|
||||
|
||||
return apiTweet;
|
||||
};
|
||||
|
||||
export const statusAPI = async (
|
||||
event: FetchEvent,
|
||||
status: string,
|
||||
language: string
|
||||
|
@ -37,35 +96,20 @@ export const statueAPI = async (
|
|||
}
|
||||
|
||||
let response: APIResponse = { code: 200, message: 'OK' } as APIResponse;
|
||||
let apiTweet: APITweet = {} as APITweet;
|
||||
let apiTweet: APITweet = (await populateTweetProperties(
|
||||
tweet,
|
||||
conversation,
|
||||
language
|
||||
)) as APITweet;
|
||||
|
||||
const user = tweet.user;
|
||||
const screenName = user?.screen_name || '';
|
||||
const name = user?.name || '';
|
||||
|
||||
apiTweet.text = tweet.full_text;
|
||||
apiTweet.author = {
|
||||
name: name,
|
||||
screen_name: screenName,
|
||||
avatar_url: user?.profile_image_url_https || '',
|
||||
banner_url: user?.profile_banner_url || ''
|
||||
};
|
||||
apiTweet.replies = tweet.reply_count;
|
||||
apiTweet.retweets = tweet.retweet_count;
|
||||
apiTweet.likes = tweet.favorite_count;
|
||||
|
||||
/* If a language is specified, let's try translating it! */
|
||||
if (typeof language === 'string' && language.length === 2 && language !== tweet.lang) {
|
||||
let translateAPI = await translateTweet(
|
||||
tweet,
|
||||
conversation.guestToken || '',
|
||||
language || 'en'
|
||||
);
|
||||
apiTweet.translation = {
|
||||
translated_text: translateAPI?.translation || '',
|
||||
source_language: tweet.lang,
|
||||
target_language: language
|
||||
};
|
||||
let quoteTweet =
|
||||
conversation.globalObjects?.tweets?.[tweet.quoted_status_id_str || '0'] || null;
|
||||
if (quoteTweet) {
|
||||
apiTweet.quote = (await populateTweetProperties(
|
||||
quoteTweet,
|
||||
conversation,
|
||||
language
|
||||
)) as APITweet;
|
||||
}
|
||||
|
||||
response.tweet = apiTweet;
|
||||
|
|
68
src/card.ts
68
src/card.ts
|
@ -44,20 +44,14 @@ export const calculateTimeLeftString = (date: Date) => {
|
|||
};
|
||||
|
||||
export const renderCard = async (
|
||||
card: TweetCard,
|
||||
headers: string[],
|
||||
userAgent: string = ''
|
||||
): Promise<string> => {
|
||||
card: TweetCard
|
||||
): Promise<{ poll?: APIPoll; external_media?: APIExternalMedia }> => {
|
||||
let str = '\n\n';
|
||||
const values = card.binding_values;
|
||||
|
||||
console.log('rendering card on ', card);
|
||||
|
||||
// Telegram's bars need to be a lot smaller to fit its bubbles
|
||||
if (userAgent.indexOf('Telegram') > -1) {
|
||||
barLength = 24;
|
||||
}
|
||||
|
||||
let choices: { [label: string]: number } = {};
|
||||
let totalVotes = 0;
|
||||
let timeLeft = '';
|
||||
|
@ -68,6 +62,9 @@ export const renderCard = async (
|
|||
typeof values.choice1_count !== 'undefined' &&
|
||||
typeof values.choice2_count !== 'undefined'
|
||||
) {
|
||||
let poll = {} as APIPoll;
|
||||
poll.ends_at = values.end_datetime_utc?.string_value || '';
|
||||
|
||||
if (typeof values.end_datetime_utc !== 'undefined') {
|
||||
const date = new Date(values.end_datetime_utc.string_value);
|
||||
timeLeft = calculateTimeLeftString(date);
|
||||
|
@ -89,44 +86,35 @@ export const renderCard = async (
|
|||
if (typeof values.choice4_count !== 'undefined') {
|
||||
choices[values.choice4_label?.string_value || ''] = parseInt(
|
||||
values.choice4_count.string_value
|
||||
);
|
||||
) || 0;
|
||||
totalVotes += parseInt(values.choice4_count.string_value);
|
||||
}
|
||||
|
||||
for (const [label, votes] of Object.entries(choices)) {
|
||||
// render bar
|
||||
const bar = '█'.repeat(Math.round((votes / totalVotes || 0) * barLength));
|
||||
str += `${bar}
|
||||
${label} (${Math.round((votes / totalVotes || 0) * 100)}%)
|
||||
`;
|
||||
}
|
||||
poll.total_votes = totalVotes;
|
||||
poll.choices = Object.keys(choices).map(label => {
|
||||
return {
|
||||
label: label,
|
||||
count: choices[label],
|
||||
percentage: ((Math.round((choices[label] / totalVotes) * 1000) || 0) / 10 || 0)
|
||||
};
|
||||
});
|
||||
|
||||
str += `\n${totalVotes} votes · ${timeLeft}`;
|
||||
return { poll: poll };
|
||||
/* Oh good, a non-Twitter video URL! This enables YouTube embeds and stuff to just work */
|
||||
} else if (typeof values.player_url !== 'undefined') {
|
||||
headers.push(
|
||||
`<meta name="twitter:player" content="${values.player_url.string_value}">`,
|
||||
`<meta name="twitter:player:width" content="${
|
||||
values.player_width?.string_value || '1280'
|
||||
}">`,
|
||||
`<meta name="twitter:player:height" content="${
|
||||
values.player_height?.string_value || '720'
|
||||
}">`,
|
||||
`<meta property="og:type" content="video.other">`,
|
||||
`<meta property="og:video:url" content="${values.player_url.string_value}">`,
|
||||
`<meta property="og:video:secure_url" content="${values.player_url.string_value}">`,
|
||||
`<meta property="og:video:width" content="${
|
||||
values.player_width?.string_value || '1280'
|
||||
}">`,
|
||||
`<meta property="og:video:height" content="${
|
||||
values.player_height?.string_value || '720'
|
||||
}">`
|
||||
);
|
||||
|
||||
/* A control sequence I made up to tell status.ts that external media is being embedded */
|
||||
str = 'EMBED_CARD';
|
||||
return {
|
||||
external_media: {
|
||||
type: 'video',
|
||||
url: values.player_url.string_value,
|
||||
width: parseInt(
|
||||
(values.player_width?.string_value || '1280').replace('px', '')
|
||||
), // TODO: Replacing px might not be necessary, it's just there as a precaution
|
||||
height: parseInt(
|
||||
(values.player_height?.string_value || '720').replace('px', '')
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
return {};
|
||||
};
|
||||
|
|
|
@ -99,6 +99,7 @@ router.get('/:prefix?/:handle/statuses/:id/video/:mediaNumber', statusRequest);
|
|||
router.get('/:prefix?/:handle/status/:id/:language', statusRequest);
|
||||
router.get('/:prefix?/:handle/statuses/:id/:language', statusRequest);
|
||||
router.get('/status/:id', statusRequest);
|
||||
router.get('/status/:id/:language', statusRequest);
|
||||
|
||||
router.get('/owoembed', async (request: Request) => {
|
||||
console.log('oembed hit!');
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Strings } from './strings';
|
|||
import { handleMosaic } from './mosaic';
|
||||
import { translateTweet } from './translate';
|
||||
import { getAuthorText } from './author';
|
||||
import { statueAPI } from './api';
|
||||
import { statusAPI } from './api';
|
||||
|
||||
export const returnError = (error: string): StatusResponse => {
|
||||
return {
|
||||
|
@ -33,7 +33,7 @@ export const handleStatus = async (
|
|||
): Promise<StatusResponse> => {
|
||||
console.log('Direct?', flags?.direct);
|
||||
|
||||
let api = await statueAPI(event, status, language || 'en');
|
||||
let api = await statusAPI(event, status, language || 'en');
|
||||
|
||||
if (flags?.api || true) {
|
||||
return {
|
||||
|
|
35
src/types.d.ts
vendored
35
src/types.d.ts
vendored
|
@ -24,19 +24,37 @@ interface APIResponse {
|
|||
}
|
||||
|
||||
interface APITranslate {
|
||||
translated_text: string;
|
||||
source_language: string;
|
||||
target_language: string;
|
||||
text: string;
|
||||
source_lang: string;
|
||||
target_lang: string;
|
||||
}
|
||||
|
||||
interface APIAuthor {
|
||||
name?: string;
|
||||
screen_name?: string;
|
||||
avatar_url?: string;
|
||||
avatar_color: string;
|
||||
banner_url?: string;
|
||||
}
|
||||
|
||||
interface APIPoll {}
|
||||
interface APIExternalMedia {
|
||||
type: 'video';
|
||||
url: string;
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
interface APIPollChoice {
|
||||
label: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
interface APIPoll {
|
||||
choices: APIPollChoice[];
|
||||
total_votes: number;
|
||||
ends_at: string;
|
||||
}
|
||||
|
||||
interface APITweet {
|
||||
id: string;
|
||||
|
@ -48,9 +66,14 @@ interface APITweet {
|
|||
retweets: number;
|
||||
replies: number;
|
||||
|
||||
quote_tweet?: APITweet;
|
||||
palette: string;
|
||||
|
||||
quote?: APITweet;
|
||||
poll?: APIPoll;
|
||||
translation?: APITranslate;
|
||||
author: APIAuthor;
|
||||
|
||||
thumbnail: string;
|
||||
media: {
|
||||
external?: APIExternalMedia;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue