+9
-3
db/config.ts
+9
-3
db/config.ts
···
24
24
tags: column.json(),
25
25
createdAt: column.date({ name: "created_at", default: NOW }),
26
26
updatedAt: column.date({ name: "updated_at", optional: true }),
27
+
draft: column.boolean({ default: true }),
27
28
},
28
29
indexes: [
29
30
{ on: ["author", "slug"], unique: true },
30
31
{ on: ["slug", "createdAt"], unique: true },
31
-
{ on: ["uri", "createdAt"], unique: true },
32
32
],
33
33
});
34
34
···
37
37
id: column.number({ primaryKey: true }),
38
38
uri: column.text({ optional: true }),
39
39
workId: column.number({ references: () => Works.columns.id }),
40
-
// order: column.number(), // i don't think this is needed...
40
+
slug: column.text({ unique: true }),
41
41
title: column.text(),
42
+
warnings: column.text({ multiline: true, optional: true }),
42
43
notes: column.text({ multiline: true, optional: true }),
43
-
content: column.text({ multiline: true }),
44
+
content: column.json(),
44
45
createdAt: column.date({ name: "created_at", default: NOW }),
45
46
updatedAt: column.date({ name: "updated_at", optional: true }),
47
+
draft: column.boolean({ default: true }),
46
48
},
49
+
indexes: [
50
+
{ on: ["workId", "slug"], unique: true },
51
+
{ on: ["workId", "createdAt"], unique: true },
52
+
]
47
53
});
48
54
49
55
const Tags = defineTable({
+5
-5
db/seed.ts
+5
-5
db/seed.ts
···
13
13
author: "test",
14
14
title: "Hey there title",
15
15
summary: "<p>i have evil html</p>",
16
-
tags: [{ label: "test", url: "#" }],
16
+
tags: [{ label: "test", slug: "#", type: "character" }],
17
17
},
18
18
{
19
19
slug: "1235",
20
20
author: "another",
21
21
title: "Hello world",
22
22
summary: "<p>whoag i have <b>BOLD</b></p>",
23
-
tags: [{ label: "label", url: "#" }],
23
+
tags: [{ label: "label", slug: "#", type: "relationship" }, { label: "label", slug: "#", type: "character" }],
24
24
},
25
25
{
26
26
uri: "at://did:plc:dg2qmmjic7mmecrbvpuhtvh6/moe.fanfics.works/3lyeiyq32ek2o",
···
35
35
await db.insert(Chapters).values([
36
36
{
37
37
workId: 1,
38
-
// order: 1,
38
+
slug: `${new Date().valueOf().toString()}-1`,
39
39
title: "chapter title 1",
40
40
content: "what's up?! <b>bold</b> and <em>italics</em> should work.",
41
41
},
42
42
{
43
43
workId: 2,
44
-
// order: 1,
44
+
slug: `${new Date().valueOf().toString()}-2`,
45
45
title: "chapter title 2",
46
46
content: "test",
47
47
},
48
48
{
49
49
workId: 3,
50
-
// order: 1,
50
+
slug: `${new Date().valueOf().toString()}-3`,
51
51
title: "at proto",
52
52
content: "what's up?! <b>bold</b> and <em>italics</em> should work.",
53
53
}
+7
lexicons/fan/fics/work.json
+7
lexicons/fan/fics/work.json
+39
-115
src/actions/works/addWork.ts
+39
-115
src/actions/works/addWork.ts
···
1
1
import { ActionError, defineAction } from "astro:actions";
2
-
import { db, eq, Users, Works } from "astro:db";
2
+
import { Chapters, db, eq, Users, Works } from "astro:db";
3
3
import { z } from "astro:schema";
4
-
import { AtUri } from "@atproto/api";
5
-
import { TID } from "@atproto/common-web";
6
-
import { customAlphabet } from "nanoid";
7
-
import { callSlices, fetchBskyPost, fetchLeaflet, getAgent } from "@/lib/atproto";
8
-
import { addChapter, updateWork } from "@/lib/db";
4
+
import { customAlphabet, nanoid } from "nanoid";
5
+
import { createFanficWork, importChapter } from "@/lib/atproto";
9
6
import schema from "./schema";
7
+
import type { BskyPost, ChapterText, LeafletDoc } from "@/lib/types";
10
8
11
9
export default defineAction({
12
10
accept: "form",
13
-
input: schema.extend({
14
-
option: z.enum(["manual", "bsky", "leaflet"]),
15
-
bskyUri: z.string().optional(),
16
-
leafletUri: z.string().optional(),
17
-
chapterTitle: z.string().optional(),
18
-
content: z.string().optional(),
19
-
notes: z.string().optional(),
20
-
}),
21
-
handler: async (
22
-
// yeah this is fucking insane
23
-
{
24
-
title,
25
-
summary,
26
-
tags,
27
-
option,
28
-
bskyUri,
29
-
leafletUri,
30
-
chapterTitle,
31
-
content,
32
-
notes,
33
-
publish
34
-
}, context) => {
11
+
input: schema,
12
+
handler: async ({ title, summary, tags, publish }, context) => {
35
13
const loggedInUser = context.locals.loggedInUser;
36
14
37
15
//#region "Check authentication"
···
69
47
};
70
48
//#endregion
71
49
72
-
//#region "Import chapter from Bluesky or Leaflet"
73
-
if (option !== "manual") {
74
-
if (bskyUri) {
75
-
const result = await fetchBskyPost(bskyUri);
76
-
console.log("bsky post: " + JSON.stringify(result));
77
-
}
78
-
if (leafletUri) {
79
-
const result = await fetchLeaflet(leafletUri);
80
-
console.log("leaflet: " + JSON.stringify(result));
81
-
}
82
-
}
83
-
//#endregion
84
-
85
50
//#region "Start publishing work to ATProto"
86
51
// we'll assign this after a successful request was made
87
52
let uri: string | undefined;
88
-
let cUri: string | undefined;
53
+
// let cUri: string | undefined;
89
54
90
55
if (publish) {
91
56
try {
92
-
const rkey = TID.nextStr();
93
57
const { tags, ...rest } = record;
94
58
95
-
const result = await callSlices(
96
-
"work",
97
-
"createRecord",
98
-
rkey,
99
-
{
100
-
...rest,
101
-
tags: [tags],
102
-
author: loggedInUser.did,
103
-
createdAt: createdAt.toISOString()
104
-
}
105
-
);
59
+
const result = await createFanficWork({
60
+
...rest,
61
+
tags: [tags],
62
+
author: loggedInUser.did,
63
+
createdAt: createdAt.toISOString(),
64
+
});
106
65
107
66
console.log(JSON.stringify(result));
108
67
if (result.error) {
109
-
console.error(`this went wrong for WORK: ${result.message}`);
110
68
throw new ActionError({
111
69
code: "BAD_REQUEST",
112
-
message: "Something went wrong!",
70
+
message: "Something went wrong with posting your fic to your PDS!",
113
71
});
114
72
}
115
73
uri = result.uri;
116
-
117
-
//#region "Publish the first chapter with the work to ATProto"
118
-
// ONLY proceed if the uri is set from successfully adding a work
119
-
if (uri) {
120
-
const crkey = TID.nextStr(rkey);
121
-
let chapterContent = {};
122
-
123
-
if (option === "manual") {
124
-
chapterContent = {
125
-
$type: "fan.fics.work.chapter#chapterText",
126
-
text: content,
127
-
};
128
-
}
129
-
130
-
const chapter = await callSlices(
131
-
"work.chapter",
132
-
"createRecord",
133
-
crkey,
134
-
{
135
-
title: chapterTitle,
136
-
content: chapterContent,
137
-
createdAt: createdAt.toISOString(),
138
-
workUri: uri,
139
-
}
140
-
);
141
-
142
-
console.log(JSON.stringify(chapter));
143
-
if (chapter.error) {
144
-
console.error(`this went wrong: ${chapter.message}`);
145
-
throw new ActionError({
146
-
code: "BAD_REQUEST",
147
-
message: "Something went wrong!",
148
-
});
149
-
}
150
-
cUri = chapter.uri;
151
-
//#endregion
152
-
}
153
74
} catch (error) {
154
75
console.error(error);
155
76
throw new ActionError({
···
163
84
//#region "Add a new work to the database"
164
85
// check nanoid for collision probability: https://zelark.github.io/nano-id-cc/
165
86
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
166
-
const nanoid = customAlphabet(alphabet, 16);
167
-
const slug = nanoid();
168
-
87
+
const custom = customAlphabet(alphabet, 16);
88
+
const slug = custom();
89
+
169
90
const [work] = await db.insert(Works).values({
170
91
uri,
171
92
slug,
172
93
createdAt,
173
94
author: user.did,
174
95
...record,
175
-
}).returning();
96
+
}).onConflictDoNothing({ target: Works.slug }).returning();
176
97
177
-
if (chapterTitle && content) {
178
-
try {
179
-
await addChapter(
180
-
work.id,
181
-
chapterTitle,
182
-
content,
183
-
cUri,
184
-
notes,
185
-
);
186
-
} catch (error) {
187
-
console.error(error);
188
-
throw new ActionError({
189
-
code: "BAD_REQUEST",
190
-
message: "Something went wrong!",
191
-
});
192
-
}
193
-
}
194
-
195
98
return work;
99
+
// const newWork = await db.transaction(async (tx) => {
100
+
// const [work] = await tx.insert(Works).values({
101
+
// uri,
102
+
// slug,
103
+
// createdAt,
104
+
// author: user.did,
105
+
// ...record,
106
+
// }).onConflictDoNothing({ target: Works.slug }).returning();
107
+
// if (!work) { tx.rollback(); }
108
+
// const [chapter] = await tx.insert(Chapters).values({
109
+
// workId: work.id,
110
+
// slug: nanoid(),
111
+
// title: chapterTitle!,
112
+
// content: content!,
113
+
// uri: cUri,
114
+
// authorsNotes: notes,
115
+
// }).onConflictDoNothing({ target: Chapters.id }).returning();
116
+
// if (!chapter) { tx.rollback(); }
117
+
// return work;
118
+
// });
119
+
// return newWork;
196
120
//#endregion
197
121
},
198
122
});
+6
-6
src/components/chapters/Chapter.astro
+6
-6
src/components/chapters/Chapter.astro
···
10
10
<section>
11
11
<header>
12
12
<h1>{chapter.title}</h1>
13
-
{chapter.authorsNotes && (
13
+
{chapter.warnings && (
14
14
<details>
15
-
<summary>Author's Notes</summary>
16
-
<Fragment set:html={chapter.authorsNotes} />
15
+
<summary>Content warnings</summary>
16
+
<Fragment set:html={chapter.warnings} />
17
17
</details>
18
18
)}
19
19
<time datetime={chapter.createdAt.toISOString()}>
···
26
26
</div>
27
27
28
28
<footer>
29
-
{chapter.endNotes && (
29
+
{chapter.notes && (
30
30
<aside>
31
-
<p>End notes</p>
32
-
<Fragment set:html={chapter.endNotes} />
31
+
<p>Author's Notes</p>
32
+
<Fragment set:html={chapter.notes} />
33
33
</aside>
34
34
)}
35
35
</footer>
+23
-1
src/lib/db.ts
+23
-1
src/lib/db.ts
···
1
1
import slugify from "@sindresorhus/slugify";
2
2
import { and, Chapters, db, eq, Tags, Works } from "astro:db";
3
3
import type { Chapter } from "./types";
4
+
import { SQLiteOAuthStorage } from "@slices/oauth";
5
+
6
+
7
+
const OAUTH_CLIENT_ID = import.meta.env.OAUTH_CLIENT_ID;
8
+
const OAUTH_CLIENT_SECRET = import.meta.env.OAUTH_CLIENT_SECRET;
9
+
const OAUTH_REDIRECT_URI = import.meta.env.OAUTH_REDIRECT_URI;
10
+
const OAUTH_AIP_BASE_URL = import.meta.env.OAUTH_AIP_BASE_URL;
11
+
const API_URL = import.meta.env.API_URL;
12
+
export const SLICE_URI = import.meta.env.SLICE_URI;
13
+
14
+
// oauth session
15
+
// const DATABASE_URL = import.meta.env.DATABASE_URL || "slices.db";
16
+
// const oauthStorage = new SQLiteOAuthStorage(DATABASE_URL);
17
+
// const oauthConfig = {
18
+
// clientId: OAUTH_CLIENT_ID,
19
+
// clientSecret: OAUTH_CLIENT_SECRET,
20
+
// authBaseUrl: OAUTH_AIP_BASE_URL,
21
+
// redirectUri: OAUTH_REDIRECT_URI,
22
+
// scopes: ["atproto", "openid", "profile", "transition:generic"],
23
+
// };
4
24
5
25
// fetch tags
6
26
export async function searchTags(search: string) {
···
39
59
workId,
40
60
uri,
41
61
title,
62
+
slug: "",
42
63
// order,
43
64
notes,
44
65
content,
···
58
79
eq(Works.id, chapter.workId),
59
80
eq(Chapters.id, chapter.id)
60
81
));
61
-
}
82
+
}
83
+
+40
-1
src/lib/types.ts
+40
-1
src/lib/types.ts
···
3
3
export type Work = typeof Works.$inferSelect;
4
4
export type Chapter = typeof Chapters.$inferSelect;
5
5
export type Tag = typeof Tags.$inferSelect;
6
-
export type User = typeof Users.$inferSelect;
6
+
export type User = typeof Users.$inferSelect;
7
+
8
+
export type atProtoWork = Omit<Work, "id" | "slug"> | {
9
+
createdAt: string;
10
+
updatedAt?: string;
11
+
};
12
+
13
+
export type atProtoChapter = Omit<Chapter, "id" | "uri" | "slug" | "workId"> | {
14
+
workUri: string;
15
+
chapterRef?: string;
16
+
content: ChapterText | BskyPost | LeafletDoc;
17
+
createdAt: string;
18
+
updatedAt?: string;
19
+
};
20
+
21
+
export type atProtoComment = {
22
+
content: string;
23
+
createdAt: string;
24
+
postedTo?: string;
25
+
};
26
+
27
+
type comAtProtoStrongRef = {
28
+
uri: string;
29
+
cid: string;
30
+
};
31
+
32
+
export type ChapterText = {
33
+
$type: "fan.fics.work.chapter#chapterText";
34
+
text: string;
35
+
};
36
+
37
+
export type BskyPost = {
38
+
$type: "fan.fics.work.chapter#bskyPost";
39
+
postRef: comAtProtoStrongRef;
40
+
};
41
+
42
+
export type LeafletDoc = {
43
+
$type: "fan.fics.work.chapter#leafletDoc";
44
+
docRef: comAtProtoStrongRef;
45
+
};
+2
-1
src/middleware.ts
+2
-1
src/middleware.ts
···
3
3
4
4
export const onRequest = defineMiddleware(async (context, next) => {
5
5
if (context.isPrerendered) return next();
6
+
// context.session?.set("oauth", "hey") // figure this out
7
+
6
8
const { action, setActionResult, serializeActionResult } = getActionContext(context);
7
-
8
9
const latestAction = await context.session?.get("latest-action");
9
10
if (latestAction) {
10
11
setActionResult(latestAction.name, latestAction.result);