xbird-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xbird-mcp might be problematic. Click here for more details.
- package/docs/listing.md +42 -0
- package/docs/mcp-setup.md +146 -0
- package/package.json +50 -0
- package/src/cli/context.ts +80 -0
- package/src/cli/output.ts +33 -0
- package/src/cli/program.ts +63 -0
- package/src/cli.ts +8 -0
- package/src/commands/about.ts +30 -0
- package/src/commands/bookmark.ts +40 -0
- package/src/commands/bookmarks-list.ts +108 -0
- package/src/commands/check.ts +27 -0
- package/src/commands/engagement.ts +31 -0
- package/src/commands/follow.ts +40 -0
- package/src/commands/followers.ts +132 -0
- package/src/commands/home.ts +59 -0
- package/src/commands/likes.ts +73 -0
- package/src/commands/lists.ts +101 -0
- package/src/commands/mentions.ts +32 -0
- package/src/commands/news.ts +38 -0
- package/src/commands/read.ts +36 -0
- package/src/commands/replies.ts +59 -0
- package/src/commands/search.ts +60 -0
- package/src/commands/tweet.ts +128 -0
- package/src/commands/user-tweets.ts +73 -0
- package/src/commands/user.ts +20 -0
- package/src/commands/whoami.ts +26 -0
- package/src/formatters/common.ts +49 -0
- package/src/formatters/list.ts +22 -0
- package/src/formatters/news.ts +23 -0
- package/src/formatters/tweet.ts +38 -0
- package/src/formatters/user.ts +20 -0
- package/src/index.ts +26 -0
- package/src/lib/auth.ts +105 -0
- package/src/lib/client-bookmarks.ts +62 -0
- package/src/lib/client-engagement.ts +152 -0
- package/src/lib/client-follow.ts +90 -0
- package/src/lib/client-full.ts +48 -0
- package/src/lib/client-likes.ts +62 -0
- package/src/lib/client-lists.ts +227 -0
- package/src/lib/client-media.ts +162 -0
- package/src/lib/client-news.ts +185 -0
- package/src/lib/client-posting.ts +163 -0
- package/src/lib/client-reading.ts +452 -0
- package/src/lib/client-search.ts +156 -0
- package/src/lib/client-timeline.ts +98 -0
- package/src/lib/client-user.ts +518 -0
- package/src/lib/client.ts +134 -0
- package/src/lib/config.ts +22 -0
- package/src/lib/constants.ts +55 -0
- package/src/lib/cookies.ts +132 -0
- package/src/lib/features.ts +175 -0
- package/src/lib/headers.ts +28 -0
- package/src/lib/paginate.ts +39 -0
- package/src/lib/query-ids.ts +190 -0
- package/src/lib/types.ts +147 -0
- package/src/lib/utils.ts +176 -0
- package/src/mcp/executor.ts +178 -0
- package/src/mcp/payment.ts +38 -0
- package/src/mcp/server.ts +53 -0
- package/src/mcp/tools.ts +389 -0
- package/src/server/app.ts +117 -0
- package/src/server/config/accounts.ts +137 -0
- package/src/server/config/pricing.ts +217 -0
- package/src/server/erc8004/register.ts +77 -0
- package/src/server/middleware/account-pool.ts +101 -0
- package/src/server/middleware/error-handler.ts +27 -0
- package/src/server/middleware/payer-extract.ts +43 -0
- package/src/server/middleware/x402.ts +61 -0
- package/src/server/routes/accounts.ts +93 -0
- package/src/server/routes/authorize.ts +19 -0
- package/src/server/routes/bookmarks.ts +21 -0
- package/src/server/routes/engagement.ts +84 -0
- package/src/server/routes/follow.ts +32 -0
- package/src/server/routes/health.ts +14 -0
- package/src/server/routes/lists.ts +38 -0
- package/src/server/routes/media.ts +44 -0
- package/src/server/routes/mentions.ts +20 -0
- package/src/server/routes/news.ts +23 -0
- package/src/server/routes/search.ts +26 -0
- package/src/server/routes/timeline.ts +21 -0
- package/src/server/routes/tweets.ts +82 -0
- package/src/server/routes/users.ts +92 -0
- package/src/server/storage/accounts-db.ts +84 -0
- package/src/server.ts +34 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const engagement = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** POST /api/tweets/:id/like */
|
|
7
|
+
engagement.post("/:id/like", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
const id = c.req.param("id");
|
|
10
|
+
|
|
11
|
+
const result = await client.like(id);
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
return c.json({ error: result.error ?? "Failed to like" }, 400);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return c.json({ data: { liked: true } });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** DELETE /api/tweets/:id/like */
|
|
20
|
+
engagement.delete("/:id/like", async (c) => {
|
|
21
|
+
const client = c.get("xClient");
|
|
22
|
+
const id = c.req.param("id");
|
|
23
|
+
|
|
24
|
+
const result = await client.unlike(id);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
return c.json({ error: result.error ?? "Failed to unlike" }, 400);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return c.json({ data: { liked: false } });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** POST /api/tweets/:id/retweet */
|
|
33
|
+
engagement.post("/:id/retweet", async (c) => {
|
|
34
|
+
const client = c.get("xClient");
|
|
35
|
+
const id = c.req.param("id");
|
|
36
|
+
|
|
37
|
+
const result = await client.retweet(id);
|
|
38
|
+
if (!result.success) {
|
|
39
|
+
return c.json({ error: result.error ?? "Failed to retweet" }, 400);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return c.json({ data: { retweeted: true } });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** DELETE /api/tweets/:id/retweet */
|
|
46
|
+
engagement.delete("/:id/retweet", async (c) => {
|
|
47
|
+
const client = c.get("xClient");
|
|
48
|
+
const id = c.req.param("id");
|
|
49
|
+
|
|
50
|
+
const result = await client.unretweet(id);
|
|
51
|
+
if (!result.success) {
|
|
52
|
+
return c.json({ error: result.error ?? "Failed to unretweet" }, 400);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return c.json({ data: { retweeted: false } });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** POST /api/tweets/:id/bookmark */
|
|
59
|
+
engagement.post("/:id/bookmark", async (c) => {
|
|
60
|
+
const client = c.get("xClient");
|
|
61
|
+
const id = c.req.param("id");
|
|
62
|
+
|
|
63
|
+
const result = await client.bookmark(id);
|
|
64
|
+
if (!result.success) {
|
|
65
|
+
return c.json({ error: result.error ?? "Failed to bookmark" }, 400);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return c.json({ data: { bookmarked: true } });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** DELETE /api/tweets/:id/bookmark */
|
|
72
|
+
engagement.delete("/:id/bookmark", async (c) => {
|
|
73
|
+
const client = c.get("xClient");
|
|
74
|
+
const id = c.req.param("id");
|
|
75
|
+
|
|
76
|
+
const result = await client.unbookmark(id);
|
|
77
|
+
if (!result.success) {
|
|
78
|
+
return c.json({ error: result.error ?? "Failed to unbookmark" }, 400);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return c.json({ data: { bookmarked: false } });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export { engagement };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const follow = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** POST /api/users/:handle/follow */
|
|
7
|
+
follow.post("/:handle/follow", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
const handle = c.req.param("handle");
|
|
10
|
+
|
|
11
|
+
const result = await client.follow(handle);
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
return c.json({ error: result.error ?? "Failed to follow" }, 400);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return c.json({ data: { following: true } });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** DELETE /api/users/:handle/follow */
|
|
20
|
+
follow.delete("/:handle/follow", async (c) => {
|
|
21
|
+
const client = c.get("xClient");
|
|
22
|
+
const handle = c.req.param("handle");
|
|
23
|
+
|
|
24
|
+
const result = await client.unfollow(handle);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
return c.json({ error: result.error ?? "Failed to unfollow" }, 400);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return c.json({ data: { following: false } });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export { follow };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const lists = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** GET /api/lists — get owned + membership lists */
|
|
7
|
+
lists.get("/", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
|
|
10
|
+
const [owned, memberships] = await Promise.all([
|
|
11
|
+
client.getOwnedLists(),
|
|
12
|
+
client.getListMemberships(),
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
return c.json({
|
|
16
|
+
data: {
|
|
17
|
+
owned: owned.success ? owned.lists : [],
|
|
18
|
+
memberships: memberships.success ? memberships.lists : [],
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** GET /api/lists/:id/tweets — get list timeline */
|
|
24
|
+
lists.get("/:id/tweets", async (c) => {
|
|
25
|
+
const client = c.get("xClient");
|
|
26
|
+
const id = c.req.param("id");
|
|
27
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
28
|
+
const cursor = c.req.query("cursor");
|
|
29
|
+
|
|
30
|
+
const result = await client.getListTimeline(id, count, cursor);
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
return c.json({ error: result.error ?? "Failed to fetch list timeline" }, 400);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return c.json({ data: result.tweets, cursor: result.cursor });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export { lists };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const media = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** POST /api/media — upload media (multipart/form-data or raw binary) */
|
|
7
|
+
media.post("/", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
10
|
+
|
|
11
|
+
let data: Uint8Array;
|
|
12
|
+
let mimeType: string;
|
|
13
|
+
let alt: string | undefined;
|
|
14
|
+
|
|
15
|
+
if (contentType.includes("multipart/form-data")) {
|
|
16
|
+
const formData = await c.req.formData();
|
|
17
|
+
const file = formData.get("file");
|
|
18
|
+
if (!file || !(file instanceof File)) {
|
|
19
|
+
return c.json({ error: "file field is required (multipart)" }, 400);
|
|
20
|
+
}
|
|
21
|
+
data = new Uint8Array(await file.arrayBuffer());
|
|
22
|
+
mimeType = file.type || "image/png";
|
|
23
|
+
const altField = formData.get("alt");
|
|
24
|
+
alt = altField ? altField.toString() : undefined;
|
|
25
|
+
} else {
|
|
26
|
+
// Raw binary upload — mimeType from Content-Type header
|
|
27
|
+
data = new Uint8Array(await c.req.arrayBuffer());
|
|
28
|
+
mimeType = contentType.split(";")[0]?.trim() || "image/png";
|
|
29
|
+
alt = c.req.header("x-alt-text") ?? undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (data.length === 0) {
|
|
33
|
+
return c.json({ error: "Empty file" }, 400);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = await client.uploadMedia({ data, mimeType, alt });
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
return c.json({ error: result.error ?? "Upload failed" }, 400);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return c.json({ data: { mediaId: result.mediaId } }, 201);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export { media };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const mentions = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** GET /api/mentions/:handle?count=... */
|
|
7
|
+
mentions.get("/:handle", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
const handle = c.req.param("handle");
|
|
10
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
11
|
+
|
|
12
|
+
const result = await client.mentions(handle, count);
|
|
13
|
+
if (!result.success) {
|
|
14
|
+
return c.json({ error: result.error ?? "Failed to fetch mentions" }, 400);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return c.json({ data: result.tweets });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export { mentions };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const news = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** GET /api/news?count=...&tab=...&aiOnly=...&withTweets=...&trendingOnly=... */
|
|
7
|
+
news.get("/", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
10
|
+
const tab = c.req.query("tab");
|
|
11
|
+
const aiOnly = c.req.query("aiOnly") === "true";
|
|
12
|
+
const withTweets = c.req.query("withTweets") === "true";
|
|
13
|
+
const trendingOnly = c.req.query("trendingOnly") === "true";
|
|
14
|
+
|
|
15
|
+
const result = await client.getNews(count, { tab, aiOnly, withTweets, trendingOnly });
|
|
16
|
+
if (!result.success) {
|
|
17
|
+
return c.json({ error: result.error ?? "Failed to fetch news" }, 400);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return c.json({ data: result.items });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export { news };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const search = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** GET /api/search?q=...&count=...&cursor=... */
|
|
7
|
+
search.get("/", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
const q = c.req.query("q");
|
|
10
|
+
|
|
11
|
+
if (!q) {
|
|
12
|
+
return c.json({ error: "q query parameter is required" }, 400);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
16
|
+
const cursor = c.req.query("cursor");
|
|
17
|
+
|
|
18
|
+
const result = await client.search(q, count, cursor);
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
return c.json({ error: result.error ?? "Search failed" }, 400);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return c.json({ data: result.tweets, cursor: result.cursor });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export { search };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const timeline = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** GET /api/timeline/home?count=...&cursor=...&following=true */
|
|
7
|
+
timeline.get("/home", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
10
|
+
const cursor = c.req.query("cursor");
|
|
11
|
+
const following = c.req.query("following") === "true";
|
|
12
|
+
|
|
13
|
+
const result = await client.home(count, cursor, following);
|
|
14
|
+
if (!result.success) {
|
|
15
|
+
return c.json({ error: result.error ?? "Failed to fetch timeline" }, 400);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return c.json({ data: result.tweets, cursor: result.cursor });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export { timeline };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const tweets = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** GET /api/tweets/:id — fetch a single tweet */
|
|
7
|
+
tweets.get("/:id", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
const id = c.req.param("id");
|
|
10
|
+
|
|
11
|
+
const result = await client.getTweet(id);
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
return c.json({ error: result.error ?? "Tweet not found" }, 404);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return c.json({ data: result.tweet });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** GET /api/tweets/:id/thread — fetch tweet thread */
|
|
20
|
+
tweets.get("/:id/thread", async (c) => {
|
|
21
|
+
const client = c.get("xClient");
|
|
22
|
+
const id = c.req.param("id");
|
|
23
|
+
|
|
24
|
+
const result = await client.getThread(id);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
return c.json({ error: result.error ?? "Thread not found" }, 404);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return c.json({ data: result.tweets });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** GET /api/tweets/:id/replies — fetch replies to a tweet */
|
|
33
|
+
tweets.get("/:id/replies", async (c) => {
|
|
34
|
+
const client = c.get("xClient");
|
|
35
|
+
const id = c.req.param("id");
|
|
36
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
37
|
+
const cursor = c.req.query("cursor");
|
|
38
|
+
|
|
39
|
+
const result = await client.getReplies(id, count, cursor);
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
return c.json({ error: result.error ?? "Failed to fetch replies" }, 400);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return c.json({ data: result.tweets, cursor: result.cursor });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** POST /api/tweets — create a new tweet */
|
|
48
|
+
tweets.post("/", async (c) => {
|
|
49
|
+
const client = c.get("xClient");
|
|
50
|
+
const body = await c.req.json<{ text: string; mediaIds?: string[] }>();
|
|
51
|
+
|
|
52
|
+
if (!body.text) {
|
|
53
|
+
return c.json({ error: "text is required" }, 400);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = await client.tweet(body.text, body.mediaIds);
|
|
57
|
+
if (!result.success) {
|
|
58
|
+
return c.json({ error: result.error ?? "Failed to post tweet" }, 400);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return c.json({ data: { tweetId: result.tweetId } }, 201);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** POST /api/tweets/:id/reply — reply to a tweet */
|
|
65
|
+
tweets.post("/:id/reply", async (c) => {
|
|
66
|
+
const client = c.get("xClient");
|
|
67
|
+
const id = c.req.param("id");
|
|
68
|
+
const body = await c.req.json<{ text: string; mediaIds?: string[] }>();
|
|
69
|
+
|
|
70
|
+
if (!body.text) {
|
|
71
|
+
return c.json({ error: "text is required" }, 400);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = await client.reply(body.text, id, body.mediaIds);
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
return c.json({ error: result.error ?? "Failed to reply" }, 400);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return c.json({ data: { tweetId: result.tweetId } }, 201);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export { tweets };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { XbirdEnv } from "../middleware/account-pool.ts";
|
|
3
|
+
|
|
4
|
+
const users = new Hono<XbirdEnv>();
|
|
5
|
+
|
|
6
|
+
/** GET /api/users/:handle — get user profile by handle */
|
|
7
|
+
users.get("/:handle", async (c) => {
|
|
8
|
+
const client = c.get("xClient");
|
|
9
|
+
const handle = c.req.param("handle");
|
|
10
|
+
|
|
11
|
+
const result = await client.getUserByHandle(handle);
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
return c.json({ error: result.error ?? "User not found" }, 404);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return c.json({ data: result.user });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** GET /api/users/:handle/about — get user about info */
|
|
20
|
+
users.get("/:handle/about", async (c) => {
|
|
21
|
+
const client = c.get("xClient");
|
|
22
|
+
const handle = c.req.param("handle");
|
|
23
|
+
|
|
24
|
+
const result = await client.getAbout(handle);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
return c.json({ error: result.error ?? "About info not found" }, 404);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return c.json({ data: result.about });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** GET /api/users/:id/tweets — get user's tweets by user ID */
|
|
33
|
+
users.get("/:id/tweets", async (c) => {
|
|
34
|
+
const client = c.get("xClient");
|
|
35
|
+
const id = c.req.param("id");
|
|
36
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
37
|
+
const cursor = c.req.query("cursor");
|
|
38
|
+
|
|
39
|
+
const result = await client.getUserTweets(id, count, cursor);
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
return c.json({ error: result.error ?? "Failed to fetch tweets" }, 400);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return c.json({ data: result.tweets, cursor: result.cursor });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** GET /api/users/:id/followers — get user's followers */
|
|
48
|
+
users.get("/:id/followers", async (c) => {
|
|
49
|
+
const client = c.get("xClient");
|
|
50
|
+
const id = c.req.param("id");
|
|
51
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
52
|
+
const cursor = c.req.query("cursor");
|
|
53
|
+
|
|
54
|
+
const result = await client.getFollowers(id, count, cursor);
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
return c.json({ error: result.error ?? "Failed to fetch followers" }, 400);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return c.json({ data: result.users, cursor: result.cursor });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** GET /api/users/:id/following — get user's following */
|
|
63
|
+
users.get("/:id/following", async (c) => {
|
|
64
|
+
const client = c.get("xClient");
|
|
65
|
+
const id = c.req.param("id");
|
|
66
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
67
|
+
const cursor = c.req.query("cursor");
|
|
68
|
+
|
|
69
|
+
const result = await client.getFollowing(id, count, cursor);
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
return c.json({ error: result.error ?? "Failed to fetch following" }, 400);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return c.json({ data: result.users, cursor: result.cursor });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** GET /api/users/:id/likes — get user's liked tweets */
|
|
78
|
+
users.get("/:id/likes", async (c) => {
|
|
79
|
+
const client = c.get("xClient");
|
|
80
|
+
const id = c.req.param("id");
|
|
81
|
+
const count = Number(c.req.query("count") ?? 20);
|
|
82
|
+
const cursor = c.req.query("cursor");
|
|
83
|
+
|
|
84
|
+
const result = await client.getLikes(id, count, cursor);
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
return c.json({ error: result.error ?? "Failed to fetch likes" }, 400);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return c.json({ data: result.tweets, cursor: result.cursor });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export { users };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { chmodSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
export interface RegisteredAccount {
|
|
5
|
+
wallet_address: string;
|
|
6
|
+
auth_token: string;
|
|
7
|
+
ct0: string;
|
|
8
|
+
twitter_username: string | null;
|
|
9
|
+
registered_at: number;
|
|
10
|
+
last_used_at: number | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class AccountsDB {
|
|
14
|
+
private db: Database;
|
|
15
|
+
|
|
16
|
+
constructor(dbPath: string = "xbird-accounts.db") {
|
|
17
|
+
this.db = new Database(dbPath, { strict: true });
|
|
18
|
+
this.db.run("PRAGMA journal_mode = WAL");
|
|
19
|
+
this.db.run(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS registered_accounts (
|
|
21
|
+
wallet_address TEXT PRIMARY KEY,
|
|
22
|
+
auth_token TEXT NOT NULL,
|
|
23
|
+
ct0 TEXT NOT NULL,
|
|
24
|
+
twitter_username TEXT,
|
|
25
|
+
registered_at INTEGER NOT NULL,
|
|
26
|
+
last_used_at INTEGER
|
|
27
|
+
)
|
|
28
|
+
`);
|
|
29
|
+
// Restrict DB file permissions to owner-only (0600)
|
|
30
|
+
if (dbPath !== ":memory:") {
|
|
31
|
+
try {
|
|
32
|
+
chmodSync(dbPath, 0o600);
|
|
33
|
+
chmodSync(`${dbPath}-wal`, 0o600);
|
|
34
|
+
chmodSync(`${dbPath}-shm`, 0o600);
|
|
35
|
+
} catch {
|
|
36
|
+
// WAL/SHM files may not exist yet
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Register or update Twitter credentials for a wallet */
|
|
42
|
+
register(wallet: string, authToken: string, ct0: string, username?: string): void {
|
|
43
|
+
wallet = wallet.toLowerCase();
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
this.db.run(
|
|
46
|
+
`INSERT INTO registered_accounts (wallet_address, auth_token, ct0, twitter_username, registered_at, last_used_at)
|
|
47
|
+
VALUES (?, ?, ?, ?, ?, NULL)
|
|
48
|
+
ON CONFLICT(wallet_address) DO UPDATE SET
|
|
49
|
+
auth_token = excluded.auth_token,
|
|
50
|
+
ct0 = excluded.ct0,
|
|
51
|
+
twitter_username = COALESCE(excluded.twitter_username, registered_accounts.twitter_username),
|
|
52
|
+
last_used_at = NULL`,
|
|
53
|
+
[wallet, authToken, ct0, username ?? null, now],
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Look up credentials for a wallet */
|
|
58
|
+
lookup(wallet: string): RegisteredAccount | null {
|
|
59
|
+
return this.db.query<RegisteredAccount, [string]>(
|
|
60
|
+
"SELECT * FROM registered_accounts WHERE wallet_address = ?",
|
|
61
|
+
).get(wallet.toLowerCase());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Remove a registered account */
|
|
65
|
+
remove(wallet: string): boolean {
|
|
66
|
+
const result = this.db.run(
|
|
67
|
+
"DELETE FROM registered_accounts WHERE wallet_address = ?",
|
|
68
|
+
[wallet.toLowerCase()],
|
|
69
|
+
);
|
|
70
|
+
return result.changes > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Touch the last_used_at timestamp */
|
|
74
|
+
updateLastUsed(wallet: string): void {
|
|
75
|
+
this.db.run(
|
|
76
|
+
"UPDATE registered_accounts SET last_used_at = ? WHERE wallet_address = ?",
|
|
77
|
+
[Date.now(), wallet.toLowerCase()],
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
close(): void {
|
|
82
|
+
this.db.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AccountsDB } from "./server/storage/accounts-db.ts";
|
|
2
|
+
import { createApp } from "./server/app.ts";
|
|
3
|
+
import { NETWORK, FACILITATOR_URL } from "./server/config/pricing.ts";
|
|
4
|
+
|
|
5
|
+
const PORT = Number(process.env.PORT ?? 3402);
|
|
6
|
+
const PAY_TO = process.env.XBIRD_PAY_TO_ADDRESS;
|
|
7
|
+
const isMainnet = process.env.XBIRD_NETWORK === "mainnet";
|
|
8
|
+
const DB_PATH = process.env.XBIRD_DB_PATH ?? "xbird-accounts.db";
|
|
9
|
+
|
|
10
|
+
if (!PAY_TO) {
|
|
11
|
+
console.error("XBIRD_PAY_TO_ADDRESS env is required (your USDC wallet on Base)");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (isMainnet && !process.env.CDP_API_KEY_ID) {
|
|
16
|
+
console.warn("WARNING: Mainnet mode requires CDP_API_KEY_ID and CDP_API_KEY_SECRET for the facilitator");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const accountsDb = new AccountsDB(DB_PATH);
|
|
20
|
+
|
|
21
|
+
const app = createApp(PAY_TO, accountsDb);
|
|
22
|
+
|
|
23
|
+
console.log(`xbird API server starting on port ${PORT} (BYOA mode)`);
|
|
24
|
+
console.log(` Network: ${NETWORK} (${isMainnet ? "MAINNET" : "testnet"})`);
|
|
25
|
+
console.log(` Facilitator: ${FACILITATOR_URL}`);
|
|
26
|
+
console.log(` Accounts DB: ${DB_PATH}`);
|
|
27
|
+
console.log(` Pay-to address: ${PAY_TO}`);
|
|
28
|
+
console.log(` Health: http://localhost:${PORT}/health`);
|
|
29
|
+
console.log(` Agent card: http://localhost:${PORT}/.well-known/agent.json`);
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
port: PORT,
|
|
33
|
+
fetch: app.fetch,
|
|
34
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|