tangled
alpha
login
or
join now
yoginth.com
/
hey
1
fork
atom
Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿
1
fork
atom
overview
issues
pulls
pipelines
Remove redis
yoginth.com
5 months ago
ccb1248d
24d79606
verified
This commit was signed with the committer's
known signature
.
yoginth.com
SSH Key Fingerprint:
SHA256:SLCGp+xtY+FtXnVKtpl4bpmTttAxnxJ3DBCeikAHlG4=
+18
-568
18 changed files
expand all
collapse all
unified
split
apps
api
.env.example
env.d.ts
package.json
src
collects.ts
index.ts
likes.ts
posts.ts
utils
discordQueue.ts
redis.ts
workers
discord
rateLimit.ts
webhook.ts
discordWebhook.ts
web
src
components
Post
Actions
Like.tsx
Share
Repost.tsx
OpenAction
CollectAction
CollectActionButton.tsx
helpers
fetcher.ts
hooks
useCreatePost.tsx
pnpm-lock.yaml
-5
apps/api/.env.example
···
4
4
EVER_ACCESS_KEY=""
5
5
EVER_ACCESS_SECRET=""
6
6
SHARED_SECRET=""
7
7
-
EVENTS_DISCORD_WEBHOOK_URL=""
8
8
-
PAGEVIEWS_DISCORD_WEBHOOK_URL=""
9
9
-
COLLECTS_DISCORD_WEBHOOK_URL=""
10
10
-
LIKES_DISCORD_WEBHOOK_URL=""
11
11
-
REDIS_URL=""
-3
apps/api/env.d.ts
···
6
6
EVER_ACCESS_KEY: string;
7
7
EVER_ACCESS_SECRET: string;
8
8
SHARED_SECRET: string;
9
9
-
EVENTS_DISCORD_WEBHOOK_URL: string;
10
10
-
COLLECTS_DISCORD_WEBHOOK_URL: string;
11
11
-
LIKES_DISCORD_WEBHOOK_URL: string;
12
9
}
13
10
}
-1
apps/api/package.json
···
21
21
"dotenv": "^17.2.2",
22
22
"hono": "^4.9.9",
23
23
"hono-rate-limiter": "^0.4.2",
24
24
-
"ioredis": "^5.8.0",
25
24
"jose": "^6.1.0",
26
25
"linkedom": "^0.18.12",
27
26
"pg-promise": "^12.1.3",
-45
apps/api/src/collects.ts
···
1
1
-
import { Status } from "@hey/data/enums";
2
2
-
import { withPrefix } from "@hey/helpers/logger";
3
3
-
import type { Context } from "hono";
4
4
-
import enqueueDiscordWebhook from "./utils/discordQueue";
5
5
-
6
6
-
const log = withPrefix("[API]");
7
7
-
8
8
-
interface CollectsBody {
9
9
-
slug?: string;
10
10
-
}
11
11
-
12
12
-
const collects = async (ctx: Context) => {
13
13
-
let body: CollectsBody = {};
14
14
-
try {
15
15
-
body = (await ctx.req.json()) as CollectsBody;
16
16
-
} catch {
17
17
-
body = {};
18
18
-
}
19
19
-
20
20
-
const host = ctx.req.header("host") ?? "";
21
21
-
22
22
-
if (host.includes("localhost")) {
23
23
-
return ctx.json({
24
24
-
data: { ok: true, skipped: true },
25
25
-
status: Status.Success
26
26
-
});
27
27
-
}
28
28
-
29
29
-
try {
30
30
-
const item = {
31
31
-
createdAt: Date.now(),
32
32
-
kind: "collect" as const,
33
33
-
payload: { slug: body.slug },
34
34
-
retries: 0
35
35
-
};
36
36
-
37
37
-
void enqueueDiscordWebhook(item);
38
38
-
} catch (err) {
39
39
-
log.error("Failed to enqueue collect webhook", err as Error);
40
40
-
}
41
41
-
42
42
-
return ctx.json({ data: { ok: true }, status: Status.Success });
43
43
-
};
44
44
-
45
45
-
export default collects;
-29
apps/api/src/index.ts
···
3
3
import { Status } from "@hey/data/enums";
4
4
import { withPrefix } from "@hey/helpers/logger";
5
5
import { Hono } from "hono";
6
6
-
import collects from "./collects";
7
6
import authContext from "./context/authContext";
8
8
-
import likes from "./likes";
9
9
-
import authMiddleware from "./middlewares/authMiddleware";
10
7
import cors from "./middlewares/cors";
11
11
-
import rateLimiter from "./middlewares/rateLimiter";
12
12
-
import posts from "./posts";
13
8
import cronRouter from "./routes/cron";
14
9
import metadataRouter from "./routes/metadata";
15
10
import oembedRouter from "./routes/oembed";
16
11
import ogRouter from "./routes/og";
17
12
import ping from "./routes/ping";
18
18
-
import {
19
19
-
startDiscordWebhookWorkerCollects,
20
20
-
startDiscordWebhookWorkerLikes,
21
21
-
startDiscordWebhookWorkerPosts
22
22
-
} from "./workers/discordWebhook";
23
13
24
14
const log = withPrefix("[API]");
25
15
···
33
23
app.route("/metadata", metadataRouter);
34
24
app.route("/oembed", oembedRouter);
35
25
app.route("/og", ogRouter);
36
36
-
app.post("/posts", rateLimiter({ requests: 10 }), authMiddleware, posts);
37
37
-
app.post("/likes", rateLimiter({ requests: 20 }), authMiddleware, likes);
38
38
-
app.post("/collects", rateLimiter({ requests: 20 }), authMiddleware, collects);
39
26
40
27
app.notFound((ctx) =>
41
28
ctx.json({ error: "Not Found", status: Status.Error }, 404)
···
44
31
serve({ fetch: app.fetch, port: 4784 }, (info) => {
45
32
log.info(`Server running on port ${info.port}`);
46
33
});
47
47
-
48
48
-
if (process.env.REDIS_URL) {
49
49
-
const hasEvents = !!process.env.EVENTS_DISCORD_WEBHOOK_URL;
50
50
-
const hasLikes = !!process.env.LIKES_DISCORD_WEBHOOK_URL;
51
51
-
const hasCollects = !!process.env.COLLECTS_DISCORD_WEBHOOK_URL;
52
52
-
53
53
-
if (hasEvents) void startDiscordWebhookWorkerPosts();
54
54
-
if (hasLikes) void startDiscordWebhookWorkerLikes();
55
55
-
if (hasCollects) void startDiscordWebhookWorkerCollects();
56
56
-
57
57
-
if (!hasEvents && !hasLikes && !hasCollects) {
58
58
-
log.warn("Discord workers not started: missing webhook envs");
59
59
-
}
60
60
-
} else {
61
61
-
log.warn("Discord workers not started: missing Redis env");
62
62
-
}
-45
apps/api/src/likes.ts
···
1
1
-
import { Status } from "@hey/data/enums";
2
2
-
import { withPrefix } from "@hey/helpers/logger";
3
3
-
import type { Context } from "hono";
4
4
-
import enqueueDiscordWebhook from "./utils/discordQueue";
5
5
-
6
6
-
const log = withPrefix("[API]");
7
7
-
8
8
-
interface LikesBody {
9
9
-
slug?: string;
10
10
-
}
11
11
-
12
12
-
const likes = async (ctx: Context) => {
13
13
-
let body: LikesBody = {};
14
14
-
try {
15
15
-
body = (await ctx.req.json()) as LikesBody;
16
16
-
} catch {
17
17
-
body = {};
18
18
-
}
19
19
-
20
20
-
const host = ctx.req.header("host") ?? "";
21
21
-
22
22
-
if (host.includes("localhost")) {
23
23
-
return ctx.json({
24
24
-
data: { ok: true, skipped: true },
25
25
-
status: Status.Success
26
26
-
});
27
27
-
}
28
28
-
29
29
-
try {
30
30
-
const item = {
31
31
-
createdAt: Date.now(),
32
32
-
kind: "like" as const,
33
33
-
payload: { slug: body.slug },
34
34
-
retries: 0
35
35
-
};
36
36
-
37
37
-
void enqueueDiscordWebhook(item);
38
38
-
} catch (err) {
39
39
-
log.error("Failed to enqueue like webhook", err as Error);
40
40
-
}
41
41
-
42
42
-
return ctx.json({ data: { ok: true }, status: Status.Success });
43
43
-
};
44
44
-
45
45
-
export default likes;
-46
apps/api/src/posts.ts
···
1
1
-
import { Status } from "@hey/data/enums";
2
2
-
import { withPrefix } from "@hey/helpers/logger";
3
3
-
import type { Context } from "hono";
4
4
-
import enqueueDiscordWebhook from "./utils/discordQueue";
5
5
-
6
6
-
const log = withPrefix("[API]");
7
7
-
8
8
-
interface PostsBody {
9
9
-
slug?: string;
10
10
-
type?: string;
11
11
-
}
12
12
-
13
13
-
const posts = async (ctx: Context) => {
14
14
-
let body: PostsBody = {};
15
15
-
try {
16
16
-
body = (await ctx.req.json()) as PostsBody;
17
17
-
} catch {
18
18
-
body = {};
19
19
-
}
20
20
-
21
21
-
const host = ctx.req.header("host") ?? "";
22
22
-
23
23
-
if (host.includes("localhost")) {
24
24
-
return ctx.json({
25
25
-
data: { ok: true, skipped: true },
26
26
-
status: Status.Success
27
27
-
});
28
28
-
}
29
29
-
30
30
-
try {
31
31
-
const item = {
32
32
-
createdAt: Date.now(),
33
33
-
kind: "post" as const,
34
34
-
payload: { slug: body.slug, type: body.type },
35
35
-
retries: 0
36
36
-
};
37
37
-
38
38
-
void enqueueDiscordWebhook(item);
39
39
-
} catch (err) {
40
40
-
log.error("Failed to enqueue post webhook", err as Error);
41
41
-
}
42
42
-
43
43
-
return ctx.json({ data: { ok: true }, status: Status.Success });
44
44
-
};
45
45
-
46
46
-
export default posts;
-50
apps/api/src/utils/discordQueue.ts
···
1
1
-
import { withPrefix } from "@hey/helpers/logger";
2
2
-
import {
3
3
-
DISCORD_QUEUE_COLLECTS,
4
4
-
DISCORD_QUEUE_LIKES,
5
5
-
DISCORD_QUEUE_POSTS,
6
6
-
getRedis
7
7
-
} from "./redis";
8
8
-
9
9
-
export interface DiscordQueueItemBase {
10
10
-
createdAt: number;
11
11
-
retries?: number;
12
12
-
}
13
13
-
14
14
-
export interface PostQueueItem extends DiscordQueueItemBase {
15
15
-
kind: "post";
16
16
-
payload: { slug?: string; type?: string };
17
17
-
}
18
18
-
19
19
-
export interface LikeQueueItem extends DiscordQueueItemBase {
20
20
-
kind: "like";
21
21
-
payload: { slug?: string };
22
22
-
}
23
23
-
24
24
-
export interface CollectQueueItem extends DiscordQueueItemBase {
25
25
-
kind: "collect";
26
26
-
payload: { slug?: string };
27
27
-
}
28
28
-
29
29
-
export type DiscordQueueItem = PostQueueItem | LikeQueueItem | CollectQueueItem;
30
30
-
31
31
-
export const enqueueDiscordWebhook = async (
32
32
-
item: DiscordQueueItem
33
33
-
): Promise<void> => {
34
34
-
const log = withPrefix("[API]");
35
35
-
try {
36
36
-
const redis = getRedis();
37
37
-
const key =
38
38
-
item.kind === "post"
39
39
-
? DISCORD_QUEUE_POSTS
40
40
-
: item.kind === "like"
41
41
-
? DISCORD_QUEUE_LIKES
42
42
-
: DISCORD_QUEUE_COLLECTS;
43
43
-
await redis.rpush(key, JSON.stringify(item));
44
44
-
log.info(`Enqueued discord webhook: ${item.kind}`);
45
45
-
} catch (err) {
46
46
-
log.error("Failed to enqueue discord webhook", err as Error);
47
47
-
}
48
48
-
};
49
49
-
50
50
-
export default enqueueDiscordWebhook;
-19
apps/api/src/utils/redis.ts
···
1
1
-
import IORedis from "ioredis";
2
2
-
3
3
-
let redisClient: IORedis | null = null;
4
4
-
5
5
-
export const DISCORD_QUEUE_POSTS = "hey:discord:webhooks:posts";
6
6
-
export const DISCORD_QUEUE_LIKES = "hey:discord:webhooks:likes";
7
7
-
export const DISCORD_QUEUE_COLLECTS = "hey:discord:webhooks:collects";
8
8
-
9
9
-
export const getRedis = (): IORedis => {
10
10
-
if (redisClient) return redisClient;
11
11
-
12
12
-
const url = process.env.REDIS_URL;
13
13
-
if (!url) {
14
14
-
throw new Error("Redis not configured. Set REDIS_URL");
15
15
-
}
16
16
-
17
17
-
redisClient = new IORedis(url);
18
18
-
return redisClient;
19
19
-
};
-68
apps/api/src/workers/discord/rateLimit.ts
···
1
1
-
import type IORedis from "ioredis";
2
2
-
3
3
-
export const DELAYED_QUEUE_KEY = "hey:discord:webhooks:delayed" as const;
4
4
-
5
5
-
declare global {
6
6
-
var __heyWebhookNextAt: Map<string, number> | undefined;
7
7
-
}
8
8
-
9
9
-
const nextAtMap = (): Map<string, number> =>
10
10
-
(globalThis.__heyWebhookNextAt ??= new Map<string, number>());
11
11
-
12
12
-
export const getWaitMs = (webhookUrl: string, now = Date.now()): number => {
13
13
-
const until = nextAtMap().get(webhookUrl) ?? 0;
14
14
-
return until > now ? until - now : 0;
15
15
-
};
16
16
-
17
17
-
export const setNextIn = (webhookUrl: string, delayMs: number): number => {
18
18
-
const until = Date.now() + Math.max(0, Math.ceil(delayMs));
19
19
-
const map = nextAtMap();
20
20
-
const prev = map.get(webhookUrl) ?? 0;
21
21
-
const value = Math.max(prev, until);
22
22
-
map.set(webhookUrl, value);
23
23
-
return value;
24
24
-
};
25
25
-
26
26
-
export const updateFromHeaders = (webhookUrl: string, res: Response): void => {
27
27
-
const remaining = Number.parseInt(
28
28
-
res.headers.get("x-ratelimit-remaining") ?? "NaN",
29
29
-
10
30
30
-
);
31
31
-
const resetAfterSec = Number.parseFloat(
32
32
-
res.headers.get("x-ratelimit-reset-after") ?? "NaN"
33
33
-
);
34
34
-
if (
35
35
-
Number.isFinite(remaining) &&
36
36
-
remaining <= 0 &&
37
37
-
Number.isFinite(resetAfterSec)
38
38
-
) {
39
39
-
setNextIn(webhookUrl, resetAfterSec * 1000);
40
40
-
}
41
41
-
};
42
42
-
43
43
-
export const promoteDue = async (
44
44
-
r: IORedis,
45
45
-
key: string,
46
46
-
limit = 100
47
47
-
): Promise<number> => {
48
48
-
const now = Date.now();
49
49
-
const due = await r.zrangebyscore(key, 0, now, "LIMIT", 0, limit);
50
50
-
if (due.length === 0) return 0;
51
51
-
const multi = r.multi();
52
52
-
for (const value of due) {
53
53
-
multi.zrem(key, value);
54
54
-
multi.rpush(key.replace(":delayed", ""), value);
55
55
-
}
56
56
-
await multi.exec();
57
57
-
return due.length;
58
58
-
};
59
59
-
60
60
-
export const schedule = async (
61
61
-
r: IORedis,
62
62
-
key: string,
63
63
-
item: unknown,
64
64
-
delayMs: number
65
65
-
): Promise<void> => {
66
66
-
const ts = Date.now() + Math.max(0, Math.ceil(delayMs));
67
67
-
await r.zadd(key, String(ts), JSON.stringify(item));
68
68
-
};
-46
apps/api/src/workers/discord/webhook.ts
···
1
1
-
import type {
2
2
-
CollectQueueItem,
3
3
-
DiscordQueueItem,
4
4
-
LikeQueueItem,
5
5
-
PostQueueItem
6
6
-
} from "../../utils/discordQueue";
7
7
-
8
8
-
export type WebhookDetails = { webhookUrl?: string; body: unknown };
9
9
-
10
10
-
const postContent = (payload: PostQueueItem["payload"]) => {
11
11
-
const postUrl = payload.slug ? `https://hey.xyz/posts/${payload.slug}` : "";
12
12
-
const type = payload.type ?? "post";
13
13
-
return { content: `New ${type} on Hey ${postUrl}`.trim() };
14
14
-
};
15
15
-
16
16
-
const likeContent = (payload: LikeQueueItem["payload"]) => {
17
17
-
const postUrl = payload.slug ? `https://hey.xyz/posts/${payload.slug}` : "";
18
18
-
return { content: `New like on Hey ${postUrl}`.trim() };
19
19
-
};
20
20
-
21
21
-
const collectContent = (payload: CollectQueueItem["payload"]) => {
22
22
-
const postUrl = payload.slug ? `https://hey.xyz/posts/${payload.slug}` : "";
23
23
-
return { content: `New collect on Hey ${postUrl}`.trim() };
24
24
-
};
25
25
-
26
26
-
export const resolveWebhook = (item: DiscordQueueItem): WebhookDetails => {
27
27
-
if (item.kind === "post") {
28
28
-
return {
29
29
-
body: postContent(item.payload),
30
30
-
webhookUrl: process.env.EVENTS_DISCORD_WEBHOOK_URL
31
31
-
};
32
32
-
}
33
33
-
if (item.kind === "collect") {
34
34
-
return {
35
35
-
body: collectContent((item as CollectQueueItem).payload),
36
36
-
webhookUrl: process.env.COLLECTS_DISCORD_WEBHOOK_URL
37
37
-
};
38
38
-
}
39
39
-
if (item.kind === "like") {
40
40
-
return {
41
41
-
body: likeContent((item as any).payload),
42
42
-
webhookUrl: process.env.LIKES_DISCORD_WEBHOOK_URL
43
43
-
};
44
44
-
}
45
45
-
return { body: {}, webhookUrl: undefined };
46
46
-
};
-165
apps/api/src/workers/discordWebhook.ts
···
1
1
-
import { withPrefix } from "@hey/helpers/logger";
2
2
-
import type IORedis from "ioredis";
3
3
-
import type { DiscordQueueItem } from "../utils/discordQueue";
4
4
-
import {
5
5
-
DISCORD_QUEUE_COLLECTS,
6
6
-
DISCORD_QUEUE_LIKES,
7
7
-
DISCORD_QUEUE_POSTS,
8
8
-
getRedis
9
9
-
} from "../utils/redis";
10
10
-
import {
11
11
-
getWaitMs,
12
12
-
promoteDue,
13
13
-
schedule,
14
14
-
setNextIn,
15
15
-
updateFromHeaders
16
16
-
} from "./discord/rateLimit";
17
17
-
import { resolveWebhook } from "./discord/webhook";
18
18
-
19
19
-
const log = withPrefix("[Worker]");
20
20
-
21
21
-
const sleep = (ms: number): Promise<void> =>
22
22
-
new Promise((res) => setTimeout(res, ms));
23
23
-
24
24
-
const parseItem = (raw: unknown): DiscordQueueItem | null => {
25
25
-
if (!raw) return null;
26
26
-
try {
27
27
-
const item =
28
28
-
typeof raw === "string"
29
29
-
? (JSON.parse(raw) as DiscordQueueItem)
30
30
-
: (raw as DiscordQueueItem);
31
31
-
return item && "kind" in item ? item : null;
32
32
-
} catch (e) {
33
33
-
log.error("Failed to parse queue item", e as Error);
34
34
-
return null;
35
35
-
}
36
36
-
};
37
37
-
38
38
-
const dispatch = async (item: DiscordQueueItem) => {
39
39
-
const { webhookUrl, body } = resolveWebhook(item);
40
40
-
if (!webhookUrl) {
41
41
-
log.warn(`Skipping ${item.kind} webhook: missing webhook URL env`);
42
42
-
return { status: 0, webhookUrl: undefined as string | undefined };
43
43
-
}
44
44
-
const res = await fetch(webhookUrl, {
45
45
-
body: JSON.stringify(body),
46
46
-
headers: { "content-type": "application/json" },
47
47
-
method: "POST"
48
48
-
});
49
49
-
return { res, status: res.status, webhookUrl } as const;
50
50
-
};
51
51
-
52
52
-
const startQueueWorker = async (queueKey: string, label: string) => {
53
53
-
let redis: IORedis;
54
54
-
try {
55
55
-
redis = getRedis();
56
56
-
} catch {
57
57
-
log.warn(`Discord worker (${label}) disabled: Redis not configured`);
58
58
-
return;
59
59
-
}
60
60
-
61
61
-
log.info(`Discord worker started (${label}). Queue: ${queueKey}`);
62
62
-
63
63
-
// eslint-disable-next-line no-constant-condition
64
64
-
while (true) {
65
65
-
try {
66
66
-
const delayedKey = `${queueKey}:delayed`;
67
67
-
await promoteDue(redis, delayedKey, 100);
68
68
-
69
69
-
const res = (await redis.brpop(queueKey, 1)) as [string, string] | null;
70
70
-
if (!res) continue;
71
71
-
72
72
-
const [, raw] = res;
73
73
-
const item = parseItem(raw);
74
74
-
if (!item) continue;
75
75
-
76
76
-
const { webhookUrl } = resolveWebhook(item);
77
77
-
if (!webhookUrl) {
78
78
-
log.warn(`Skipping ${item.kind} webhook: missing webhook URL env`);
79
79
-
continue;
80
80
-
}
81
81
-
82
82
-
// If this webhook has a pending cooldown, re-schedule it individually
83
83
-
const waitMs = getWaitMs(webhookUrl);
84
84
-
if (waitMs > 0) {
85
85
-
await schedule(redis, delayedKey, item, waitMs);
86
86
-
log.warn(
87
87
-
`Cooldown for ${item.kind}. Scheduled after ${Math.ceil(waitMs / 1000)}s`
88
88
-
);
89
89
-
continue;
90
90
-
}
91
91
-
// Reserve 1 req/s slot pre-dispatch to avoid concurrent sends for same URL
92
92
-
setNextIn(webhookUrl, 1000);
93
93
-
94
94
-
try {
95
95
-
const result = await dispatch(item);
96
96
-
const { webhookUrl: url, status, res } = result;
97
97
-
98
98
-
if (!url) continue;
99
99
-
100
100
-
if (status === 429) {
101
101
-
// Parse retry-after headers/body; fallback to 10s min
102
102
-
const resetAfter = Number.parseFloat(
103
103
-
res?.headers.get("x-ratelimit-reset-after") ?? "NaN"
104
104
-
);
105
105
-
const retryAfterHeader = Number.parseFloat(
106
106
-
res?.headers.get("retry-after") ?? "NaN"
107
107
-
);
108
108
-
let retryAfterSec = Number.isFinite(resetAfter)
109
109
-
? resetAfter
110
110
-
: Number.isFinite(retryAfterHeader)
111
111
-
? retryAfterHeader
112
112
-
: Number.NaN;
113
113
-
if (!Number.isFinite(retryAfterSec)) {
114
114
-
const payload = (await res?.json().catch(() => null)) as {
115
115
-
retry_after?: number | string;
116
116
-
} | null;
117
117
-
const bodyRetry = payload?.retry_after;
118
118
-
retryAfterSec =
119
119
-
typeof bodyRetry === "number"
120
120
-
? bodyRetry
121
121
-
: Number.parseFloat(String(bodyRetry ?? "NaN"));
122
122
-
}
123
123
-
if (!Number.isFinite(retryAfterSec)) retryAfterSec = 10;
124
124
-
retryAfterSec = Math.max(10, retryAfterSec);
125
125
-
126
126
-
const until = setNextIn(url, retryAfterSec * 1000);
127
127
-
const ms = Math.max(0, until - Date.now());
128
128
-
await schedule(redis, delayedKey, item, ms);
129
129
-
log.warn(
130
130
-
`Rate limited for ${item.kind}. Scheduled after ${Math.ceil(ms / 1000)}s`
131
131
-
);
132
132
-
continue;
133
133
-
}
134
134
-
135
135
-
if (!res?.ok) {
136
136
-
const text = await res?.text().catch(() => "");
137
137
-
throw new Error(`Discord webhook failed (${status}): ${text}`);
138
138
-
}
139
139
-
140
140
-
// Success: update from headers if provided
141
141
-
updateFromHeaders(url, res);
142
142
-
log.info(`Dispatched Discord webhook: ${item.kind}`);
143
143
-
} catch (_err) {
144
144
-
const retries = (item.retries ?? 0) + 1;
145
145
-
if (retries <= 3) {
146
146
-
item.retries = retries;
147
147
-
await redis.rpush(queueKey, JSON.stringify(item));
148
148
-
log.warn(`Requeued ${item.kind} webhook (attempt ${retries})`);
149
149
-
} else {
150
150
-
log.error(`Dropped ${item.kind} webhook after ${retries} attempts`);
151
151
-
}
152
152
-
}
153
153
-
} catch (e) {
154
154
-
log.error("Discord worker loop error", e as Error);
155
155
-
await sleep(1000);
156
156
-
}
157
157
-
}
158
158
-
};
159
159
-
160
160
-
export const startDiscordWebhookWorkerPosts = async () =>
161
161
-
startQueueWorker(DISCORD_QUEUE_POSTS, "posts");
162
162
-
export const startDiscordWebhookWorkerLikes = async () =>
163
163
-
startQueueWorker(DISCORD_QUEUE_LIKES, "likes");
164
164
-
export const startDiscordWebhookWorkerCollects = async () =>
165
165
-
startQueueWorker(DISCORD_QUEUE_COLLECTS, "collects");
+2
-6
apps/web/src/components/Post/Actions/Like.tsx
···
16
16
import { Tooltip } from "@/components/Shared/UI";
17
17
import cn from "@/helpers/cn";
18
18
import errorToast from "@/helpers/errorToast";
19
19
-
import { hono } from "@/helpers/fetcher";
20
19
import { useAccountStore } from "@/store/persisted/useAccountStore";
21
20
22
21
interface LikeProps {
···
92
91
}
93
92
94
93
increment();
95
95
-
const res = await addReaction({
94
94
+
95
95
+
return await addReaction({
96
96
variables: {
97
97
request: { post: post.id, reaction: PostReactionType.Upvote }
98
98
}
99
99
});
100
100
-
101
101
-
void hono.likes.create({ slug: post.slug });
102
102
-
103
103
-
return res;
104
100
};
105
101
106
102
const iconClassName = showCount
-3
apps/web/src/components/Post/Actions/Share/Repost.tsx
···
9
9
import { toast } from "sonner";
10
10
import cn from "@/helpers/cn";
11
11
import errorToast from "@/helpers/errorToast";
12
12
-
import { hono } from "@/helpers/fetcher";
13
12
import useTransactionLifecycle from "@/hooks/useTransactionLifecycle";
14
13
import { useAccountStore } from "@/store/persisted/useAccountStore";
15
14
···
34
33
if (!post.operations) {
35
34
return;
36
35
}
37
37
-
38
38
-
void hono.posts.create({ slug: post.slug, type: "Repost" });
39
36
40
37
cache.modify({
41
38
fields: {
-3
apps/web/src/components/Post/OpenAction/CollectAction/CollectActionButton.tsx
···
14
14
import LoginButton from "@/components/Shared/LoginButton";
15
15
import { Button, Spinner } from "@/components/Shared/UI";
16
16
import errorToast from "@/helpers/errorToast";
17
17
-
import { hono } from "@/helpers/fetcher";
18
17
import getCollectActionData from "@/helpers/getCollectActionData";
19
18
import useTransactionLifecycle from "@/hooks/useTransactionLifecycle";
20
19
import { useAccountStore } from "@/store/persisted/useAccountStore";
···
79
78
onCollectSuccess?.();
80
79
updateCache();
81
80
toast.success("Collected successfully");
82
82
-
83
83
-
void hono.collects.create({ slug: post.slug });
84
81
};
85
82
86
83
const onError = useCallback((error: ApolloClientError) => {
-21
apps/web/src/helpers/fetcher.ts
···
62
62
};
63
63
64
64
export const hono = {
65
65
-
collects: {
66
66
-
create: async (payload: { slug: string }) =>
67
67
-
fetchApi<{ ok: boolean; skipped?: boolean }>("/collects", {
68
68
-
body: JSON.stringify(payload),
69
69
-
method: "POST"
70
70
-
})
71
71
-
},
72
65
events: {
73
66
create: async (payload: { event: string }) =>
74
67
fetch("https://yoginth.com/api/hey/events", {
···
80
73
method: "POST"
81
74
})
82
75
},
83
83
-
likes: {
84
84
-
create: async (payload: { slug: string }) =>
85
85
-
fetchApi<{ ok: boolean; skipped?: boolean }>("/likes", {
86
86
-
body: JSON.stringify(payload),
87
87
-
method: "POST"
88
88
-
})
89
89
-
},
90
76
metadata: {
91
77
sts: (): Promise<STS> => {
92
78
return fetchApi<STS>("/metadata/sts", { method: "GET" });
···
96
82
get: (url: string): Promise<Oembed> => {
97
83
return fetchApi<Oembed>(`/oembed/get?url=${url}`, { method: "GET" });
98
84
}
99
99
-
},
100
100
-
posts: {
101
101
-
create: async (payload: { slug: string; type?: string }) =>
102
102
-
fetchApi<{ ok: boolean; skipped?: boolean }>("/posts", {
103
103
-
body: JSON.stringify(payload),
104
104
-
method: "POST"
105
105
-
})
106
85
}
107
86
};
-3
apps/web/src/hooks/useCreatePost.tsx
···
9
9
import { useCallback } from "react";
10
10
import { useNavigate } from "react-router";
11
11
import { toast } from "sonner";
12
12
-
import { hono } from "@/helpers/fetcher";
13
12
import useTransactionLifecycle from "./useTransactionLifecycle";
14
13
import useWaitForTransactionToComplete from "./useWaitForTransactionToComplete";
15
14
···
43
42
}
44
43
45
44
const type = isComment ? "Comment" : "Post";
46
46
-
47
47
-
void hono.posts.create({ slug: data.post.slug, type });
48
45
49
46
toast.success(`${type} created successfully!`, {
50
47
action: {
+16
-10
pnpm-lock.yaml
···
53
53
hono-rate-limiter:
54
54
specifier: ^0.4.2
55
55
version: 0.4.2(hono@4.9.9)
56
56
-
ioredis:
57
57
-
specifier: ^5.8.0
58
58
-
version: 5.8.0
59
56
jose:
60
57
specifier: ^6.1.0
61
58
version: 6.1.0
···
8234
8231
optionalDependencies:
8235
8232
'@types/node': 24.5.2
8236
8233
8237
8237
-
'@ioredis/commands@1.4.0': {}
8234
8234
+
'@ioredis/commands@1.4.0':
8235
8235
+
optional: true
8238
8236
8239
8237
'@isaacs/fs-minipass@4.0.1':
8240
8238
dependencies:
···
11188
11186
11189
11187
clsx@2.1.1: {}
11190
11188
11191
11191
-
cluster-key-slot@1.1.2: {}
11189
11189
+
cluster-key-slot@1.1.2:
11190
11190
+
optional: true
11192
11191
11193
11192
color-convert@2.0.1:
11194
11193
dependencies:
···
11321
11320
11322
11321
defu@6.1.4: {}
11323
11322
11324
11324
-
denque@2.1.0: {}
11323
11323
+
denque@2.1.0:
11324
11324
+
optional: true
11325
11325
11326
11326
dependency-graph@0.11.0: {}
11327
11327
···
11985
11985
standard-as-callback: 2.1.0
11986
11986
transitivePeerDependencies:
11987
11987
- supports-color
11988
11988
+
optional: true
11988
11989
11989
11990
iron-webcrypto@1.2.1: {}
11990
11991
···
12225
12226
dependencies:
12226
12227
p-locate: 4.1.0
12227
12228
12228
12228
-
lodash.defaults@4.2.0: {}
12229
12229
+
lodash.defaults@4.2.0:
12230
12230
+
optional: true
12229
12231
12230
12230
-
lodash.isarguments@3.1.0: {}
12232
12232
+
lodash.isarguments@3.1.0:
12233
12233
+
optional: true
12231
12234
12232
12235
lodash.sortby@4.7.0: {}
12233
12236
···
13254
13257
13255
13258
real-require@0.1.0: {}
13256
13259
13257
13257
-
redis-errors@1.2.0: {}
13260
13260
+
redis-errors@1.2.0:
13261
13261
+
optional: true
13258
13262
13259
13263
redis-parser@3.0.0:
13260
13264
dependencies:
13261
13265
redis-errors: 1.2.0
13266
13266
+
optional: true
13262
13267
13263
13268
regex-recursion@6.0.2:
13264
13269
dependencies:
···
13536
13541
dependencies:
13537
13542
tslib: 2.8.1
13538
13543
13539
13539
-
standard-as-callback@2.1.0: {}
13544
13544
+
standard-as-callback@2.1.0:
13545
13545
+
optional: true
13540
13546
13541
13547
stream-browserify@3.0.0:
13542
13548
dependencies: