Skip to content

Commit 149a84a

Browse files
committed
better twitter support
1 parent ae2fe4c commit 149a84a

File tree

4 files changed

+228
-61
lines changed

4 files changed

+228
-61
lines changed

cobalt.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { z } from "zod";
2+
import { USER_AGENT } from "./consts";
3+
4+
export const CobaltResult = z.discriminatedUnion("status", [
5+
z.object({
6+
status: z.literal("error"),
7+
error: z.object({
8+
code: z.string(),
9+
context: z
10+
.object({
11+
service: z.string().optional(),
12+
limit: z.number().optional(),
13+
})
14+
.optional(),
15+
}),
16+
}),
17+
z.object({
18+
status: z.literal("picker"),
19+
audio: z.string().optional(),
20+
audioFilename: z.string().optional(),
21+
picker: z.array(
22+
z.object({
23+
type: z.enum(["photo", "video", "gif"]),
24+
url: z.string(),
25+
thumb: z.string().optional(),
26+
})
27+
),
28+
}),
29+
z.object({
30+
status: z.enum(["tunnel", "redirect"]),
31+
url: z.string(),
32+
filename: z.string(),
33+
}),
34+
]);
35+
export type CobaltResult = z.infer<typeof CobaltResult>;
36+
export type VideoQuality =
37+
| "144"
38+
| "240"
39+
| "360"
40+
| "480"
41+
| "720"
42+
| "1080"
43+
| "1440"
44+
| "2160"
45+
| "4320"
46+
| "max";
47+
export async function askCobalt(
48+
url: string,
49+
options?: {
50+
videoQuality?: VideoQuality;
51+
}
52+
) {
53+
const response = await fetch(`https://dorsiblancoapicobalt.nulo.in/`, {
54+
method: "POST",
55+
body: JSON.stringify({ url, ...options }),
56+
headers: {
57+
"Content-Type": "application/json",
58+
Accept: "application/json",
59+
"User-Agent": USER_AGENT,
60+
},
61+
});
62+
const data = await response.json();
63+
return CobaltResult.parse(data);
64+
}

consts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const USER_AGENT = "dlbot/1.0 (+https://nulo.lol/dlbot)";

fxtwitter.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { z } from "zod";
2+
import { USER_AGENT } from "./consts";
3+
4+
export const FxtwitterResult = z.object({
5+
code: z.number(),
6+
message: z.string(),
7+
tweet: z.object({
8+
url: z.string(),
9+
text: z.string(),
10+
created_at: z.string(),
11+
created_timestamp: z.number(),
12+
author: z.object({
13+
name: z.string(),
14+
screen_name: z.string(),
15+
avatar_url: z.string(),
16+
avatar_color: z.string().nullable(),
17+
banner_url: z.string(),
18+
}),
19+
replies: z.number(),
20+
retweets: z.number(),
21+
likes: z.number(),
22+
views: z.number(),
23+
color: z.string().nullable(),
24+
twitter_card: z.string().optional(),
25+
lang: z.string(),
26+
source: z.string(),
27+
replying_to: z.any(),
28+
replying_to_status: z.any(),
29+
quote: z
30+
.object({
31+
text: z.string(),
32+
author: z.object({
33+
name: z.string(),
34+
screen_name: z.string(),
35+
}),
36+
})
37+
.optional(),
38+
media: z
39+
.object({
40+
all: z
41+
.array(
42+
z.object({
43+
type: z.enum(["video", "gif", "photo"]),
44+
url: z.string(),
45+
thumbnail_url: z.string().optional(),
46+
width: z.number(),
47+
height: z.number(),
48+
duration: z.number().optional(),
49+
format: z.string().optional(),
50+
})
51+
)
52+
.optional(),
53+
external: z
54+
.object({
55+
type: z.literal("video"),
56+
url: z.string(),
57+
height: z.number(),
58+
width: z.number(),
59+
duration: z.number(),
60+
})
61+
.optional(),
62+
photos: z
63+
.array(
64+
z.object({
65+
type: z.literal("photo"),
66+
url: z.string(),
67+
width: z.number(),
68+
height: z.number(),
69+
})
70+
)
71+
.optional(),
72+
videos: z
73+
.array(
74+
z.object({
75+
type: z.enum(["video", "gif"]),
76+
url: z.string(),
77+
thumbnail_url: z.string(),
78+
width: z.number(),
79+
height: z.number(),
80+
duration: z.number(),
81+
format: z.string(),
82+
})
83+
)
84+
.optional(),
85+
mosaic: z
86+
.object({
87+
type: z.literal("mosaic_photo"),
88+
width: z.number().optional(),
89+
height: z.number().optional(),
90+
formats: z.object({
91+
webp: z.string(),
92+
jpeg: z.string(),
93+
}),
94+
})
95+
.optional(),
96+
})
97+
.optional(),
98+
}),
99+
});
100+
101+
export async function askFxtwitter(
102+
screenName: string,
103+
id: string,
104+
translateTo?: string
105+
) {
106+
const url = `https://api.fxtwitter.com/${screenName}/status/${id}/${translateTo}`;
107+
const response = await fetch(url, {
108+
headers: {
109+
"User-Agent": USER_AGENT,
110+
},
111+
});
112+
const json = await response.json();
113+
console.debug("fxtwitter res", JSON.stringify(json));
114+
if (response.status !== 200) {
115+
throw new Error(`Fxtwitter returned status ${response.status}`);
116+
}
117+
return FxtwitterResult.parse(json);
118+
}

index.ts

Lines changed: 45 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import TelegramBot from "node-telegram-bot-api";
22
import { Readable, type Stream } from "stream";
33
import { z } from "zod";
4+
import { askCobalt, CobaltResult } from "./cobalt";
5+
import { askFxtwitter } from "./fxtwitter";
46

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

13-
const CobaltResult = z.discriminatedUnion("status", [
14-
z.object({
15-
status: z.literal("error"),
16-
error: z.object({
17-
code: z.string(),
18-
context: z
19-
.object({
20-
service: z.string().optional(),
21-
limit: z.number().optional(),
22-
})
23-
.optional(),
24-
}),
25-
}),
26-
z.object({
27-
status: z.literal("picker"),
28-
audio: z.string().optional(),
29-
audioFilename: z.string().optional(),
30-
picker: z.array(
31-
z.object({
32-
type: z.enum(["photo", "video", "gif"]),
33-
url: z.string(),
34-
thumb: z.string().optional(),
35-
})
36-
),
37-
}),
38-
z.object({
39-
status: z.enum(["tunnel", "redirect"]),
40-
url: z.string(),
41-
filename: z.string(),
42-
}),
43-
]);
44-
type CobaltResult = z.infer<typeof CobaltResult>;
45-
type VideoQuality =
46-
| "144"
47-
| "240"
48-
| "360"
49-
| "480"
50-
| "720"
51-
| "1080"
52-
| "1440"
53-
| "2160"
54-
| "4320"
55-
| "max";
56-
async function askCobalt(
57-
url: string,
58-
options?: {
59-
videoQuality?: VideoQuality;
60-
}
61-
) {
62-
const response = await fetch(`https://dorsiblancoapicobalt.nulo.in/`, {
63-
method: "POST",
64-
body: JSON.stringify({ url, ...options }),
65-
headers: {
66-
"Content-Type": "application/json",
67-
Accept: "application/json",
68-
},
69-
});
70-
const data = await response.json();
71-
return CobaltResult.parse(data);
72-
}
73-
7415
class Bot {
7516
private bot: TelegramBot;
7617
constructor(token: string) {
@@ -124,6 +65,49 @@ class Bot {
12465

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

68+
if (
69+
parsedUrl.hostname === "twitter.com" ||
70+
parsedUrl.hostname === "x.com"
71+
) {
72+
try {
73+
const pathParts = parsedUrl.pathname.split("/");
74+
const statusIndex = pathParts.indexOf("status");
75+
if (statusIndex !== -1 && statusIndex + 1 < pathParts.length) {
76+
const screenName = pathParts[1];
77+
const tweetId = pathParts[statusIndex + 1];
78+
const fxResult = await askFxtwitter(screenName, tweetId);
79+
hasDownloadables = true;
80+
await this.bot.sendMessage(
81+
chatId,
82+
`${fxResult.tweet.author.name} (@${
83+
fxResult.tweet.author.screen_name
84+
}):\n<blockquote>${fxResult.tweet.text}${
85+
fxResult.tweet.quote
86+
? `</blockquote>\nQuoting: ${fxResult.tweet.quote.author.name} (@${fxResult.tweet.quote.author.screen_name}):\n<blockquote>${fxResult.tweet.quote.text}`
87+
: ""
88+
}</blockquote>\nhttps://fxtwitter.com/${screenName}/status/${tweetId}`,
89+
{ reply_to_message_id: msg.message_id, parse_mode: "HTML" }
90+
);
91+
if (fxResult.tweet.media?.all?.length) {
92+
await this.bot.sendMediaGroup(
93+
chatId,
94+
fxResult.tweet.media?.all?.map((media) => ({
95+
type: media.type === "gif" ? "photo" : media.type,
96+
media: media.url,
97+
thumb: media.thumbnail_url,
98+
})) ?? [],
99+
{
100+
reply_to_message_id: msg.message_id,
101+
}
102+
);
103+
}
104+
continue;
105+
}
106+
} catch (error) {
107+
console.error("Failed to fetch from fxtwitter:", error);
108+
}
109+
}
110+
127111
const cobaltResult = await askCobalt(parsedUrl.href);
128112
console.log(JSON.stringify(cobaltResult));
129113
if (cobaltResult.status === "error") {

0 commit comments

Comments
 (0)