tangled
alpha
login
or
join now
socksthewolf.com
/
skyscheduler
2
fork
atom
Schedule posts to Bluesky with Cloudflare workers.
skyscheduler.work
cf
tool
bsky-tool
cloudflare
bluesky
schedule
bsky
service
social-media
cloudflare-workers
2
fork
atom
overview
issues
pulls
pipelines
Optimize routing
closes #90 and #89
SocksTheWolf
3 weeks ago
2b76260b
64689bf5
+130
-82
6 changed files
expand all
collapse all
unified
split
src
endpoints
admin.tsx
index.tsx
types.d.ts
utils
inviteKeys.ts
wrangler.d.ts
wrangler.toml
+59
src/endpoints/admin.tsx
···
1
1
+
import { Hono } from "hono";
2
2
+
import { secureHeaders } from "hono/secure-headers";
3
3
+
import { ContextVariables } from "../auth";
4
4
+
import { authAdminOnlyMiddleware } from "../middleware/adminOnly";
5
5
+
import { Bindings } from "../types.d";
6
6
+
import { getAllAbandonedMedia } from "../utils/db/file";
7
7
+
import { runMaintenanceUpdates } from "../utils/db/maintain";
8
8
+
import { makeInviteKey } from "../utils/inviteKeys";
9
9
+
import { corsHelperMiddleware } from "../middleware/corsHelper";
10
10
+
import { cleanupAbandonedFiles, cleanUpPostsTask, schedulePostTask } from "../utils/scheduler";
11
11
+
12
12
+
export const admin = new Hono<{ Bindings: Bindings, Variables: ContextVariables }>();
13
13
+
14
14
+
admin.use(secureHeaders());
15
15
+
admin.use(corsHelperMiddleware);
16
16
+
17
17
+
// Generate invites route
18
18
+
admin.get("/invite", authAdminOnlyMiddleware, (c) => {
19
19
+
const newKey = makeInviteKey(c);
20
20
+
if (newKey !== null)
21
21
+
return c.text(`${newKey} is good for ${c.env.SIGNUP_SETTINGS.invite_uses} uses`);
22
22
+
else
23
23
+
return c.text("Invite keys are disabled.");
24
24
+
});
25
25
+
26
26
+
// Admin Maintenance Cleanup
27
27
+
admin.get("/cron", authAdminOnlyMiddleware, async (c) => {
28
28
+
await schedulePostTask(c);
29
29
+
return c.text("ran");
30
30
+
});
31
31
+
32
32
+
admin.get("/cron-clean", authAdminOnlyMiddleware, (c) => {
33
33
+
c.executionCtx.waitUntil(cleanUpPostsTask(c));
34
34
+
return c.text("ran");
35
35
+
});
36
36
+
37
37
+
admin.get("/db-update", authAdminOnlyMiddleware, (c) => {
38
38
+
c.executionCtx.waitUntil(runMaintenanceUpdates(c));
39
39
+
return c.text("ran");
40
40
+
});
41
41
+
42
42
+
admin.get("/abandoned", authAdminOnlyMiddleware, async (c) => {
43
43
+
let returnHTML = "";
44
44
+
const abandonedFiles: string[] = await getAllAbandonedMedia(c);
45
45
+
// print out all abandoned files
46
46
+
for (const file of abandonedFiles) {
47
47
+
returnHTML += `${file}\n`;
48
48
+
}
49
49
+
if (c.env.R2_SETTINGS.auto_prune == true) {
50
50
+
console.log("pruning abandoned files...");
51
51
+
await cleanupAbandonedFiles(c);
52
52
+
}
53
53
+
54
54
+
if (returnHTML.length == 0) {
55
55
+
returnHTML = "no files abandoned";
56
56
+
}
57
57
+
58
58
+
return c.text(returnHTML);
59
59
+
});
+28
-69
src/index.tsx
···
2
2
import { Env, Hono } from "hono";
3
3
import { ContextVariables, createAuth } from "./auth";
4
4
import { account } from "./endpoints/account";
5
5
+
import { admin } from "./endpoints/admin";
5
6
import { post } from "./endpoints/post";
6
7
import { preview } from "./endpoints/preview";
7
7
-
import { authAdminOnlyMiddleware } from "./middleware/adminOnly";
8
8
import { authMiddleware } from "./middleware/auth";
9
9
import { corsHelperMiddleware } from "./middleware/corsHelper";
10
10
import { redirectToDashIfLogin } from "./middleware/redirectDash";
···
19
19
import { Bindings, QueueTaskData, ScheduledContext, TaskType } from "./types.d";
20
20
import { AgentMap } from "./utils/bskyAgents";
21
21
import { makeConstScript } from "./utils/constScriptGen";
22
22
-
import { getAllAbandonedMedia } from "./utils/db/file";
23
23
-
import { runMaintenanceUpdates } from "./utils/db/maintain";
24
24
-
import { makeInviteKey } from "./utils/inviteKeys";
25
22
import {
26
26
-
cleanupAbandonedFiles, cleanUpPostsTask, handlePostTask,
23
23
+
cleanUpPostsTask, handlePostTask,
27
24
handleRepostTask, schedulePostTask
28
25
} from "./utils/scheduler";
29
26
import { setupAccounts } from "./utils/setup";
30
27
31
28
const app = new Hono<{ Bindings: Bindings, Variables: ContextVariables }>();
32
29
30
30
+
///// Static Pages /////
31
31
+
32
32
+
// Root route
33
33
+
app.all("/", (c) => c.html(<Home />));
34
34
+
35
35
+
// JS injection of const variables
36
36
+
app.get("/js/consts.js", (c) => {
37
37
+
const constScript = makeConstScript();
38
38
+
return c.body(constScript, 200, {
39
39
+
'Content-Type': 'text/javascript',
40
40
+
'Cache-Control': 'max-age=604800'
41
41
+
});
42
42
+
});
43
43
+
44
44
+
// Add redirects
45
45
+
app.all("/contact", (c) => c.redirect(c.env.REDIRECTS.contact));
46
46
+
app.all("/tip", (c) => c.redirect(c.env.REDIRECTS.tip));
47
47
+
48
48
+
// Legal linkies
49
49
+
app.get("/tos", (c) => c.html(<TermsOfService />));
50
50
+
app.get("/privacy", (c) => c.html(<PrivacyPolicy />));
51
51
+
33
52
///// Inline Middleware /////
34
53
// CORS configuration for auth routes
35
54
app.use("/api/auth/**", corsHelperMiddleware);
···
56
75
app.use("/post/**", corsHelperMiddleware);
57
76
app.route("/post", post);
58
77
78
78
+
// Admin endpoints
79
79
+
app.use("/admin/**", corsHelperMiddleware);
80
80
+
app.route("/admin", admin);
81
81
+
59
82
// Image preview endpoint
60
83
app.use("/preview/**", corsHelperMiddleware);
61
84
app.route("/preview", preview);
62
85
63
63
-
// Root route
64
64
-
app.all("/", (c) => c.html(<Home />));
65
65
-
66
66
-
// JS injection of const variables
67
67
-
app.get("/js/consts.js", (c) => {
68
68
-
const constScript = makeConstScript();
69
69
-
return c.body(constScript, 200, {
70
70
-
'Content-Type': 'text/javascript',
71
71
-
'Cache-Control': 'max-age=604800'
72
72
-
});
73
73
-
});
74
74
-
75
75
-
// Add redirects
76
76
-
app.all("/contact", (c) => c.redirect(c.env.REDIRECTS.contact));
77
77
-
app.all("/tip", (c) => c.redirect(c.env.REDIRECTS.tip));
78
78
-
79
79
-
// Legal linkies
80
80
-
app.get("/tos", (c) => c.html(<TermsOfService />));
81
81
-
app.get("/privacy", (c) => c.html(<PrivacyPolicy />));
82
82
-
83
86
// Dashboard route
84
87
app.get("/dashboard", authMiddleware, (c) => c.html(<Dashboard c={c} />));
85
88
···
94
97
95
98
// Reset Password route
96
99
app.get("/reset", redirectToDashIfLogin, (c) => c.html(<ResetPassword />));
97
97
-
98
98
-
// Generate invites route
99
99
-
app.get("/invite", authAdminOnlyMiddleware, (c) => {
100
100
-
const newKey = makeInviteKey(c);
101
101
-
if (newKey !== null)
102
102
-
return c.text(`${newKey} is good for 10 uses`);
103
103
-
else
104
104
-
return c.text("Invite keys are disabled.");
105
105
-
});
106
106
-
107
107
-
// Admin Maintenance Cleanup
108
108
-
app.get("/cron", authAdminOnlyMiddleware, async (c) => {
109
109
-
await schedulePostTask(c);
110
110
-
return c.text("ran");
111
111
-
});
112
112
-
113
113
-
app.get("/cron-clean", authAdminOnlyMiddleware, (c) => {
114
114
-
c.executionCtx.waitUntil(cleanUpPostsTask(c));
115
115
-
return c.text("ran");
116
116
-
});
117
117
-
118
118
-
app.get("/db-update", authAdminOnlyMiddleware, (c) => {
119
119
-
c.executionCtx.waitUntil(runMaintenanceUpdates(c));
120
120
-
return c.text("ran");
121
121
-
});
122
122
-
123
123
-
app.get("/abandoned", authAdminOnlyMiddleware, async (c) => {
124
124
-
let returnHTML = "";
125
125
-
const abandonedFiles: string[] = await getAllAbandonedMedia(c);
126
126
-
// print out all abandoned files
127
127
-
for (const file of abandonedFiles) {
128
128
-
returnHTML += `${file}\n`;
129
129
-
}
130
130
-
if (c.env.R2_SETTINGS.auto_prune == true) {
131
131
-
console.log("pruning abandoned files...");
132
132
-
await cleanupAbandonedFiles(c);
133
133
-
}
134
134
-
135
135
-
if (returnHTML.length == 0) {
136
136
-
returnHTML = "no files abandoned";
137
137
-
}
138
138
-
139
139
-
return c.text(returnHTML);
140
140
-
});
141
100
142
101
// Startup Application
143
102
app.get("/start", (c) => c.redirect('/setup'));
+1
src/types.d.ts
···
13
13
use_captcha: boolean;
14
14
invite_only: boolean;
15
15
invite_thread?: string;
16
16
+
invite_uses: number;
16
17
}
17
18
18
19
type RedirectConfigSettings = {
+5
-1
src/utils/inviteKeys.ts
···
76
76
return null;
77
77
}
78
78
79
79
+
const usages: number = c.env.SIGNUP_SETTINGS.invite_uses;
80
80
+
if (usages === undefined)
81
81
+
return null;
82
82
+
79
83
const newKey: string = humanId({
80
84
separator: '-',
81
85
capitalize: false,
82
86
});
83
83
-
c.executionCtx.waitUntil(c.env.INVITE_POOL!.put(newKey, "10"));
87
87
+
c.executionCtx.waitUntil(c.env.INVITE_POOL!.put(newKey, usages));
84
88
return newKey;
85
89
}
+34
-9
src/wrangler.d.ts
···
1
1
/* eslint-disable */
2
2
-
// Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: 55b4346bed1553dbff2b8c9d511b4b56)
3
3
-
// Runtime types generated with workerd@1.20260212.0 2024-12-13 nodejs_compat
2
2
+
// Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: ef02624a1e49d0409702d04e46795d86)
3
3
+
// Runtime types generated with workerd@1.20260212.0 2025-11-18 disable_ctx_exports,disable_nodejs_http_server_modules,nodejs_compat,nodejs_compat_do_not_populate_process_env
4
4
declare namespace Cloudflare {
5
5
interface GlobalProps {
6
6
mainModule: typeof import("./index");
···
11
11
DB: D1Database;
12
12
IMAGES: ImagesBinding;
13
13
IMAGE_SETTINGS: {"enabled":false};
14
14
-
SIGNUP_SETTINGS: {"use_captcha":false,"invite_only":false,"invite_thread":""};
14
14
+
SIGNUP_SETTINGS: {"use_captcha":false,"invite_only":false,"invite_thread":"","invite_uses":10};
15
15
QUEUE_SETTINGS: {"enabled":false,"repostsEnabled":false,"postNowEnabled":false,"threadEnabled":true,"delay_val":100,"post_queues":["POST_QUEUE"],"repost_queues":[]};
16
16
REDIRECTS: {"contact":"https://bsky.app/profile/skyscheduler.work","tip":"https://ko-fi.com/socksthewolf/tip"};
17
17
R2_SETTINGS: {"auto_prune":false,"prune_days":3};
18
18
-
TASK_SETTINGS: {"use_posts":false,"use_reposts":false};
18
18
+
TASK_SETTINGS: {"use_posts":true,"use_reposts":true};
19
19
BETTER_AUTH_SECRET: string;
20
20
BETTER_AUTH_URL: string;
21
21
DEFAULT_ADMIN_USER: string;
···
45
45
DB: D1Database;
46
46
IMAGES: ImagesBinding;
47
47
IMAGE_SETTINGS: {"enabled":false} | {"enabled":true,"steps":[95,85,75],"bucket_url":"https://resize.skyscheduler.work/"};
48
48
-
SIGNUP_SETTINGS: {"use_captcha":false,"invite_only":false,"invite_thread":""} | {"use_captcha":true,"invite_only":false,"invite_thread":""};
48
48
+
SIGNUP_SETTINGS: {"use_captcha":false,"invite_only":false,"invite_thread":"","invite_uses":10} | {"use_captcha":true,"invite_only":false,"invite_thread":"","invite_uses":10};
49
49
QUEUE_SETTINGS: {"enabled":false,"repostsEnabled":false,"postNowEnabled":false,"threadEnabled":true,"delay_val":100,"post_queues":["POST_QUEUE"],"repost_queues":[]} | {"enabled":true,"repostsEnabled":true,"postNowEnabled":false,"threadEnabled":true,"delay_val":100,"post_queues":["POST_QUEUE"],"repost_queues":["REPOST_QUEUE"]};
50
50
REDIRECTS: {"contact":"https://bsky.app/profile/skyscheduler.work","tip":"https://ko-fi.com/socksthewolf/tip"};
51
51
R2_SETTINGS: {"auto_prune":false,"prune_days":3} | {"auto_prune":true,"prune_days":3};
52
52
-
TASK_SETTINGS: {"use_posts":false,"use_reposts":false};
52
52
+
TASK_SETTINGS: {"use_posts":true,"use_reposts":true};
53
53
INVITE_POOL?: KVNamespace;
54
54
R2RESIZE?: R2Bucket;
55
55
DBStaging?: D1Database;
···
375
375
ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;
376
376
CountQueuingStrategy: typeof CountQueuingStrategy;
377
377
ErrorEvent: typeof ErrorEvent;
378
378
+
MessageChannel: typeof MessageChannel;
379
379
+
MessagePort: typeof MessagePort;
378
380
EventSource: typeof EventSource;
379
381
ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;
380
382
ReadableStreamDefaultController: typeof ReadableStreamDefaultController;
···
503
505
sendBeacon(url: string, body?: BodyInit): boolean;
504
506
readonly userAgent: string;
505
507
readonly hardwareConcurrency: number;
508
508
+
readonly language: string;
509
509
+
readonly languages: string[];
506
510
}
507
511
interface AlarmInvocationInfo {
508
512
readonly isRetry: boolean;
···
1822
1826
*
1823
1827
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/cache)
1824
1828
*/
1825
1825
-
cache?: "no-store";
1829
1829
+
cache?: "no-store" | "no-cache";
1826
1830
}
1827
1831
interface RequestInit<Cf = CfProperties> {
1828
1832
/* A string to set request's method. */
···
1836
1840
fetcher?: (Fetcher | null);
1837
1841
cf?: Cf;
1838
1842
/* A string indicating how the request will interact with the browser's cache to set request's cache. */
1839
1839
-
cache?: "no-store";
1843
1843
+
cache?: "no-store" | "no-cache";
1840
1844
/* A cryptographic hash of the resource to be fetched by request. Sets request's integrity. */
1841
1845
integrity?: string;
1842
1846
/* An AbortSignal to set request's signal. */
···
2994
2998
get pathname(): string;
2995
2999
get search(): string;
2996
3000
get hash(): string;
3001
3001
+
get hasRegExpGroups(): boolean;
2997
3002
test(input?: (string | URLPatternInit), baseURL?: string): boolean;
2998
3003
exec(input?: (string | URLPatternInit), baseURL?: string): URLPatternResult | null;
2999
3004
}
···
3255
3260
*
3256
3261
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort)
3257
3262
*/
3258
3258
-
interface MessagePort extends EventTarget {
3263
3263
+
declare abstract class MessagePort extends EventTarget {
3259
3264
/**
3260
3265
* The **`postMessage()`** method of the transfers ownership of objects to other browsing contexts.
3261
3266
*
···
3276
3281
start(): void;
3277
3282
get onmessage(): any | null;
3278
3283
set onmessage(value: any | null);
3284
3284
+
}
3285
3285
+
/**
3286
3286
+
* The **`MessageChannel`** interface of the Channel Messaging API allows us to create a new message channel and send data through it via its two MessagePort properties.
3287
3287
+
*
3288
3288
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel)
3289
3289
+
*/
3290
3290
+
declare class MessageChannel {
3291
3291
+
constructor();
3292
3292
+
/**
3293
3293
+
* The **`port1`** read-only property of the the port attached to the context that originated the channel.
3294
3294
+
*
3295
3295
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel/port1)
3296
3296
+
*/
3297
3297
+
readonly port1: MessagePort;
3298
3298
+
/**
3299
3299
+
* The **`port2`** read-only property of the the port attached to the context at the other end of the channel, which the message is initially sent to.
3300
3300
+
*
3301
3301
+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel/port2)
3302
3302
+
*/
3303
3303
+
readonly port2: MessagePort;
3279
3304
}
3280
3305
interface MessagePortPostMessageOptions {
3281
3306
transfer?: any[];
+3
-3
wrangler.toml
···
71
71
72
72
[[queues.consumers]]
73
73
queue = "skyscheduler-post-queue"
74
74
-
max_batch_size = 5
74
74
+
max_batch_size = 3
75
75
max_batch_timeout = 5
76
76
max_retries = 3
77
77
···
93
93
IMAGE_SETTINGS={enabled=true, steps=[95, 85, 75], bucket_url="https://resize.skyscheduler.work/"}
94
94
95
95
# Signup options and if keys should be used
96
96
-
SIGNUP_SETTINGS = {use_captcha=true, invite_only=false, invite_thread=""}
96
96
+
SIGNUP_SETTINGS = {use_captcha=true, invite_only=false, invite_thread="", invite_uses=10}
97
97
98
98
# queue handling, pushing information.
99
99
QUEUE_SETTINGS = {enabled=true, repostsEnabled=true, postNowEnabled=false, threadEnabled=true, delay_val=100, post_queues=["POST_QUEUE"], repost_queues=["REPOST_QUEUE"]}
···
116
116
[env.staging.vars]
117
117
BETTER_AUTH_URL="*"
118
118
IMAGE_SETTINGS={enabled=false}
119
119
-
SIGNUP_SETTINGS = {use_captcha=false, invite_only=false, invite_thread=""}
119
119
+
SIGNUP_SETTINGS = {use_captcha=false, invite_only=false, invite_thread="", invite_uses=10}
120
120
QUEUE_SETTINGS = {enabled=false, repostsEnabled=false, postNowEnabled=false, threadEnabled=true, delay_val=100, post_queues=["POST_QUEUE"], repost_queues=[]}
121
121
REDIRECTS = {contact="https://bsky.app/profile/skyscheduler.work", tip="https://ko-fi.com/socksthewolf/tip"}
122
122
R2_SETTINGS={auto_prune=false, prune_days=3}