mirror of
https://github.com/CompeyDev/fxtwitter-docker.git
synced 2025-04-05 02:20:54 +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 { fetchUsingGuest } from './fetch';
|
||||||
|
import { linkFixer } from './linkFixer';
|
||||||
|
import { colorFromPalette } from './palette';
|
||||||
import { translateTweet } from './translate';
|
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,
|
event: FetchEvent,
|
||||||
status: string,
|
status: string,
|
||||||
language: string
|
language: string
|
||||||
|
@ -37,35 +96,20 @@ export const statueAPI = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: APIResponse = { code: 200, message: 'OK' } as APIResponse;
|
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;
|
let quoteTweet =
|
||||||
const screenName = user?.screen_name || '';
|
conversation.globalObjects?.tweets?.[tweet.quoted_status_id_str || '0'] || null;
|
||||||
const name = user?.name || '';
|
if (quoteTweet) {
|
||||||
|
apiTweet.quote = (await populateTweetProperties(
|
||||||
apiTweet.text = tweet.full_text;
|
quoteTweet,
|
||||||
apiTweet.author = {
|
conversation,
|
||||||
name: name,
|
language
|
||||||
screen_name: screenName,
|
)) as APITweet;
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response.tweet = 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 (
|
export const renderCard = async (
|
||||||
card: TweetCard,
|
card: TweetCard
|
||||||
headers: string[],
|
): Promise<{ poll?: APIPoll; external_media?: APIExternalMedia }> => {
|
||||||
userAgent: string = ''
|
|
||||||
): Promise<string> => {
|
|
||||||
let str = '\n\n';
|
let str = '\n\n';
|
||||||
const values = card.binding_values;
|
const values = card.binding_values;
|
||||||
|
|
||||||
console.log('rendering card on ', card);
|
console.log('rendering card on ', card);
|
||||||
|
|
||||||
// Telegram's bars need to be a lot smaller to fit its bubbles
|
// 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 choices: { [label: string]: number } = {};
|
||||||
let totalVotes = 0;
|
let totalVotes = 0;
|
||||||
let timeLeft = '';
|
let timeLeft = '';
|
||||||
|
@ -68,6 +62,9 @@ export const renderCard = async (
|
||||||
typeof values.choice1_count !== 'undefined' &&
|
typeof values.choice1_count !== 'undefined' &&
|
||||||
typeof values.choice2_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') {
|
if (typeof values.end_datetime_utc !== 'undefined') {
|
||||||
const date = new Date(values.end_datetime_utc.string_value);
|
const date = new Date(values.end_datetime_utc.string_value);
|
||||||
timeLeft = calculateTimeLeftString(date);
|
timeLeft = calculateTimeLeftString(date);
|
||||||
|
@ -89,44 +86,35 @@ export const renderCard = async (
|
||||||
if (typeof values.choice4_count !== 'undefined') {
|
if (typeof values.choice4_count !== 'undefined') {
|
||||||
choices[values.choice4_label?.string_value || ''] = parseInt(
|
choices[values.choice4_label?.string_value || ''] = parseInt(
|
||||||
values.choice4_count.string_value
|
values.choice4_count.string_value
|
||||||
);
|
) || 0;
|
||||||
totalVotes += parseInt(values.choice4_count.string_value);
|
totalVotes += parseInt(values.choice4_count.string_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [label, votes] of Object.entries(choices)) {
|
poll.total_votes = totalVotes;
|
||||||
// render bar
|
poll.choices = Object.keys(choices).map(label => {
|
||||||
const bar = '█'.repeat(Math.round((votes / totalVotes || 0) * barLength));
|
return {
|
||||||
str += `${bar}
|
label: label,
|
||||||
${label} (${Math.round((votes / totalVotes || 0) * 100)}%)
|
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 */
|
/* Oh good, a non-Twitter video URL! This enables YouTube embeds and stuff to just work */
|
||||||
} else if (typeof values.player_url !== 'undefined') {
|
} else if (typeof values.player_url !== 'undefined') {
|
||||||
headers.push(
|
return {
|
||||||
`<meta name="twitter:player" content="${values.player_url.string_value}">`,
|
external_media: {
|
||||||
`<meta name="twitter:player:width" content="${
|
type: 'video',
|
||||||
values.player_width?.string_value || '1280'
|
url: values.player_url.string_value,
|
||||||
}">`,
|
width: parseInt(
|
||||||
`<meta name="twitter:player:height" content="${
|
(values.player_width?.string_value || '1280').replace('px', '')
|
||||||
values.player_height?.string_value || '720'
|
), // TODO: Replacing px might not be necessary, it's just there as a precaution
|
||||||
}">`,
|
height: parseInt(
|
||||||
`<meta property="og:type" content="video.other">`,
|
(values.player_height?.string_value || '720').replace('px', '')
|
||||||
`<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 {};
|
||||||
return str;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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/status/:id/:language', statusRequest);
|
||||||
router.get('/:prefix?/:handle/statuses/:id/:language', statusRequest);
|
router.get('/:prefix?/:handle/statuses/:id/:language', statusRequest);
|
||||||
router.get('/status/:id', statusRequest);
|
router.get('/status/:id', statusRequest);
|
||||||
|
router.get('/status/:id/:language', statusRequest);
|
||||||
|
|
||||||
router.get('/owoembed', async (request: Request) => {
|
router.get('/owoembed', async (request: Request) => {
|
||||||
console.log('oembed hit!');
|
console.log('oembed hit!');
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Strings } from './strings';
|
||||||
import { handleMosaic } from './mosaic';
|
import { handleMosaic } from './mosaic';
|
||||||
import { translateTweet } from './translate';
|
import { translateTweet } from './translate';
|
||||||
import { getAuthorText } from './author';
|
import { getAuthorText } from './author';
|
||||||
import { statueAPI } from './api';
|
import { statusAPI } from './api';
|
||||||
|
|
||||||
export const returnError = (error: string): StatusResponse => {
|
export const returnError = (error: string): StatusResponse => {
|
||||||
return {
|
return {
|
||||||
|
@ -33,7 +33,7 @@ export const handleStatus = async (
|
||||||
): Promise<StatusResponse> => {
|
): Promise<StatusResponse> => {
|
||||||
console.log('Direct?', flags?.direct);
|
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) {
|
if (flags?.api || true) {
|
||||||
return {
|
return {
|
||||||
|
|
35
src/types.d.ts
vendored
35
src/types.d.ts
vendored
|
@ -24,19 +24,37 @@ interface APIResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APITranslate {
|
interface APITranslate {
|
||||||
translated_text: string;
|
text: string;
|
||||||
source_language: string;
|
source_lang: string;
|
||||||
target_language: string;
|
target_lang: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APIAuthor {
|
interface APIAuthor {
|
||||||
name?: string;
|
name?: string;
|
||||||
screen_name?: string;
|
screen_name?: string;
|
||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
|
avatar_color: string;
|
||||||
banner_url?: 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 {
|
interface APITweet {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -48,9 +66,14 @@ interface APITweet {
|
||||||
retweets: number;
|
retweets: number;
|
||||||
replies: number;
|
replies: number;
|
||||||
|
|
||||||
quote_tweet?: APITweet;
|
palette: string;
|
||||||
|
|
||||||
|
quote?: APITweet;
|
||||||
|
poll?: APIPoll;
|
||||||
translation?: APITranslate;
|
translation?: APITranslate;
|
||||||
author: APIAuthor;
|
author: APIAuthor;
|
||||||
|
|
||||||
thumbnail: string;
|
media: {
|
||||||
|
external?: APIExternalMedia;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue