Skip to content

Commit

Permalink
better twitter support
Browse files Browse the repository at this point in the history
  • Loading branch information
catdevnull committed Nov 17, 2024
1 parent ae2fe4c commit 149a84a
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 61 deletions.
64 changes: 64 additions & 0 deletions cobalt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { z } from "zod";
import { USER_AGENT } from "./consts";

export const CobaltResult = z.discriminatedUnion("status", [
z.object({
status: z.literal("error"),
error: z.object({
code: z.string(),
context: z
.object({
service: z.string().optional(),
limit: z.number().optional(),
})
.optional(),
}),
}),
z.object({
status: z.literal("picker"),
audio: z.string().optional(),
audioFilename: z.string().optional(),
picker: z.array(
z.object({
type: z.enum(["photo", "video", "gif"]),
url: z.string(),
thumb: z.string().optional(),
})
),
}),
z.object({
status: z.enum(["tunnel", "redirect"]),
url: z.string(),
filename: z.string(),
}),
]);
export type CobaltResult = z.infer<typeof CobaltResult>;
export type VideoQuality =
| "144"
| "240"
| "360"
| "480"
| "720"
| "1080"
| "1440"
| "2160"
| "4320"
| "max";
export async function askCobalt(
url: string,
options?: {
videoQuality?: VideoQuality;
}
) {
const response = await fetch(`https://dorsiblancoapicobalt.nulo.in/`, {
method: "POST",
body: JSON.stringify({ url, ...options }),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": USER_AGENT,
},
});
const data = await response.json();
return CobaltResult.parse(data);
}
1 change: 1 addition & 0 deletions consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const USER_AGENT = "dlbot/1.0 (+https://nulo.lol/dlbot)";
118 changes: 118 additions & 0 deletions fxtwitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { z } from "zod";
import { USER_AGENT } from "./consts";

export const FxtwitterResult = z.object({
code: z.number(),
message: z.string(),
tweet: z.object({
url: z.string(),
text: z.string(),
created_at: z.string(),
created_timestamp: z.number(),
author: z.object({
name: z.string(),
screen_name: z.string(),
avatar_url: z.string(),
avatar_color: z.string().nullable(),
banner_url: z.string(),
}),
replies: z.number(),
retweets: z.number(),
likes: z.number(),
views: z.number(),
color: z.string().nullable(),
twitter_card: z.string().optional(),
lang: z.string(),
source: z.string(),
replying_to: z.any(),
replying_to_status: z.any(),
quote: z
.object({
text: z.string(),
author: z.object({
name: z.string(),
screen_name: z.string(),
}),
})
.optional(),
media: z
.object({
all: z
.array(
z.object({
type: z.enum(["video", "gif", "photo"]),
url: z.string(),
thumbnail_url: z.string().optional(),
width: z.number(),
height: z.number(),
duration: z.number().optional(),
format: z.string().optional(),
})
)
.optional(),
external: z
.object({
type: z.literal("video"),
url: z.string(),
height: z.number(),
width: z.number(),
duration: z.number(),
})
.optional(),
photos: z
.array(
z.object({
type: z.literal("photo"),
url: z.string(),
width: z.number(),
height: z.number(),
})
)
.optional(),
videos: z
.array(
z.object({
type: z.enum(["video", "gif"]),
url: z.string(),
thumbnail_url: z.string(),
width: z.number(),
height: z.number(),
duration: z.number(),
format: z.string(),
})
)
.optional(),
mosaic: z
.object({
type: z.literal("mosaic_photo"),
width: z.number().optional(),
height: z.number().optional(),
formats: z.object({
webp: z.string(),
jpeg: z.string(),
}),
})
.optional(),
})
.optional(),
}),
});

export async function askFxtwitter(
screenName: string,
id: string,
translateTo?: string
) {
const url = `https://api.fxtwitter.com/${screenName}/status/${id}/${translateTo}`;
const response = await fetch(url, {
headers: {
"User-Agent": USER_AGENT,
},
});
const json = await response.json();
console.debug("fxtwitter res", JSON.stringify(json));
if (response.status !== 200) {
throw new Error(`Fxtwitter returned status ${response.status}`);
}
return FxtwitterResult.parse(json);
}
106 changes: 45 additions & 61 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import TelegramBot from "node-telegram-bot-api";
import { Readable, type Stream } from "stream";
import { z } from "zod";
import { askCobalt, CobaltResult } from "./cobalt";
import { askFxtwitter } from "./fxtwitter";

// https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#file-options-metadata
process.env.NTBA_FIX_350 = "false";
Expand All @@ -10,67 +12,6 @@ const botParams = {
baseApiUrl: process.env.TELEGRAM_API_URL,
};

const CobaltResult = z.discriminatedUnion("status", [
z.object({
status: z.literal("error"),
error: z.object({
code: z.string(),
context: z
.object({
service: z.string().optional(),
limit: z.number().optional(),
})
.optional(),
}),
}),
z.object({
status: z.literal("picker"),
audio: z.string().optional(),
audioFilename: z.string().optional(),
picker: z.array(
z.object({
type: z.enum(["photo", "video", "gif"]),
url: z.string(),
thumb: z.string().optional(),
})
),
}),
z.object({
status: z.enum(["tunnel", "redirect"]),
url: z.string(),
filename: z.string(),
}),
]);
type CobaltResult = z.infer<typeof CobaltResult>;
type VideoQuality =
| "144"
| "240"
| "360"
| "480"
| "720"
| "1080"
| "1440"
| "2160"
| "4320"
| "max";
async function askCobalt(
url: string,
options?: {
videoQuality?: VideoQuality;
}
) {
const response = await fetch(`https://dorsiblancoapicobalt.nulo.in/`, {
method: "POST",
body: JSON.stringify({ url, ...options }),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
const data = await response.json();
return CobaltResult.parse(data);
}

class Bot {
private bot: TelegramBot;
constructor(token: string) {
Expand Down Expand Up @@ -124,6 +65,49 @@ class Bot {

console.log(`Descargando ${parsedUrl.href}`);

if (
parsedUrl.hostname === "twitter.com" ||
parsedUrl.hostname === "x.com"
) {
try {
const pathParts = parsedUrl.pathname.split("/");
const statusIndex = pathParts.indexOf("status");
if (statusIndex !== -1 && statusIndex + 1 < pathParts.length) {
const screenName = pathParts[1];
const tweetId = pathParts[statusIndex + 1];
const fxResult = await askFxtwitter(screenName, tweetId);
hasDownloadables = true;
await this.bot.sendMessage(
chatId,
`${fxResult.tweet.author.name} (@${
fxResult.tweet.author.screen_name
}):\n<blockquote>${fxResult.tweet.text}${
fxResult.tweet.quote
? `</blockquote>\nQuoting: ${fxResult.tweet.quote.author.name} (@${fxResult.tweet.quote.author.screen_name}):\n<blockquote>${fxResult.tweet.quote.text}`
: ""
}</blockquote>\nhttps://fxtwitter.com/${screenName}/status/${tweetId}`,
{ reply_to_message_id: msg.message_id, parse_mode: "HTML" }
);
if (fxResult.tweet.media?.all?.length) {
await this.bot.sendMediaGroup(
chatId,
fxResult.tweet.media?.all?.map((media) => ({
type: media.type === "gif" ? "photo" : media.type,
media: media.url,
thumb: media.thumbnail_url,
})) ?? [],
{
reply_to_message_id: msg.message_id,
}
);
}
continue;
}
} catch (error) {
console.error("Failed to fetch from fxtwitter:", error);
}
}

const cobaltResult = await askCobalt(parsedUrl.href);
console.log(JSON.stringify(cobaltResult));
if (cobaltResult.status === "error") {
Expand Down

0 comments on commit 149a84a

Please sign in to comment.