@fedify/botkit-sqlite 0.3.0-dev.132
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.
- package/LICENSE +661 -0
- package/README.md +111 -0
- package/dist/mod.d.ts +67 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +419 -0
- package/dist/mod.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
@fedify/botkit-sqlite
|
|
2
|
+
=====================
|
|
3
|
+
|
|
4
|
+
[![JSR][JSR badge]][JSR]
|
|
5
|
+
[![npm][npm badge]][npm]
|
|
6
|
+
[![GitHub Actions][GitHub Actions badge]][GitHub Actions]
|
|
7
|
+
|
|
8
|
+
This package is a [SQLite]-based repository implementation for [BotKit].
|
|
9
|
+
It provides a production-ready data storage solution using the built-in
|
|
10
|
+
`node:sqlite` module, offering better performance and reliability compared to
|
|
11
|
+
in-memory storage while maintaining compatibility with both [Deno] and [Node.js]
|
|
12
|
+
environments.
|
|
13
|
+
|
|
14
|
+
[JSR]: https://jsr.io/@fedify/botkit-sqlite
|
|
15
|
+
[JSR badge]: https://jsr.io/badges/@fedify/botkit-sqlite
|
|
16
|
+
[npm]: https://www.npmjs.com/package/@fedify/botkit-sqlite
|
|
17
|
+
[npm badge]: https://img.shields.io/npm/v/@fedify/botkit-sqlite?logo=npm
|
|
18
|
+
[GitHub Actions]: https://github.com/fedify-dev/botkit/actions/workflows/main.yaml
|
|
19
|
+
[GitHub Actions badge]: https://github.com/fedify-dev/botkit/actions/workflows/main.yaml/badge.svg
|
|
20
|
+
[SQLite]: https://sqlite.org/
|
|
21
|
+
[BotKit]: https://botkit.fedify.dev/
|
|
22
|
+
[Deno]: https://deno.land/
|
|
23
|
+
[Node.js]: https://nodejs.org/
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
Installation
|
|
27
|
+
------------
|
|
28
|
+
|
|
29
|
+
~~~~ sh
|
|
30
|
+
deno add jsr:@fedify/botkit-sqlite
|
|
31
|
+
npm add @fedify/botkit-sqlite
|
|
32
|
+
pnpm add @fedify/botkit-sqlite
|
|
33
|
+
yarn add @fedify/botkit-sqlite
|
|
34
|
+
~~~~
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Usage
|
|
38
|
+
-----
|
|
39
|
+
|
|
40
|
+
The `SqliteRepository` can be used as a drop-in replacement for other repository
|
|
41
|
+
implementations in BotKit:
|
|
42
|
+
|
|
43
|
+
~~~~ typescript
|
|
44
|
+
import { createBot } from "@fedify/botkit";
|
|
45
|
+
import { SqliteRepository } from "@fedify/botkit-sqlite";
|
|
46
|
+
|
|
47
|
+
const bot = createBot({
|
|
48
|
+
username: "mybot",
|
|
49
|
+
name: "My Bot",
|
|
50
|
+
repository: new SqliteRepository({
|
|
51
|
+
// Use a file-based database for persistence:
|
|
52
|
+
path: "./bot-data.db",
|
|
53
|
+
// Enable WAL mode for better performance (default: true):
|
|
54
|
+
wal: true,
|
|
55
|
+
}),
|
|
56
|
+
// ... other bot configuration
|
|
57
|
+
});
|
|
58
|
+
~~~~
|
|
59
|
+
|
|
60
|
+
### Options
|
|
61
|
+
|
|
62
|
+
The `SqliteRepository` constructor accepts the following options:
|
|
63
|
+
|
|
64
|
+
- **`path`** (optional): Path to the SQLite database file. Defaults to
|
|
65
|
+
`":memory:"` for an in-memory database. Use a file path for persistent
|
|
66
|
+
storage.
|
|
67
|
+
|
|
68
|
+
- **`wal`** (optional): Whether to enable write-ahead logging (WAL) mode for
|
|
69
|
+
better performance. Defaults to `true`. Note that WAL mode is automatically
|
|
70
|
+
disabled for in-memory databases.
|
|
71
|
+
|
|
72
|
+
### Examples
|
|
73
|
+
|
|
74
|
+
#### In-memory database (for testing/development)
|
|
75
|
+
|
|
76
|
+
~~~~ typescript
|
|
77
|
+
const repository = new SqliteRepository(); // Uses :memory: by default
|
|
78
|
+
~~~~
|
|
79
|
+
|
|
80
|
+
#### File-based database (for production)
|
|
81
|
+
|
|
82
|
+
~~~~ typescript
|
|
83
|
+
const repository = new SqliteRepository({
|
|
84
|
+
path: "./data/botkit.db",
|
|
85
|
+
wal: true,
|
|
86
|
+
});
|
|
87
|
+
~~~~
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
Features
|
|
91
|
+
--------
|
|
92
|
+
|
|
93
|
+
- **Cross-runtime**: Works with both Deno and Node.js using the `node:sqlite`
|
|
94
|
+
module
|
|
95
|
+
|
|
96
|
+
- **High performance**: Utilizes WAL mode and proper indexing for optimal
|
|
97
|
+
performance
|
|
98
|
+
|
|
99
|
+
- **ACID compliance**: Transactions ensure data integrity and consistency
|
|
100
|
+
|
|
101
|
+
- **Full `Repository` API**: Implements all BotKit repository methods
|
|
102
|
+
including:
|
|
103
|
+
|
|
104
|
+
- Key pair management for ActivityPub signing
|
|
105
|
+
- Message storage and retrieval with temporal filtering
|
|
106
|
+
- Follower and following relationship management
|
|
107
|
+
- Poll voting system
|
|
108
|
+
|
|
109
|
+
- **Resource management**: Implements `Disposable` interface for proper cleanup
|
|
110
|
+
|
|
111
|
+
<!-- cSpell: ignore mybot -->
|
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
2
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
3
|
+
import { Actor, Announce, Create, Follow } from "@fedify/fedify/vocab";
|
|
4
|
+
import { Repository, RepositoryGetFollowersOptions, RepositoryGetMessagesOptions, Uuid } from "@fedify/botkit/repository";
|
|
5
|
+
|
|
6
|
+
//#region src/mod.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for creating a SQLite repository.
|
|
9
|
+
* @since 0.3.0
|
|
10
|
+
*/
|
|
11
|
+
interface SqliteRepositoryOptions {
|
|
12
|
+
/**
|
|
13
|
+
* The path to the SQLite database file.
|
|
14
|
+
* If not provided, an in-memory database will be used.
|
|
15
|
+
*/
|
|
16
|
+
readonly path?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Whether to enable Write-Ahead Logging (WAL) mode.
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
readonly wal?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A repository for storing bot data using SQLite.
|
|
25
|
+
* @since 0.3.0
|
|
26
|
+
*/
|
|
27
|
+
declare class SqliteRepository implements Repository, Disposable {
|
|
28
|
+
private readonly db;
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new SQLite repository.
|
|
31
|
+
* @param options The options for creating the repository.
|
|
32
|
+
*/
|
|
33
|
+
constructor(options?: SqliteRepositoryOptions);
|
|
34
|
+
[Symbol.dispose](): void;
|
|
35
|
+
/**
|
|
36
|
+
* Closes the database connection.
|
|
37
|
+
*/
|
|
38
|
+
close(): void;
|
|
39
|
+
private initializeTables;
|
|
40
|
+
setKeyPairs(keyPairs: CryptoKeyPair[]): Promise<void>;
|
|
41
|
+
getKeyPairs(): Promise<CryptoKeyPair[] | undefined>;
|
|
42
|
+
addMessage(id: Uuid, activity: Create | Announce): Promise<void>;
|
|
43
|
+
updateMessage(id: Uuid, updater: (existing: Create | Announce) => Create | Announce | undefined | Promise<Create | Announce | undefined>): Promise<boolean>;
|
|
44
|
+
removeMessage(id: Uuid): Promise<Create | Announce | undefined>;
|
|
45
|
+
getMessages(options?: RepositoryGetMessagesOptions): AsyncIterable<Create | Announce>;
|
|
46
|
+
getMessage(id: Uuid): Promise<Create | Announce | undefined>;
|
|
47
|
+
countMessages(): Promise<number>;
|
|
48
|
+
addFollower(followRequestId: URL, follower: Actor): Promise<void>;
|
|
49
|
+
removeFollower(followRequestId: URL, actorId: URL): Promise<Actor | undefined>;
|
|
50
|
+
hasFollower(followerId: URL): Promise<boolean>;
|
|
51
|
+
getFollowers(options?: RepositoryGetFollowersOptions): AsyncIterable<Actor>;
|
|
52
|
+
countFollowers(): Promise<number>;
|
|
53
|
+
addSentFollow(id: Uuid, follow: Follow): Promise<void>;
|
|
54
|
+
removeSentFollow(id: Uuid): Promise<Follow | undefined>;
|
|
55
|
+
getSentFollow(id: Uuid): Promise<Follow | undefined>;
|
|
56
|
+
addFollowee(followeeId: URL, follow: Follow): Promise<void>;
|
|
57
|
+
removeFollowee(followeeId: URL): Promise<Follow | undefined>;
|
|
58
|
+
getFollowee(followeeId: URL): Promise<Follow | undefined>;
|
|
59
|
+
vote(messageId: Uuid, voterId: URL, option: string): Promise<void>;
|
|
60
|
+
countVoters(messageId: Uuid): Promise<number>;
|
|
61
|
+
countVotes(messageId: Uuid): Promise<Readonly<Record<string, number>>>;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=mod.d.ts.map
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { SqliteRepository, SqliteRepositoryOptions };
|
|
67
|
+
//# sourceMappingURL=mod.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.d.ts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;UAwCiB,uBAAA;;AAAjB;AAkBA;;EAA8B,SAOP,IAAA,CAAA,EAAA,MAAA;EAA4B;;;;EA4HP,SAArB,GAAA,CAAA,EAAA,OAAA;;;;;;AA0CP,cA7KH,gBAAA,YAA4B,UA6KzB,EA7KqC,UA6KrC,CAAA;EAAM,iBAAG,EAAA;EAAQ;;;;EACiC,WAAzB,CAAA,OAAA,CAAA,EAvKlB,uBAuKkB;EAAO,CAxJ7C,MAAA,CAAO,OAAA,GAyJL,EAAA,IAAA;EAAO;;;EAiC8C,KAAzB,CAAA,CAAA,EAAA,IAAA;EAAO,QA4B3B,gBAAA;EAAiC,WAC3B,CAAA,QAAA,EAlIW,aAkIX,EAAA,CAAA,EAlI6B,OAkI7B,CAAA,IAAA,CAAA;EAAM,WAAG,CAAA,CAAA,EA1GL,OA0GK,CA1GG,aA0GH,EAAA,GAAA,SAAA,CAAA;EAAQ,UAA/B,CAAA,EAAA,EAjFkB,IAiFlB,EAAA,QAAA,EAjFkC,MAiFlC,GAjF2C,QAiF3C,CAAA,EAjFsD,OAiFtD,CAAA,IAAA,CAAA;EAAa,aA2CK,CAAA,EAAA,EA7Gf,IA6Ge,EAAA,OAAA,EAAA,CAAA,QAAA,EA3GP,MA2GO,GA3GE,QA2GF,EAAA,GA1Gd,MA0Gc,GA1GL,QA0GK,GAAA,SAAA,GA1GkB,OA0GlB,CA1G0B,MA0G1B,GA1GmC,QA0GnC,GAAA,SAAA,CAAA,CAAA,EAzGlB,OAyGkB,CAAA,OAAA,CAAA;EAAI,aAAW,CAAA,EAAA,EAxEZ,IAwEY,CAAA,EAxEL,OAwEK,CAxEG,MAwEH,GAxEY,QAwEZ,GAAA,SAAA,CAAA;EAAM,WAAG,CAAA,OAAA,CAAA,EA5ClC,4BA4CkC,CAAA,EA3C1C,aA2C0C,CA3C5B,MA2C4B,GA3CnB,QA2CmB,CAAA;EAAQ,UAAzB,CAAA,EAAA,EAAP,IAAO,CAAA,EAAA,OAAA,CAAQ,MAAR,GAAiB,QAAjB,GAAA,SAAA,CAAA;EAAO,aAsBlB,CAAA,CAAA,EAAA,OAAA,CAAA,MAAA,CAAA;EAAO,WAMW,CAAA,eAAA,EAAA,GAAA,EAAA,QAAA,EAAe,KAAf,CAAA,EAAuB,OAAvB,CAAA,IAAA,CAAA;EAAG,cAAY,CAAA,eAAA,EA+B/B,GA/B+B,EAAA,OAAA,EAgCvC,GAhCuC,CAAA,EAiC/C,OAjC+C,CAiCvC,KAjCuC,GAAA,SAAA,CAAA;EAAK,WAAG,CAAA,UAAA,EAkFlC,GAlFkC,CAAA,EAkF5B,OAlF4B,CAAA,OAAA,CAAA;EAAO,YA+B9C,CAAA,OAAA,CAAA,EA4DR,6BA5DQ,CAAA,EA6DhB,aA7DgB,CA6DF,KA7DE,CAAA;EAAG,cACX,CAAA,CAAA,EA4FO,OA5FP,CAAA,MAAA,CAAA;EAAG,aACH,CAAA,EAAA,EAiGa,IAjGb,EAAA,MAAA,EAiG2B,MAjG3B,CAAA,EAiGoC,OAjGpC,CAAA,IAAA,CAAA;EAAK,gBAAb,CAAA,EAAA,EA8GwB,IA9GxB,CAAA,EA8G+B,OA9G/B,CA8GuC,MA9GvC,GAAA,SAAA,CAAA;EAAO,aAiDc,CAAA,EAAA,EAuEA,IAvEA,CAAA,EAuEO,OAvEP,CAuEe,MAvEf,GAAA,SAAA,CAAA;EAAG,WAAG,CAAA,UAAA,EAwFA,GAxFA,EAAA,MAAA,EAwFa,MAxFb,CAAA,EAwFsB,OAxFtB,CAAA,IAAA,CAAA;EAAO,cAS1B,CAAA,UAAA,EA4FsB,GA5FtB,CAAA,EA4F4B,OA5F5B,CA4FoC,MA5FpC,GAAA,SAAA,CAAA;EAAkC,WAC5B,CAAA,UAAA,EAqGa,GArGb,CAAA,EAqGmB,OArGnB,CAqG2B,MArG3B,GAAA,SAAA,CAAA;EAAK,IAAnB,CAAA,SAAA,EA2Ha,IA3Hb,EAAA,OAAA,EA2H4B,GA3H5B,EAAA,MAAA,EAAA,MAAA,CAAA,EA2HkD,OA3HlD,CAAA,IAAA,CAAA;EAAa,WAgCE,CAAA,SAAA,EAqGK,IArGL,CAAA,EAqGY,OArGZ,CAAA,MAAA,CAAA;EAAO,UAMD,CAAA,SAAA,EAyGF,IAzGE,CAAA,EAyGK,OAzGL,CAyGa,QAzGb,CAyGsB,MAzGtB,CAAA,MAAA,EAAA,MAAA,CAAA,CAAA,CAAA"}
|
package/dist/mod.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
3
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
4
|
+
|
|
5
|
+
import { exportJwk, importJwk } from "@fedify/fedify/sig";
|
|
6
|
+
import { Activity, Announce, Create, Follow, Object as Object$1, isActor } from "@fedify/fedify/vocab";
|
|
7
|
+
import { getLogger } from "@logtape/logtape";
|
|
8
|
+
import { DatabaseSync } from "node:sqlite";
|
|
9
|
+
|
|
10
|
+
//#region src/mod.ts
|
|
11
|
+
const logger = getLogger(["botkit", "sqlite"]);
|
|
12
|
+
/**
|
|
13
|
+
* A repository for storing bot data using SQLite.
|
|
14
|
+
* @since 0.3.0
|
|
15
|
+
*/
|
|
16
|
+
var SqliteRepository = class {
|
|
17
|
+
db;
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new SQLite repository.
|
|
20
|
+
* @param options The options for creating the repository.
|
|
21
|
+
*/
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
const { path = ":memory:", wal = true } = options;
|
|
24
|
+
this.db = new DatabaseSync(path);
|
|
25
|
+
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
26
|
+
if (wal && path !== ":memory:") this.db.exec("PRAGMA journal_mode = WAL;");
|
|
27
|
+
this.initializeTables();
|
|
28
|
+
}
|
|
29
|
+
[Symbol.dispose]() {
|
|
30
|
+
this.close();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Closes the database connection.
|
|
34
|
+
*/
|
|
35
|
+
close() {
|
|
36
|
+
this.db.close();
|
|
37
|
+
}
|
|
38
|
+
initializeTables() {
|
|
39
|
+
this.db.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS key_pairs (
|
|
41
|
+
id INTEGER PRIMARY KEY,
|
|
42
|
+
private_key_jwk TEXT NOT NULL,
|
|
43
|
+
public_key_jwk TEXT NOT NULL
|
|
44
|
+
)
|
|
45
|
+
`);
|
|
46
|
+
this.db.exec(`
|
|
47
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
48
|
+
id TEXT PRIMARY KEY,
|
|
49
|
+
activity_json TEXT NOT NULL,
|
|
50
|
+
published INTEGER
|
|
51
|
+
)
|
|
52
|
+
`);
|
|
53
|
+
this.db.exec(`
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_messages_published ON messages(published)
|
|
55
|
+
`);
|
|
56
|
+
this.db.exec(`
|
|
57
|
+
CREATE TABLE IF NOT EXISTS followers (
|
|
58
|
+
follower_id TEXT PRIMARY KEY,
|
|
59
|
+
actor_json TEXT NOT NULL
|
|
60
|
+
)
|
|
61
|
+
`);
|
|
62
|
+
this.db.exec(`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS follow_requests (
|
|
64
|
+
follow_request_id TEXT PRIMARY KEY,
|
|
65
|
+
follower_id TEXT NOT NULL,
|
|
66
|
+
FOREIGN KEY (follower_id) REFERENCES followers(follower_id)
|
|
67
|
+
)
|
|
68
|
+
`);
|
|
69
|
+
this.db.exec(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS sent_follows (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
follow_json TEXT NOT NULL
|
|
73
|
+
)
|
|
74
|
+
`);
|
|
75
|
+
this.db.exec(`
|
|
76
|
+
CREATE TABLE IF NOT EXISTS followees (
|
|
77
|
+
followee_id TEXT PRIMARY KEY,
|
|
78
|
+
follow_json TEXT NOT NULL
|
|
79
|
+
)
|
|
80
|
+
`);
|
|
81
|
+
this.db.exec(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS poll_votes (
|
|
83
|
+
message_id TEXT NOT NULL,
|
|
84
|
+
voter_id TEXT NOT NULL,
|
|
85
|
+
option TEXT NOT NULL,
|
|
86
|
+
PRIMARY KEY (message_id, voter_id, option)
|
|
87
|
+
)
|
|
88
|
+
`);
|
|
89
|
+
this.db.exec(`
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_poll_votes_message_option
|
|
91
|
+
ON poll_votes(message_id, option)
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
async setKeyPairs(keyPairs) {
|
|
95
|
+
const deleteStmt = this.db.prepare("DELETE FROM key_pairs");
|
|
96
|
+
const insertStmt = this.db.prepare(`
|
|
97
|
+
INSERT INTO key_pairs (private_key_jwk, public_key_jwk)
|
|
98
|
+
VALUES (?, ?)
|
|
99
|
+
`);
|
|
100
|
+
this.db.exec("BEGIN TRANSACTION");
|
|
101
|
+
try {
|
|
102
|
+
deleteStmt.run();
|
|
103
|
+
for (const keyPair of keyPairs) {
|
|
104
|
+
const privateJwk = await exportJwk(keyPair.privateKey);
|
|
105
|
+
const publicJwk = await exportJwk(keyPair.publicKey);
|
|
106
|
+
insertStmt.run(JSON.stringify(privateJwk), JSON.stringify(publicJwk));
|
|
107
|
+
}
|
|
108
|
+
this.db.exec("COMMIT");
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.db.exec("ROLLBACK");
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async getKeyPairs() {
|
|
115
|
+
const stmt = this.db.prepare(`
|
|
116
|
+
SELECT private_key_jwk, public_key_jwk FROM key_pairs
|
|
117
|
+
`);
|
|
118
|
+
const rows = stmt.all();
|
|
119
|
+
if (rows.length === 0) return void 0;
|
|
120
|
+
const keyPairs = [];
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
const privateJwk = JSON.parse(row.private_key_jwk);
|
|
123
|
+
const publicJwk = JSON.parse(row.public_key_jwk);
|
|
124
|
+
keyPairs.push({
|
|
125
|
+
privateKey: await importJwk(privateJwk, "private"),
|
|
126
|
+
publicKey: await importJwk(publicJwk, "public")
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return keyPairs;
|
|
130
|
+
}
|
|
131
|
+
async addMessage(id, activity) {
|
|
132
|
+
const stmt = this.db.prepare(`
|
|
133
|
+
INSERT INTO messages (id, activity_json, published)
|
|
134
|
+
VALUES (?, ?, ?)
|
|
135
|
+
`);
|
|
136
|
+
const activityJson = JSON.stringify(await activity.toJsonLd({ format: "compact" }));
|
|
137
|
+
const published = activity.published?.epochMilliseconds ?? null;
|
|
138
|
+
stmt.run(id, activityJson, published);
|
|
139
|
+
}
|
|
140
|
+
async updateMessage(id, updater) {
|
|
141
|
+
const selectStmt = this.db.prepare(`
|
|
142
|
+
SELECT activity_json FROM messages WHERE id = ?
|
|
143
|
+
`);
|
|
144
|
+
const row = selectStmt.get(id);
|
|
145
|
+
if (!row) return false;
|
|
146
|
+
const activityData = JSON.parse(row.activity_json);
|
|
147
|
+
const activity = await Activity.fromJsonLd(activityData);
|
|
148
|
+
if (!(activity instanceof Create || activity instanceof Announce)) return false;
|
|
149
|
+
const newActivity = await updater(activity);
|
|
150
|
+
if (newActivity == null) return false;
|
|
151
|
+
const updateStmt = this.db.prepare(`
|
|
152
|
+
UPDATE messages
|
|
153
|
+
SET activity_json = ?, published = ?
|
|
154
|
+
WHERE id = ?
|
|
155
|
+
`);
|
|
156
|
+
const newActivityJson = JSON.stringify(await newActivity.toJsonLd({ format: "compact" }));
|
|
157
|
+
const published = newActivity.published?.epochMilliseconds ?? null;
|
|
158
|
+
updateStmt.run(newActivityJson, published, id);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
async removeMessage(id) {
|
|
162
|
+
const selectStmt = this.db.prepare(`
|
|
163
|
+
SELECT activity_json FROM messages WHERE id = ?
|
|
164
|
+
`);
|
|
165
|
+
const row = selectStmt.get(id);
|
|
166
|
+
if (!row) return void 0;
|
|
167
|
+
const deleteStmt = this.db.prepare(`
|
|
168
|
+
DELETE FROM messages WHERE id = ?
|
|
169
|
+
`);
|
|
170
|
+
deleteStmt.run(id);
|
|
171
|
+
try {
|
|
172
|
+
const activityData = JSON.parse(row.activity_json);
|
|
173
|
+
const activity = await Activity.fromJsonLd(activityData);
|
|
174
|
+
if (activity instanceof Create || activity instanceof Announce) return activity;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logger.warn("Failed to parse removed message activity", {
|
|
177
|
+
id,
|
|
178
|
+
error
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return void 0;
|
|
182
|
+
}
|
|
183
|
+
async *getMessages(options = {}) {
|
|
184
|
+
const { order = "newest", until, since, limit } = options;
|
|
185
|
+
let sql = "SELECT activity_json FROM messages WHERE 1=1";
|
|
186
|
+
const params = [];
|
|
187
|
+
if (since != null) {
|
|
188
|
+
sql += " AND published >= ?";
|
|
189
|
+
params.push(since.epochMilliseconds);
|
|
190
|
+
}
|
|
191
|
+
if (until != null) {
|
|
192
|
+
sql += " AND published <= ?";
|
|
193
|
+
params.push(until.epochMilliseconds);
|
|
194
|
+
}
|
|
195
|
+
sql += order === "oldest" ? " ORDER BY published ASC" : " ORDER BY published DESC";
|
|
196
|
+
if (limit != null) {
|
|
197
|
+
sql += " LIMIT ?";
|
|
198
|
+
params.push(limit);
|
|
199
|
+
}
|
|
200
|
+
const stmt = this.db.prepare(sql);
|
|
201
|
+
const rows = stmt.all(...params);
|
|
202
|
+
for (const row of rows) try {
|
|
203
|
+
const activityData = JSON.parse(row.activity_json);
|
|
204
|
+
const activity = await Activity.fromJsonLd(activityData);
|
|
205
|
+
if (activity instanceof Create || activity instanceof Announce) yield activity;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
logger.warn("Failed to parse message activity", { error });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async getMessage(id) {
|
|
212
|
+
const stmt = this.db.prepare(`
|
|
213
|
+
SELECT activity_json FROM messages WHERE id = ?
|
|
214
|
+
`);
|
|
215
|
+
const row = stmt.get(id);
|
|
216
|
+
if (!row) return void 0;
|
|
217
|
+
try {
|
|
218
|
+
const activityData = JSON.parse(row.activity_json);
|
|
219
|
+
const activity = await Activity.fromJsonLd(activityData);
|
|
220
|
+
if (activity instanceof Create || activity instanceof Announce) return activity;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
logger.warn("Failed to parse message activity", {
|
|
223
|
+
id,
|
|
224
|
+
error
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
countMessages() {
|
|
230
|
+
const stmt = this.db.prepare("SELECT COUNT(*) as count FROM messages");
|
|
231
|
+
const row = stmt.get();
|
|
232
|
+
return Promise.resolve(row.count);
|
|
233
|
+
}
|
|
234
|
+
async addFollower(followRequestId, follower) {
|
|
235
|
+
if (follower.id == null) throw new TypeError("The follower ID is missing.");
|
|
236
|
+
const followerJson = JSON.stringify(await follower.toJsonLd({ format: "compact" }));
|
|
237
|
+
const insertFollowerStmt = this.db.prepare(`
|
|
238
|
+
INSERT OR REPLACE INTO followers (follower_id, actor_json)
|
|
239
|
+
VALUES (?, ?)
|
|
240
|
+
`);
|
|
241
|
+
const insertRequestStmt = this.db.prepare(`
|
|
242
|
+
INSERT OR REPLACE INTO follow_requests (follow_request_id, follower_id)
|
|
243
|
+
VALUES (?, ?)
|
|
244
|
+
`);
|
|
245
|
+
this.db.exec("BEGIN TRANSACTION");
|
|
246
|
+
try {
|
|
247
|
+
insertFollowerStmt.run(follower.id.href, followerJson);
|
|
248
|
+
insertRequestStmt.run(followRequestId.href, follower.id.href);
|
|
249
|
+
this.db.exec("COMMIT");
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.db.exec("ROLLBACK");
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async removeFollower(followRequestId, actorId) {
|
|
256
|
+
const checkStmt = this.db.prepare(`
|
|
257
|
+
SELECT fr.follower_id, f.actor_json
|
|
258
|
+
FROM follow_requests fr
|
|
259
|
+
JOIN followers f ON fr.follower_id = f.follower_id
|
|
260
|
+
WHERE fr.follow_request_id = ? AND fr.follower_id = ?
|
|
261
|
+
`);
|
|
262
|
+
const row = checkStmt.get(followRequestId.href, actorId.href);
|
|
263
|
+
if (!row) return void 0;
|
|
264
|
+
const deleteRequestStmt = this.db.prepare(`
|
|
265
|
+
DELETE FROM follow_requests WHERE follow_request_id = ?
|
|
266
|
+
`);
|
|
267
|
+
const deleteFollowerStmt = this.db.prepare(`
|
|
268
|
+
DELETE FROM followers WHERE follower_id = ?
|
|
269
|
+
`);
|
|
270
|
+
this.db.exec("BEGIN TRANSACTION");
|
|
271
|
+
try {
|
|
272
|
+
deleteRequestStmt.run(followRequestId.href);
|
|
273
|
+
deleteFollowerStmt.run(actorId.href);
|
|
274
|
+
this.db.exec("COMMIT");
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.db.exec("ROLLBACK");
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const actorData = JSON.parse(row.actor_json);
|
|
281
|
+
const actor = await Object$1.fromJsonLd(actorData);
|
|
282
|
+
if (isActor(actor)) return actor;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
logger.warn("Failed to parse removed follower actor", { error });
|
|
285
|
+
}
|
|
286
|
+
return void 0;
|
|
287
|
+
}
|
|
288
|
+
hasFollower(followerId) {
|
|
289
|
+
const stmt = this.db.prepare(`
|
|
290
|
+
SELECT 1 FROM followers WHERE follower_id = ?
|
|
291
|
+
`);
|
|
292
|
+
const row = stmt.get(followerId.href);
|
|
293
|
+
return Promise.resolve(row != null);
|
|
294
|
+
}
|
|
295
|
+
async *getFollowers(options = {}) {
|
|
296
|
+
const { offset = 0, limit } = options;
|
|
297
|
+
let sql = "SELECT actor_json FROM followers ORDER BY follower_id";
|
|
298
|
+
const params = [];
|
|
299
|
+
if (limit != null) {
|
|
300
|
+
sql += " LIMIT ? OFFSET ?";
|
|
301
|
+
params.push(limit, offset);
|
|
302
|
+
} else if (offset > 0) {
|
|
303
|
+
sql += " LIMIT -1 OFFSET ?";
|
|
304
|
+
params.push(offset);
|
|
305
|
+
}
|
|
306
|
+
const stmt = this.db.prepare(sql);
|
|
307
|
+
const rows = stmt.all(...params);
|
|
308
|
+
for (const row of rows) try {
|
|
309
|
+
const actorData = JSON.parse(row.actor_json);
|
|
310
|
+
const actor = await Object$1.fromJsonLd(actorData);
|
|
311
|
+
if (isActor(actor)) yield actor;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
logger.warn("Failed to parse follower actor", { error });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
countFollowers() {
|
|
318
|
+
const stmt = this.db.prepare("SELECT COUNT(*) as count FROM followers");
|
|
319
|
+
const row = stmt.get();
|
|
320
|
+
return Promise.resolve(row.count);
|
|
321
|
+
}
|
|
322
|
+
async addSentFollow(id, follow) {
|
|
323
|
+
const stmt = this.db.prepare(`
|
|
324
|
+
INSERT OR REPLACE INTO sent_follows (id, follow_json)
|
|
325
|
+
VALUES (?, ?)
|
|
326
|
+
`);
|
|
327
|
+
const followJson = JSON.stringify(await follow.toJsonLd({ format: "compact" }));
|
|
328
|
+
stmt.run(id, followJson);
|
|
329
|
+
}
|
|
330
|
+
async removeSentFollow(id) {
|
|
331
|
+
const follow = await this.getSentFollow(id);
|
|
332
|
+
if (follow == null) return void 0;
|
|
333
|
+
const stmt = this.db.prepare("DELETE FROM sent_follows WHERE id = ?");
|
|
334
|
+
stmt.run(id);
|
|
335
|
+
return follow;
|
|
336
|
+
}
|
|
337
|
+
async getSentFollow(id) {
|
|
338
|
+
const stmt = this.db.prepare(`
|
|
339
|
+
SELECT follow_json FROM sent_follows WHERE id = ?
|
|
340
|
+
`);
|
|
341
|
+
const row = stmt.get(id);
|
|
342
|
+
if (!row) return void 0;
|
|
343
|
+
try {
|
|
344
|
+
const followData = JSON.parse(row.follow_json);
|
|
345
|
+
return await Follow.fromJsonLd(followData);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
logger.warn("Failed to parse sent follow activity", {
|
|
348
|
+
id,
|
|
349
|
+
error
|
|
350
|
+
});
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async addFollowee(followeeId, follow) {
|
|
355
|
+
const stmt = this.db.prepare(`
|
|
356
|
+
INSERT OR REPLACE INTO followees (followee_id, follow_json)
|
|
357
|
+
VALUES (?, ?)
|
|
358
|
+
`);
|
|
359
|
+
const followJson = JSON.stringify(await follow.toJsonLd({ format: "compact" }));
|
|
360
|
+
stmt.run(followeeId.href, followJson);
|
|
361
|
+
}
|
|
362
|
+
async removeFollowee(followeeId) {
|
|
363
|
+
const follow = await this.getFollowee(followeeId);
|
|
364
|
+
if (follow == null) return void 0;
|
|
365
|
+
const stmt = this.db.prepare("DELETE FROM followees WHERE followee_id = ?");
|
|
366
|
+
stmt.run(followeeId.href);
|
|
367
|
+
return follow;
|
|
368
|
+
}
|
|
369
|
+
async getFollowee(followeeId) {
|
|
370
|
+
const stmt = this.db.prepare(`
|
|
371
|
+
SELECT follow_json FROM followees WHERE followee_id = ?
|
|
372
|
+
`);
|
|
373
|
+
const row = stmt.get(followeeId.href);
|
|
374
|
+
if (!row) return void 0;
|
|
375
|
+
try {
|
|
376
|
+
const followData = JSON.parse(row.follow_json);
|
|
377
|
+
return await Follow.fromJsonLd(followData);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
logger.warn("Failed to parse followee activity", {
|
|
380
|
+
followeeId: followeeId.href,
|
|
381
|
+
error
|
|
382
|
+
});
|
|
383
|
+
return void 0;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
vote(messageId, voterId, option) {
|
|
387
|
+
const stmt = this.db.prepare(`
|
|
388
|
+
INSERT OR IGNORE INTO poll_votes (message_id, voter_id, option)
|
|
389
|
+
VALUES (?, ?, ?)
|
|
390
|
+
`);
|
|
391
|
+
stmt.run(messageId, voterId.href, option);
|
|
392
|
+
return Promise.resolve();
|
|
393
|
+
}
|
|
394
|
+
countVoters(messageId) {
|
|
395
|
+
const stmt = this.db.prepare(`
|
|
396
|
+
SELECT COUNT(DISTINCT voter_id) as count
|
|
397
|
+
FROM poll_votes
|
|
398
|
+
WHERE message_id = ?
|
|
399
|
+
`);
|
|
400
|
+
const row = stmt.get(messageId);
|
|
401
|
+
return Promise.resolve(row.count);
|
|
402
|
+
}
|
|
403
|
+
countVotes(messageId) {
|
|
404
|
+
const stmt = this.db.prepare(`
|
|
405
|
+
SELECT option, COUNT(*) as count
|
|
406
|
+
FROM poll_votes
|
|
407
|
+
WHERE message_id = ?
|
|
408
|
+
GROUP BY option
|
|
409
|
+
`);
|
|
410
|
+
const rows = stmt.all(messageId);
|
|
411
|
+
const result = {};
|
|
412
|
+
for (const row of rows) result[row.option] = row.count;
|
|
413
|
+
return Promise.resolve(result);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
//#endregion
|
|
418
|
+
export { SqliteRepository };
|
|
419
|
+
//# sourceMappingURL=mod.js.map
|
package/dist/mod.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.js","names":["options: SqliteRepositoryOptions","keyPairs: CryptoKeyPair[]","id: Uuid","activity: Create | Announce","updater: (\n existing: Create | Announce,\n ) => Create | Announce | undefined | Promise<Create | Announce | undefined>","options: RepositoryGetMessagesOptions","params: (number | string)[]","followRequestId: URL","follower: Actor","actorId: URL","followerId: URL","options: RepositoryGetFollowersOptions","params: number[]","follow: Follow","followeeId: URL","messageId: Uuid","voterId: URL","option: string","result: Record<string, number>"],"sources":["../src/mod.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport type {\n Repository,\n RepositoryGetFollowersOptions,\n RepositoryGetMessagesOptions,\n Uuid,\n} from \"@fedify/botkit/repository\";\nimport { exportJwk, importJwk } from \"@fedify/fedify/sig\";\nimport {\n Activity,\n type Actor,\n Announce,\n Create,\n Follow,\n isActor,\n Object,\n} from \"@fedify/fedify/vocab\";\nimport { getLogger } from \"@logtape/logtape\";\nimport { DatabaseSync } from \"node:sqlite\";\n\nconst logger = getLogger([\"botkit\", \"sqlite\"]);\n\n/**\n * Options for creating a SQLite repository.\n * @since 0.3.0\n */\nexport interface SqliteRepositoryOptions {\n /**\n * The path to the SQLite database file.\n * If not provided, an in-memory database will be used.\n */\n readonly path?: string;\n\n /**\n * Whether to enable Write-Ahead Logging (WAL) mode.\n * @default true\n */\n readonly wal?: boolean;\n}\n\n/**\n * A repository for storing bot data using SQLite.\n * @since 0.3.0\n */\nexport class SqliteRepository implements Repository, Disposable {\n private readonly db: DatabaseSync;\n\n /**\n * Creates a new SQLite repository.\n * @param options The options for creating the repository.\n */\n constructor(options: SqliteRepositoryOptions = {}) {\n const { path = \":memory:\", wal = true } = options;\n\n this.db = new DatabaseSync(path);\n\n // Enable foreign key constraints\n this.db.exec(\"PRAGMA foreign_keys = ON;\");\n\n if (wal && path !== \":memory:\") {\n this.db.exec(\"PRAGMA journal_mode = WAL;\");\n }\n\n this.initializeTables();\n }\n\n [Symbol.dispose]() {\n this.close();\n }\n\n /**\n * Closes the database connection.\n */\n close(): void {\n this.db.close();\n }\n\n private initializeTables(): void {\n // Key pairs table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS key_pairs (\n id INTEGER PRIMARY KEY,\n private_key_jwk TEXT NOT NULL,\n public_key_jwk TEXT NOT NULL\n )\n `);\n\n // Messages table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n activity_json TEXT NOT NULL,\n published INTEGER\n )\n `);\n\n // Create index on published timestamp for efficient ordering\n this.db.exec(`\n CREATE INDEX IF NOT EXISTS idx_messages_published ON messages(published)\n `);\n\n // Followers table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS followers (\n follower_id TEXT PRIMARY KEY,\n actor_json TEXT NOT NULL\n )\n `);\n\n // Follow requests mapping table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS follow_requests (\n follow_request_id TEXT PRIMARY KEY,\n follower_id TEXT NOT NULL,\n FOREIGN KEY (follower_id) REFERENCES followers(follower_id)\n )\n `);\n\n // Sent follows table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS sent_follows (\n id TEXT PRIMARY KEY,\n follow_json TEXT NOT NULL\n )\n `);\n\n // Followees table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS followees (\n followee_id TEXT PRIMARY KEY,\n follow_json TEXT NOT NULL\n )\n `);\n\n // Poll votes table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS poll_votes (\n message_id TEXT NOT NULL,\n voter_id TEXT NOT NULL,\n option TEXT NOT NULL,\n PRIMARY KEY (message_id, voter_id, option)\n )\n `);\n\n // Create index for efficient vote counting\n this.db.exec(`\n CREATE INDEX IF NOT EXISTS idx_poll_votes_message_option \n ON poll_votes(message_id, option)\n `);\n }\n\n async setKeyPairs(keyPairs: CryptoKeyPair[]): Promise<void> {\n const deleteStmt = this.db.prepare(\"DELETE FROM key_pairs\");\n const insertStmt = this.db.prepare(`\n INSERT INTO key_pairs (private_key_jwk, public_key_jwk) \n VALUES (?, ?)\n `);\n\n this.db.exec(\"BEGIN TRANSACTION\");\n try {\n deleteStmt.run();\n\n for (const keyPair of keyPairs) {\n const privateJwk = await exportJwk(keyPair.privateKey);\n const publicJwk = await exportJwk(keyPair.publicKey);\n insertStmt.run(JSON.stringify(privateJwk), JSON.stringify(publicJwk));\n }\n\n this.db.exec(\"COMMIT\");\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n }\n\n async getKeyPairs(): Promise<CryptoKeyPair[] | undefined> {\n const stmt = this.db.prepare(`\n SELECT private_key_jwk, public_key_jwk FROM key_pairs\n `);\n const rows = stmt.all() as Array<{\n private_key_jwk: string;\n public_key_jwk: string;\n }>;\n\n if (rows.length === 0) return undefined;\n\n const keyPairs: CryptoKeyPair[] = [];\n for (const row of rows) {\n const privateJwk = JSON.parse(row.private_key_jwk);\n const publicJwk = JSON.parse(row.public_key_jwk);\n\n keyPairs.push({\n privateKey: await importJwk(privateJwk, \"private\"),\n publicKey: await importJwk(publicJwk, \"public\"),\n });\n }\n\n return keyPairs;\n }\n\n async addMessage(id: Uuid, activity: Create | Announce): Promise<void> {\n const stmt = this.db.prepare(`\n INSERT INTO messages (id, activity_json, published) \n VALUES (?, ?, ?)\n `);\n\n const activityJson = JSON.stringify(\n await activity.toJsonLd({ format: \"compact\" }),\n );\n const published = activity.published?.epochMilliseconds ?? null;\n\n stmt.run(id, activityJson, published);\n }\n\n async updateMessage(\n id: Uuid,\n updater: (\n existing: Create | Announce,\n ) => Create | Announce | undefined | Promise<Create | Announce | undefined>,\n ): Promise<boolean> {\n const selectStmt = this.db.prepare(`\n SELECT activity_json FROM messages WHERE id = ?\n `);\n const row = selectStmt.get(id) as { activity_json: string } | undefined;\n\n if (!row) return false;\n\n const activityData = JSON.parse(row.activity_json);\n const activity = await Activity.fromJsonLd(activityData);\n\n if (!(activity instanceof Create || activity instanceof Announce)) {\n return false;\n }\n\n const newActivity = await updater(activity);\n if (newActivity == null) return false;\n\n const updateStmt = this.db.prepare(`\n UPDATE messages \n SET activity_json = ?, published = ? \n WHERE id = ?\n `);\n\n const newActivityJson = JSON.stringify(\n await newActivity.toJsonLd({ format: \"compact\" }),\n );\n const published = newActivity.published?.epochMilliseconds ?? null;\n\n updateStmt.run(newActivityJson, published, id);\n return true;\n }\n\n async removeMessage(id: Uuid): Promise<Create | Announce | undefined> {\n const selectStmt = this.db.prepare(`\n SELECT activity_json FROM messages WHERE id = ?\n `);\n const row = selectStmt.get(id) as { activity_json: string } | undefined;\n\n if (!row) return undefined;\n\n const deleteStmt = this.db.prepare(`\n DELETE FROM messages WHERE id = ?\n `);\n deleteStmt.run(id);\n\n try {\n const activityData = JSON.parse(row.activity_json);\n const activity = await Activity.fromJsonLd(activityData);\n\n if (activity instanceof Create || activity instanceof Announce) {\n return activity;\n }\n } catch (error) {\n logger.warn(\"Failed to parse removed message activity\", { id, error });\n }\n\n return undefined;\n }\n\n async *getMessages(\n options: RepositoryGetMessagesOptions = {},\n ): AsyncIterable<Create | Announce> {\n const { order = \"newest\", until, since, limit } = options;\n\n let sql = \"SELECT activity_json FROM messages WHERE 1=1\";\n const params: (number | string)[] = [];\n\n if (since != null) {\n sql += \" AND published >= ?\";\n params.push(since.epochMilliseconds);\n }\n\n if (until != null) {\n sql += \" AND published <= ?\";\n params.push(until.epochMilliseconds);\n }\n\n sql += order === \"oldest\"\n ? \" ORDER BY published ASC\"\n : \" ORDER BY published DESC\";\n\n if (limit != null) {\n sql += \" LIMIT ?\";\n params.push(limit);\n }\n\n const stmt = this.db.prepare(sql);\n const rows = stmt.all(...params) as Array<{ activity_json: string }>;\n\n for (const row of rows) {\n try {\n const activityData = JSON.parse(row.activity_json);\n const activity = await Activity.fromJsonLd(activityData);\n\n if (activity instanceof Create || activity instanceof Announce) {\n yield activity;\n }\n } catch (error) {\n logger.warn(\"Failed to parse message activity\", { error });\n continue;\n }\n }\n }\n\n async getMessage(id: Uuid): Promise<Create | Announce | undefined> {\n const stmt = this.db.prepare(`\n SELECT activity_json FROM messages WHERE id = ?\n `);\n const row = stmt.get(id) as { activity_json: string } | undefined;\n\n if (!row) return undefined;\n\n try {\n const activityData = JSON.parse(row.activity_json);\n const activity = await Activity.fromJsonLd(activityData);\n\n if (activity instanceof Create || activity instanceof Announce) {\n return activity;\n }\n } catch (error) {\n logger.warn(\"Failed to parse message activity\", { id, error });\n }\n\n return undefined;\n }\n\n countMessages(): Promise<number> {\n const stmt = this.db.prepare(\"SELECT COUNT(*) as count FROM messages\");\n const row = stmt.get() as { count: number };\n return Promise.resolve(row.count);\n }\n\n async addFollower(followRequestId: URL, follower: Actor): Promise<void> {\n if (follower.id == null) {\n throw new TypeError(\"The follower ID is missing.\");\n }\n\n const followerJson = JSON.stringify(\n await follower.toJsonLd({ format: \"compact\" }),\n );\n\n const insertFollowerStmt = this.db.prepare(`\n INSERT OR REPLACE INTO followers (follower_id, actor_json) \n VALUES (?, ?)\n `);\n\n const insertRequestStmt = this.db.prepare(`\n INSERT OR REPLACE INTO follow_requests (follow_request_id, follower_id) \n VALUES (?, ?)\n `);\n\n this.db.exec(\"BEGIN TRANSACTION\");\n try {\n insertFollowerStmt.run(follower.id.href, followerJson);\n insertRequestStmt.run(followRequestId.href, follower.id.href);\n this.db.exec(\"COMMIT\");\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n }\n\n async removeFollower(\n followRequestId: URL,\n actorId: URL,\n ): Promise<Actor | undefined> {\n // Check if the follow request exists and matches the actor\n const checkStmt = this.db.prepare(`\n SELECT fr.follower_id, f.actor_json \n FROM follow_requests fr \n JOIN followers f ON fr.follower_id = f.follower_id \n WHERE fr.follow_request_id = ? AND fr.follower_id = ?\n `);\n\n const row = checkStmt.get(followRequestId.href, actorId.href) as {\n follower_id: string;\n actor_json: string;\n } | undefined;\n\n if (!row) return undefined;\n\n // Remove the follower and follow request\n const deleteRequestStmt = this.db.prepare(`\n DELETE FROM follow_requests WHERE follow_request_id = ?\n `);\n\n const deleteFollowerStmt = this.db.prepare(`\n DELETE FROM followers WHERE follower_id = ?\n `);\n\n this.db.exec(\"BEGIN TRANSACTION\");\n try {\n deleteRequestStmt.run(followRequestId.href);\n deleteFollowerStmt.run(actorId.href);\n this.db.exec(\"COMMIT\");\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n\n try {\n const actorData = JSON.parse(row.actor_json);\n const actor = await Object.fromJsonLd(actorData);\n\n if (isActor(actor)) {\n return actor;\n }\n } catch (error) {\n logger.warn(\"Failed to parse removed follower actor\", { error });\n }\n\n return undefined;\n }\n\n hasFollower(followerId: URL): Promise<boolean> {\n const stmt = this.db.prepare(`\n SELECT 1 FROM followers WHERE follower_id = ?\n `);\n const row = stmt.get(followerId.href);\n return Promise.resolve(row != null);\n }\n\n async *getFollowers(\n options: RepositoryGetFollowersOptions = {},\n ): AsyncIterable<Actor> {\n const { offset = 0, limit } = options;\n\n let sql = \"SELECT actor_json FROM followers ORDER BY follower_id\";\n const params: number[] = [];\n\n if (limit != null) {\n sql += \" LIMIT ? OFFSET ?\";\n params.push(limit, offset);\n } else if (offset > 0) {\n sql += \" LIMIT -1 OFFSET ?\";\n params.push(offset);\n }\n\n const stmt = this.db.prepare(sql);\n const rows = stmt.all(...params) as { actor_json: string }[];\n\n for (const row of rows) {\n try {\n const actorData = JSON.parse(row.actor_json);\n const actor = await Object.fromJsonLd(actorData);\n\n if (isActor(actor)) {\n yield actor;\n }\n } catch (error) {\n logger.warn(\"Failed to parse follower actor\", { error });\n continue;\n }\n }\n }\n\n countFollowers(): Promise<number> {\n const stmt = this.db.prepare(\"SELECT COUNT(*) as count FROM followers\");\n const row = stmt.get() as { count: number };\n return Promise.resolve(row.count);\n }\n\n async addSentFollow(id: Uuid, follow: Follow): Promise<void> {\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO sent_follows (id, follow_json) \n VALUES (?, ?)\n `);\n\n const followJson = JSON.stringify(\n await follow.toJsonLd({ format: \"compact\" }),\n );\n\n stmt.run(id, followJson);\n }\n\n async removeSentFollow(id: Uuid): Promise<Follow | undefined> {\n const follow = await this.getSentFollow(id);\n if (follow == null) return undefined;\n\n const stmt = this.db.prepare(\"DELETE FROM sent_follows WHERE id = ?\");\n stmt.run(id);\n\n return follow;\n }\n\n async getSentFollow(id: Uuid): Promise<Follow | undefined> {\n const stmt = this.db.prepare(`\n SELECT follow_json FROM sent_follows WHERE id = ?\n `);\n const row = stmt.get(id) as { follow_json: string } | undefined;\n\n if (!row) return undefined;\n\n try {\n const followData = JSON.parse(row.follow_json);\n return await Follow.fromJsonLd(followData);\n } catch (error) {\n logger.warn(\"Failed to parse sent follow activity\", { id, error });\n return undefined;\n }\n }\n\n async addFollowee(followeeId: URL, follow: Follow): Promise<void> {\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO followees (followee_id, follow_json) \n VALUES (?, ?)\n `);\n\n const followJson = JSON.stringify(\n await follow.toJsonLd({ format: \"compact\" }),\n );\n\n stmt.run(followeeId.href, followJson);\n }\n\n async removeFollowee(followeeId: URL): Promise<Follow | undefined> {\n const follow = await this.getFollowee(followeeId);\n if (follow == null) return undefined;\n\n const stmt = this.db.prepare(\"DELETE FROM followees WHERE followee_id = ?\");\n stmt.run(followeeId.href);\n\n return follow;\n }\n\n async getFollowee(followeeId: URL): Promise<Follow | undefined> {\n const stmt = this.db.prepare(`\n SELECT follow_json FROM followees WHERE followee_id = ?\n `);\n const row = stmt.get(followeeId.href) as\n | { follow_json: string }\n | undefined;\n\n if (!row) return undefined;\n\n try {\n const followData = JSON.parse(row.follow_json);\n return await Follow.fromJsonLd(followData);\n } catch (error) {\n logger.warn(\"Failed to parse followee activity\", {\n followeeId: followeeId.href,\n error,\n });\n return undefined;\n }\n }\n\n vote(messageId: Uuid, voterId: URL, option: string): Promise<void> {\n const stmt = this.db.prepare(`\n INSERT OR IGNORE INTO poll_votes (message_id, voter_id, option) \n VALUES (?, ?, ?)\n `);\n\n stmt.run(messageId, voterId.href, option);\n return Promise.resolve();\n }\n\n countVoters(messageId: Uuid): Promise<number> {\n const stmt = this.db.prepare(`\n SELECT COUNT(DISTINCT voter_id) as count \n FROM poll_votes \n WHERE message_id = ?\n `);\n const row = stmt.get(messageId) as { count: number };\n return Promise.resolve(row.count);\n }\n\n countVotes(messageId: Uuid): Promise<Readonly<Record<string, number>>> {\n const stmt = this.db.prepare(`\n SELECT option, COUNT(*) as count \n FROM poll_votes \n WHERE message_id = ? \n GROUP BY option\n `);\n const rows = stmt.all(messageId) as Array<{\n option: string;\n count: number;\n }>;\n\n const result: Record<string, number> = {};\n for (const row of rows) {\n result[row.option] = row.count;\n }\n\n return Promise.resolve(result);\n }\n}\n"],"mappings":"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"}
|