@chronicle.app/twitter 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/connectors/TwitterTransformer.d.ts +14 -0
- package/dist/connectors/TwitterTransformer.js +213 -0
- package/dist/connectors/archive/TwitterArchiveDirectMessagesExtractor.d.ts +38 -0
- package/dist/connectors/archive/TwitterArchiveDirectMessagesExtractor.js +193 -0
- package/dist/connectors/archive/TwitterArchiveExtractor.d.ts +36 -0
- package/dist/connectors/archive/TwitterArchiveExtractor.js +51 -0
- package/dist/connectors/archive/TwitterArchiveLikesExtractor.d.ts +35 -0
- package/dist/connectors/archive/TwitterArchiveLikesExtractor.js +59 -0
- package/dist/connectors/archive/TwitterArchiveTweetsExtractor.d.ts +36 -0
- package/dist/connectors/archive/TwitterArchiveTweetsExtractor.js +67 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Louis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @chronicle.app/twitter
|
|
2
|
+
|
|
3
|
+
Chronicle plugin for twitter
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @chronicle.app/twitter
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic extraction
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
chronicle extract twitter --input /path/to/twitter/export
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### With date filtering
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
chronicle extract twitter \
|
|
23
|
+
--input /path/to/twitter/export \
|
|
24
|
+
--since 2024-01-01 \
|
|
25
|
+
--until 2024-12-31 \
|
|
26
|
+
--limit 1000
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
The extractor supports the following options:
|
|
32
|
+
|
|
33
|
+
- `--input` (required): Path to the Twitter export data directory
|
|
34
|
+
- `--type`: Which record types to read: `tweets`, `likes`, `direct-messages`,
|
|
35
|
+
`group-direct-messages` (default: all of them)
|
|
36
|
+
- `--include-group-dms`: Include group direct messages (default: true)
|
|
37
|
+
- `--since`: Extract records from this date onwards
|
|
38
|
+
- `--until`: Extract records up to this date
|
|
39
|
+
- `--limit`: Maximum number of records to extract
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Build the plugin
|
|
45
|
+
npm run build
|
|
46
|
+
|
|
47
|
+
# Run the tests
|
|
48
|
+
npm test
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## File Format
|
|
52
|
+
|
|
53
|
+
The input is the unzipped directory of a Twitter/X data archive. The plugin
|
|
54
|
+
reads these files from its `data/` folder, each a `window.YTD.<name>.part0 = [...]`
|
|
55
|
+
script wrapping a JSON array:
|
|
56
|
+
|
|
57
|
+
- `account.js`: the archive owner's account ID, username, and display name
|
|
58
|
+
- `tweets.js`: your tweets
|
|
59
|
+
- `like.js`: the tweets you liked
|
|
60
|
+
- `direct-messages.js`: one-to-one direct messages
|
|
61
|
+
- `direct-messages-group.js` and `direct-message-group-headers.js`: group direct
|
|
62
|
+
messages and their participants
|
|
63
|
+
|
|
64
|
+
## Schema
|
|
65
|
+
|
|
66
|
+
- Each tweet is a `PublishAction` whose object is a `Post`. The post's
|
|
67
|
+
`references` are the accounts it mentions and the links it contains.
|
|
68
|
+
- Each like is a `LikeAction` on the liked `Post`.
|
|
69
|
+
- Each direct message is a `MessageAction` whose object is a `Message`, sent to
|
|
70
|
+
the other participants of the conversation.
|
|
71
|
+
|
|
72
|
+
The archive owner is an `Agent` linked to `@me`; other accounts are `Agent`s
|
|
73
|
+
keyed by their Twitter user ID.
|
|
74
|
+
|
|
75
|
+
Tests build a small synthetic archive in a temporary directory and never read
|
|
76
|
+
the host's data or accounts.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ChronicleTransformer, Record } from '@chronicle.app/etl';
|
|
2
|
+
import { ActionAndChildren } from '@chronicle.app/schema';
|
|
3
|
+
export default class TwitterTransformer extends ChronicleTransformer {
|
|
4
|
+
transform(record: Record): Promise<ActionAndChildren[]>;
|
|
5
|
+
private buildTweetActions;
|
|
6
|
+
private buildLikeActions;
|
|
7
|
+
private buildDirectMessageActions;
|
|
8
|
+
private buildAccountUser;
|
|
9
|
+
private buildPersonFromUserId;
|
|
10
|
+
private extractMentionedUsersFromEntities;
|
|
11
|
+
private extractUrlReferencesFromEntities;
|
|
12
|
+
private buildGroupDirectMessageActions;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=TwitterTransformer.d.ts.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ChronicleTransformer } from '@chronicle.app/etl';
|
|
2
|
+
export default class TwitterTransformer extends ChronicleTransformer {
|
|
3
|
+
async transform(record) {
|
|
4
|
+
const actions = [];
|
|
5
|
+
switch (record.extraction.recordType) {
|
|
6
|
+
case 'tweets':
|
|
7
|
+
actions.push(...(await this.buildTweetActions(record)));
|
|
8
|
+
break;
|
|
9
|
+
case 'likes':
|
|
10
|
+
actions.push(...this.buildLikeActions(record));
|
|
11
|
+
break;
|
|
12
|
+
case 'direct-messages':
|
|
13
|
+
actions.push(...this.buildDirectMessageActions(record));
|
|
14
|
+
break;
|
|
15
|
+
case 'group-direct-messages':
|
|
16
|
+
actions.push(...this.buildGroupDirectMessageActions(record));
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
return actions;
|
|
20
|
+
}
|
|
21
|
+
async buildTweetActions(record) {
|
|
22
|
+
const actions = [];
|
|
23
|
+
const user = this.buildAccountUser(record);
|
|
24
|
+
const mentions = this.extractMentionedUsersFromEntities(record.data.entities, record.data.text);
|
|
25
|
+
const urls = this.extractUrlReferencesFromEntities(record.data.entities, record.data.text);
|
|
26
|
+
const references = [...mentions, ...urls];
|
|
27
|
+
const tweet = {
|
|
28
|
+
'@type': 'Post',
|
|
29
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
30
|
+
source: 'twitter',
|
|
31
|
+
sourceId: record.data.id,
|
|
32
|
+
body: record.data.text, // Keep original text with t.co URLs
|
|
33
|
+
author: [user],
|
|
34
|
+
...(references.length > 0 && { references }),
|
|
35
|
+
};
|
|
36
|
+
const publishAction = {
|
|
37
|
+
'@type': 'PublishAction',
|
|
38
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
39
|
+
source: 'twitter',
|
|
40
|
+
sourceId: record.data.id,
|
|
41
|
+
timestamp: record.data.created_at,
|
|
42
|
+
agent: user,
|
|
43
|
+
object: tweet,
|
|
44
|
+
};
|
|
45
|
+
actions.push(publishAction);
|
|
46
|
+
return actions;
|
|
47
|
+
}
|
|
48
|
+
buildLikeActions(record) {
|
|
49
|
+
const actions = [];
|
|
50
|
+
const user = this.buildAccountUser(record);
|
|
51
|
+
const targetTweet = {
|
|
52
|
+
'@type': 'Post',
|
|
53
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
54
|
+
source: 'twitter',
|
|
55
|
+
sourceId: record.data.tweetId,
|
|
56
|
+
body: record.data.fullText,
|
|
57
|
+
};
|
|
58
|
+
const likeAction = {
|
|
59
|
+
'@type': 'LikeAction',
|
|
60
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
61
|
+
source: 'twitter',
|
|
62
|
+
sourceId: `${record.data.tweetId}-like`,
|
|
63
|
+
agent: user,
|
|
64
|
+
object: targetTweet,
|
|
65
|
+
};
|
|
66
|
+
actions.push(likeAction);
|
|
67
|
+
return actions;
|
|
68
|
+
}
|
|
69
|
+
buildDirectMessageActions(record) {
|
|
70
|
+
const actions = [];
|
|
71
|
+
const sender = this.buildPersonFromUserId(record.data.senderId);
|
|
72
|
+
const recipient = this.buildPersonFromUserId(record.data.recipientId);
|
|
73
|
+
const message = {
|
|
74
|
+
'@type': 'Message',
|
|
75
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
76
|
+
source: 'twitter',
|
|
77
|
+
sourceId: record.data.id,
|
|
78
|
+
body: record.data.text,
|
|
79
|
+
recipient: [recipient],
|
|
80
|
+
author: [sender],
|
|
81
|
+
};
|
|
82
|
+
// Media URLs would need to be handled separately or added to schema
|
|
83
|
+
const sendAction = {
|
|
84
|
+
'@type': 'MessageAction',
|
|
85
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
86
|
+
source: 'twitter',
|
|
87
|
+
sourceId: record.data.id,
|
|
88
|
+
timestamp: record.data.created_at,
|
|
89
|
+
agent: sender,
|
|
90
|
+
object: message,
|
|
91
|
+
};
|
|
92
|
+
actions.push(sendAction);
|
|
93
|
+
return actions;
|
|
94
|
+
}
|
|
95
|
+
buildAccountUser(record) {
|
|
96
|
+
// const strategy = record.context?.strategy || 'archive'; // Future use
|
|
97
|
+
const accountInfo = record.context?.accountInfo;
|
|
98
|
+
if (!accountInfo) {
|
|
99
|
+
throw new Error('Account information is required to build user agent. Ensure account.js is present in Twitter export.');
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
'@type': 'Agent',
|
|
103
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
104
|
+
source: 'twitter',
|
|
105
|
+
sourceId: accountInfo.accountId,
|
|
106
|
+
handle: accountInfo.username,
|
|
107
|
+
name: accountInfo.accountDisplayName,
|
|
108
|
+
url: `https://twitter.com/${accountInfo.username}`,
|
|
109
|
+
sameAs: ['@me'],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
buildPersonFromUserId(userId) {
|
|
113
|
+
return {
|
|
114
|
+
'@type': 'Agent',
|
|
115
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
116
|
+
source: 'twitter',
|
|
117
|
+
sourceId: userId,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
extractMentionedUsersFromEntities(entities, text) {
|
|
121
|
+
const mentions = [];
|
|
122
|
+
// Primary: Use entities.user_mentions for structured data
|
|
123
|
+
if (entities?.user_mentions && entities.user_mentions.length > 0) {
|
|
124
|
+
for (const mention of entities.user_mentions) {
|
|
125
|
+
mentions.push({
|
|
126
|
+
'@type': 'Agent',
|
|
127
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
128
|
+
source: 'twitter',
|
|
129
|
+
sourceId: mention.id_str,
|
|
130
|
+
handle: mention.screen_name,
|
|
131
|
+
name: mention.name,
|
|
132
|
+
url: `https://twitter.com/${mention.screen_name}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Fallback: Use regex parsing for edge cases where entities is empty
|
|
138
|
+
const mentionRegex = /@([a-zA-Z0-9_]+)/g;
|
|
139
|
+
let match;
|
|
140
|
+
while ((match = mentionRegex.exec(text)) !== null) {
|
|
141
|
+
const username = match[1];
|
|
142
|
+
mentions.push({
|
|
143
|
+
'@type': 'Agent',
|
|
144
|
+
'@key': ['@type', 'source', 'handle'],
|
|
145
|
+
source: 'twitter',
|
|
146
|
+
handle: username,
|
|
147
|
+
url: `https://twitter.com/${username}`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return mentions;
|
|
152
|
+
}
|
|
153
|
+
extractUrlReferencesFromEntities(entities, text) {
|
|
154
|
+
const urls = [];
|
|
155
|
+
// Primary: Use entities.urls for structured data
|
|
156
|
+
if (entities?.urls && entities.urls.length > 0) {
|
|
157
|
+
for (const urlEntity of entities.urls) {
|
|
158
|
+
// Use expanded URL if available, otherwise fall back to original
|
|
159
|
+
const finalUrl = urlEntity.expanded_url || urlEntity.url;
|
|
160
|
+
urls.push({
|
|
161
|
+
'@type': 'Entity',
|
|
162
|
+
'@key': ['@type', 'url'],
|
|
163
|
+
url: finalUrl,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Fallback: Use regex parsing for edge cases where entities is empty
|
|
169
|
+
const urlRegex = /https?:\/\/[^\s)]+/g;
|
|
170
|
+
let match;
|
|
171
|
+
while ((match = urlRegex.exec(text)) !== null) {
|
|
172
|
+
let url = match[0];
|
|
173
|
+
// Remove trailing punctuation that shouldn't be part of the URL
|
|
174
|
+
url = url.replace(/[).,;!?]+$/, '');
|
|
175
|
+
urls.push({
|
|
176
|
+
'@type': 'Entity',
|
|
177
|
+
'@key': ['@type', 'url'],
|
|
178
|
+
url,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return urls;
|
|
183
|
+
}
|
|
184
|
+
buildGroupDirectMessageActions(record) {
|
|
185
|
+
const actions = [];
|
|
186
|
+
const sender = this.buildPersonFromUserId(record.data.senderId);
|
|
187
|
+
// Build recipients from all participants except the sender
|
|
188
|
+
const participants = record.data.participants || [];
|
|
189
|
+
const recipients = participants
|
|
190
|
+
.filter((participantId) => participantId !== record.data.senderId)
|
|
191
|
+
.map((participantId) => this.buildPersonFromUserId(participantId));
|
|
192
|
+
const message = {
|
|
193
|
+
'@type': 'Message',
|
|
194
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
195
|
+
source: 'twitter',
|
|
196
|
+
sourceId: record.data.id,
|
|
197
|
+
body: record.data.text,
|
|
198
|
+
recipient: recipients,
|
|
199
|
+
author: [sender],
|
|
200
|
+
};
|
|
201
|
+
const sendAction = {
|
|
202
|
+
'@type': 'MessageAction',
|
|
203
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
204
|
+
source: 'twitter',
|
|
205
|
+
sourceId: record.data.id,
|
|
206
|
+
timestamp: record.data.created_at,
|
|
207
|
+
agent: sender,
|
|
208
|
+
object: message,
|
|
209
|
+
};
|
|
210
|
+
actions.push(sendAction);
|
|
211
|
+
return actions;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Record } from '@chronicle.app/etl';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import TwitterTransformer from '../TwitterTransformer.js';
|
|
4
|
+
import { TwitterArchiveExtractor } from './TwitterArchiveExtractor.js';
|
|
5
|
+
export declare class TwitterArchiveDirectMessagesExtractor extends TwitterArchiveExtractor<typeof TwitterArchiveDirectMessagesExtractor> {
|
|
6
|
+
static description: string;
|
|
7
|
+
static recordTypes: string[];
|
|
8
|
+
static defaultTransformer: typeof TwitterTransformer;
|
|
9
|
+
static newestFirst: boolean;
|
|
10
|
+
static schema: z.ZodObject<{
|
|
11
|
+
since: z.ZodOptional<z.ZodDate>;
|
|
12
|
+
until: z.ZodOptional<z.ZodDate>;
|
|
13
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
} & {
|
|
15
|
+
input: z.ZodString;
|
|
16
|
+
'include-group-dms': z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
input: string;
|
|
19
|
+
'include-group-dms': boolean;
|
|
20
|
+
since?: Date | undefined;
|
|
21
|
+
until?: Date | undefined;
|
|
22
|
+
limit?: number | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
input: string;
|
|
25
|
+
'include-group-dms'?: boolean | undefined;
|
|
26
|
+
since?: Date | undefined;
|
|
27
|
+
until?: Date | undefined;
|
|
28
|
+
limit?: number | undefined;
|
|
29
|
+
}>;
|
|
30
|
+
/** The DM's message id — Twitter's own id, identical through any tool. */
|
|
31
|
+
keyOf(record: Record): string | null;
|
|
32
|
+
extract(): AsyncGenerator<Record>;
|
|
33
|
+
private loadDirectMessages;
|
|
34
|
+
private loadGroupHeaders;
|
|
35
|
+
private loadGroupDirectMessages;
|
|
36
|
+
determineCount(): Promise<number | null>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=TwitterArchiveDirectMessagesExtractor.d.ts.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import TwitterTransformer from '../TwitterTransformer.js';
|
|
3
|
+
import { TwitterArchiveExtractor } from './TwitterArchiveExtractor.js';
|
|
4
|
+
export class TwitterArchiveDirectMessagesExtractor extends TwitterArchiveExtractor {
|
|
5
|
+
static description = 'Direct messages';
|
|
6
|
+
static recordTypes = ['direct-messages', 'group-direct-messages'];
|
|
7
|
+
static defaultTransformer = TwitterTransformer;
|
|
8
|
+
// extract() sorts every message by created_at descending before yielding,
|
|
9
|
+
// which with keyOf opts this extractor into the frontier cursor.
|
|
10
|
+
static newestFirst = true;
|
|
11
|
+
static schema = TwitterArchiveExtractor.schema;
|
|
12
|
+
/** The DM's message id — Twitter's own id, identical through any tool. */
|
|
13
|
+
keyOf(record) {
|
|
14
|
+
return record.data.id ?? null;
|
|
15
|
+
}
|
|
16
|
+
async *extract() {
|
|
17
|
+
const config = this.config;
|
|
18
|
+
try {
|
|
19
|
+
// Load account info first
|
|
20
|
+
await this.loadAccountInfoFrom(config.input);
|
|
21
|
+
// Collect all messages from both sources
|
|
22
|
+
const allMessages = [];
|
|
23
|
+
// Process 1:1 direct messages
|
|
24
|
+
const oneOnOneMessages = await this.loadDirectMessages(`${config.input}/data/direct-messages.js`);
|
|
25
|
+
allMessages.push(...oneOnOneMessages);
|
|
26
|
+
// Process group direct messages (if enabled)
|
|
27
|
+
if (config['include-group-dms']) {
|
|
28
|
+
const groupMessages = await this.loadGroupDirectMessages(`${config.input}/data/direct-messages-group.js`);
|
|
29
|
+
allMessages.push(...groupMessages);
|
|
30
|
+
}
|
|
31
|
+
// Sort by timestamp descending (newest first)
|
|
32
|
+
allMessages.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
33
|
+
// Yield messages in chronological order (newest first)
|
|
34
|
+
let count = 0;
|
|
35
|
+
for (const message of allMessages) {
|
|
36
|
+
if (this.shouldStopExtracting(count))
|
|
37
|
+
break;
|
|
38
|
+
if (this.isWithinDateRange(new Date(message.created_at))) {
|
|
39
|
+
yield this.createRecordWithArchiveContext(message.data, message.metadata);
|
|
40
|
+
count++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new Error(`Failed to process Twitter direct messages: ${error}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async loadDirectMessages(filePath) {
|
|
49
|
+
const messages = [];
|
|
50
|
+
try {
|
|
51
|
+
const content = await readFile(filePath, 'utf-8');
|
|
52
|
+
const jsData = this.parseTwitterJS(content, 'direct_messages');
|
|
53
|
+
for (const item of jsData) {
|
|
54
|
+
const conversation = item.dmConversation;
|
|
55
|
+
if (conversation && conversation.messages) {
|
|
56
|
+
for (const message of conversation.messages) {
|
|
57
|
+
const msg = message.messageCreate;
|
|
58
|
+
if (msg) {
|
|
59
|
+
messages.push({
|
|
60
|
+
created_at: msg.createdAt,
|
|
61
|
+
data: {
|
|
62
|
+
type: 'direct-message',
|
|
63
|
+
id: msg.id,
|
|
64
|
+
conversationId: conversation.conversationId,
|
|
65
|
+
senderId: msg.senderId,
|
|
66
|
+
recipientId: msg.recipientId,
|
|
67
|
+
text: msg.text,
|
|
68
|
+
created_at: msg.createdAt,
|
|
69
|
+
mediaUrls: msg.mediaUrls,
|
|
70
|
+
urls: msg.urls,
|
|
71
|
+
},
|
|
72
|
+
metadata: { originalData: msg },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.warn(`Could not process 1:1 direct messages: ${error}`);
|
|
81
|
+
}
|
|
82
|
+
return messages;
|
|
83
|
+
}
|
|
84
|
+
async loadGroupHeaders(filePath) {
|
|
85
|
+
const groupHeaders = new Map();
|
|
86
|
+
try {
|
|
87
|
+
const content = await readFile(filePath, 'utf-8');
|
|
88
|
+
const jsData = this.parseTwitterJS(content, 'direct_message_group_headers');
|
|
89
|
+
for (const item of jsData) {
|
|
90
|
+
const conversation = item.dmConversation;
|
|
91
|
+
if (conversation) {
|
|
92
|
+
// Find the joinConversation event to get participants
|
|
93
|
+
const joinEvent = conversation.messages?.find((msg) => msg.joinConversation);
|
|
94
|
+
if (joinEvent) {
|
|
95
|
+
groupHeaders.set(conversation.conversationId, {
|
|
96
|
+
conversationId: conversation.conversationId,
|
|
97
|
+
participants: joinEvent.joinConversation.participantsSnapshot || [],
|
|
98
|
+
initiator: joinEvent.joinConversation.initiatingUserId,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.warn(`Could not load group headers: ${error}`);
|
|
106
|
+
}
|
|
107
|
+
return groupHeaders;
|
|
108
|
+
}
|
|
109
|
+
async loadGroupDirectMessages(filePath) {
|
|
110
|
+
const messages = [];
|
|
111
|
+
try {
|
|
112
|
+
// First load group headers to get participant info
|
|
113
|
+
const groupHeaders = await this.loadGroupHeaders(`${filePath.replace('direct-messages-group.js', 'direct-message-group-headers.js')}`);
|
|
114
|
+
const content = await readFile(filePath, 'utf-8');
|
|
115
|
+
const jsData = this.parseTwitterJS(content, 'direct_messages_group');
|
|
116
|
+
for (const item of jsData) {
|
|
117
|
+
const conversation = item.dmConversation;
|
|
118
|
+
if (conversation && conversation.messages) {
|
|
119
|
+
const groupInfo = groupHeaders.get(conversation.conversationId);
|
|
120
|
+
for (const message of conversation.messages) {
|
|
121
|
+
// Only process messageCreate events (actual messages), skip other events
|
|
122
|
+
if (message.messageCreate) {
|
|
123
|
+
const msg = message.messageCreate;
|
|
124
|
+
messages.push({
|
|
125
|
+
created_at: msg.createdAt,
|
|
126
|
+
data: {
|
|
127
|
+
type: 'group-direct-message',
|
|
128
|
+
id: msg.id,
|
|
129
|
+
conversationId: conversation.conversationId,
|
|
130
|
+
senderId: msg.senderId,
|
|
131
|
+
participants: groupInfo?.participants || [],
|
|
132
|
+
text: msg.text || '',
|
|
133
|
+
created_at: msg.createdAt,
|
|
134
|
+
mediaUrls: msg.mediaUrls || [],
|
|
135
|
+
urls: msg.urls || [],
|
|
136
|
+
},
|
|
137
|
+
metadata: {
|
|
138
|
+
recordType: 'group-direct-messages',
|
|
139
|
+
originalData: msg,
|
|
140
|
+
groupInfo,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.warn(`Could not process group direct messages: ${error}`);
|
|
150
|
+
}
|
|
151
|
+
return messages;
|
|
152
|
+
}
|
|
153
|
+
async determineCount() {
|
|
154
|
+
const config = this.config;
|
|
155
|
+
try {
|
|
156
|
+
let totalMessages = 0;
|
|
157
|
+
// Count 1:1 direct messages
|
|
158
|
+
try {
|
|
159
|
+
const dmContent = await readFile(`${config.input}/data/direct-messages.js`, 'utf-8');
|
|
160
|
+
const dmData = this.parseTwitterJS(dmContent, 'direct_messages');
|
|
161
|
+
for (const item of dmData) {
|
|
162
|
+
const conversation = item.dmConversation;
|
|
163
|
+
if (conversation && conversation.messages) {
|
|
164
|
+
totalMessages += conversation.messages.filter((msg) => msg.messageCreate).length;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// 1:1 DMs file may not exist
|
|
170
|
+
}
|
|
171
|
+
// Count group direct messages (if enabled)
|
|
172
|
+
if (config['include-group-dms']) {
|
|
173
|
+
try {
|
|
174
|
+
const groupContent = await readFile(`${config.input}/data/direct-messages-group.js`, 'utf-8');
|
|
175
|
+
const groupData = this.parseTwitterJS(groupContent, 'direct_messages_group');
|
|
176
|
+
for (const item of groupData) {
|
|
177
|
+
const conversation = item.dmConversation;
|
|
178
|
+
if (conversation && conversation.messages) {
|
|
179
|
+
totalMessages += conversation.messages.filter((msg) => msg.messageCreate).length;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Group DMs file may not exist
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return totalMessages;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ArchiveExtractor } from '@chronicle.app/etl';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare abstract class TwitterArchiveExtractor<T extends typeof TwitterArchiveExtractor> extends ArchiveExtractor<T> {
|
|
4
|
+
static source: string;
|
|
5
|
+
static strategy: string;
|
|
6
|
+
static schema: z.ZodObject<{
|
|
7
|
+
since: z.ZodOptional<z.ZodDate>;
|
|
8
|
+
until: z.ZodOptional<z.ZodDate>;
|
|
9
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
} & {
|
|
11
|
+
input: z.ZodString;
|
|
12
|
+
'include-group-dms': z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
input: string;
|
|
15
|
+
'include-group-dms': boolean;
|
|
16
|
+
since?: Date | undefined;
|
|
17
|
+
until?: Date | undefined;
|
|
18
|
+
limit?: number | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
input: string;
|
|
21
|
+
'include-group-dms'?: boolean | undefined;
|
|
22
|
+
since?: Date | undefined;
|
|
23
|
+
until?: Date | undefined;
|
|
24
|
+
limit?: number | undefined;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Twitter's export needs the archive's base directory to locate
|
|
28
|
+
* `data/account.js`, so this can't share the base class's no-arg
|
|
29
|
+
* `loadAccountInfo()` signature — kept as its own method instead of an
|
|
30
|
+
* incompatible override.
|
|
31
|
+
*/
|
|
32
|
+
protected loadAccountInfoFrom(basePath: string): Promise<void>;
|
|
33
|
+
protected parseTwitterJS(content: string, dataType: string): any[];
|
|
34
|
+
protected convertTwitterDateToISO(twitterDate: string): string;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=TwitterArchiveExtractor.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ArchiveExtractor, Extractor } from '@chronicle.app/etl';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export class TwitterArchiveExtractor extends ArchiveExtractor {
|
|
5
|
+
static source = 'twitter';
|
|
6
|
+
static strategy = 'archive';
|
|
7
|
+
static schema = Extractor.schema.extend({
|
|
8
|
+
input: z.string().describe('Path to Twitter export data directory'),
|
|
9
|
+
'include-group-dms': z
|
|
10
|
+
.boolean()
|
|
11
|
+
.optional()
|
|
12
|
+
.default(true)
|
|
13
|
+
.describe('Include group direct messages in extraction'),
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Twitter's export needs the archive's base directory to locate
|
|
17
|
+
* `data/account.js`, so this can't share the base class's no-arg
|
|
18
|
+
* `loadAccountInfo()` signature — kept as its own method instead of an
|
|
19
|
+
* incompatible override.
|
|
20
|
+
*/
|
|
21
|
+
async loadAccountInfoFrom(basePath) {
|
|
22
|
+
try {
|
|
23
|
+
const content = await readFile(`${basePath}/data/account.js`, 'utf-8');
|
|
24
|
+
const jsData = this.parseTwitterJS(content, 'account');
|
|
25
|
+
this.accountInfo = jsData[0]?.account || null;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.warn(`Could not load account info: ${error}`);
|
|
29
|
+
this.accountInfo = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
parseTwitterJS(content, dataType) {
|
|
33
|
+
const regex = new RegExp(`window\\.YTD\\.${dataType}\\.part0\\s*=\\s*(.+)`, 's');
|
|
34
|
+
const match = content.match(regex);
|
|
35
|
+
if (!match) {
|
|
36
|
+
throw new Error(`Could not parse ${dataType} data from Twitter export`);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(match[1]);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new Error(`Invalid JSON in ${dataType} file: ${error}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
convertTwitterDateToISO(twitterDate) {
|
|
46
|
+
// Convert Twitter's date format "Sat Jan 11 17:10:28 +0000 2025" to ISO
|
|
47
|
+
// Twitter uses format: "ddd MMM DD HH:mm:ss +0000 YYYY"
|
|
48
|
+
const date = new Date(twitterDate);
|
|
49
|
+
return date.toISOString();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Record } from '@chronicle.app/etl';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import TwitterTransformer from '../TwitterTransformer.js';
|
|
4
|
+
import { TwitterArchiveExtractor } from './TwitterArchiveExtractor.js';
|
|
5
|
+
export declare class TwitterArchiveLikesExtractor extends TwitterArchiveExtractor<typeof TwitterArchiveLikesExtractor> {
|
|
6
|
+
static description: string;
|
|
7
|
+
static recordTypes: string[];
|
|
8
|
+
static defaultTransformer: typeof TwitterTransformer;
|
|
9
|
+
static schema: z.ZodObject<{
|
|
10
|
+
since: z.ZodOptional<z.ZodDate>;
|
|
11
|
+
until: z.ZodOptional<z.ZodDate>;
|
|
12
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
} & {
|
|
14
|
+
input: z.ZodString;
|
|
15
|
+
'include-group-dms': z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
input: string;
|
|
18
|
+
'include-group-dms': boolean;
|
|
19
|
+
since?: Date | undefined;
|
|
20
|
+
until?: Date | undefined;
|
|
21
|
+
limit?: number | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
input: string;
|
|
24
|
+
'include-group-dms'?: boolean | undefined;
|
|
25
|
+
since?: Date | undefined;
|
|
26
|
+
until?: Date | undefined;
|
|
27
|
+
limit?: number | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
/** The liked tweet's id — the like entry's only id, and Twitter's own. */
|
|
30
|
+
keyOf(record: Record): string | null;
|
|
31
|
+
extract(): AsyncGenerator<Record>;
|
|
32
|
+
private processLikes;
|
|
33
|
+
determineCount(): Promise<number | null>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=TwitterArchiveLikesExtractor.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import TwitterTransformer from '../TwitterTransformer.js';
|
|
3
|
+
import { TwitterArchiveExtractor } from './TwitterArchiveExtractor.js';
|
|
4
|
+
export class TwitterArchiveLikesExtractor extends TwitterArchiveExtractor {
|
|
5
|
+
static description = 'Likes';
|
|
6
|
+
static recordTypes = ['likes'];
|
|
7
|
+
static defaultTransformer = TwitterTransformer;
|
|
8
|
+
static schema = TwitterArchiveExtractor.schema;
|
|
9
|
+
/** The liked tweet's id — the like entry's only id, and Twitter's own. */
|
|
10
|
+
keyOf(record) {
|
|
11
|
+
return record.data.tweetId ?? null;
|
|
12
|
+
}
|
|
13
|
+
async *extract() {
|
|
14
|
+
const config = this.config;
|
|
15
|
+
try {
|
|
16
|
+
// Load account info first
|
|
17
|
+
await this.loadAccountInfoFrom(config.input);
|
|
18
|
+
yield* this.processLikes(`${config.input}/data/like.js`);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new Error(`Failed to process Twitter likes: ${error}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async *processLikes(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const content = await readFile(filePath, 'utf-8');
|
|
27
|
+
const jsData = this.parseTwitterJS(content, 'like');
|
|
28
|
+
let count = 0;
|
|
29
|
+
for (const item of jsData) {
|
|
30
|
+
const { like } = item;
|
|
31
|
+
if (like) {
|
|
32
|
+
if (this.shouldStopExtracting(count))
|
|
33
|
+
break;
|
|
34
|
+
yield this.createRecordWithArchiveContext({
|
|
35
|
+
type: 'like',
|
|
36
|
+
tweetId: like.tweetId,
|
|
37
|
+
fullText: like.fullText,
|
|
38
|
+
expandedUrl: like.expandedUrl,
|
|
39
|
+
}, { originalData: like });
|
|
40
|
+
count++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.warn(`Could not process likes: ${error}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async determineCount() {
|
|
49
|
+
const config = this.config;
|
|
50
|
+
try {
|
|
51
|
+
const likesContent = await readFile(`${config.input}/data/like.js`, 'utf-8');
|
|
52
|
+
const likesData = this.parseTwitterJS(likesContent, 'like');
|
|
53
|
+
return likesData.length;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Record } from '@chronicle.app/etl';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import TwitterTransformer from '../TwitterTransformer.js';
|
|
4
|
+
import { TwitterArchiveExtractor } from './TwitterArchiveExtractor.js';
|
|
5
|
+
export declare class TwitterArchiveTweetsExtractor extends TwitterArchiveExtractor<typeof TwitterArchiveTweetsExtractor> {
|
|
6
|
+
static description: string;
|
|
7
|
+
static recordTypes: string[];
|
|
8
|
+
static default: boolean;
|
|
9
|
+
static defaultTransformer: typeof TwitterTransformer;
|
|
10
|
+
static schema: z.ZodObject<{
|
|
11
|
+
since: z.ZodOptional<z.ZodDate>;
|
|
12
|
+
until: z.ZodOptional<z.ZodDate>;
|
|
13
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
} & {
|
|
15
|
+
input: z.ZodString;
|
|
16
|
+
'include-group-dms': z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
input: string;
|
|
19
|
+
'include-group-dms': boolean;
|
|
20
|
+
since?: Date | undefined;
|
|
21
|
+
until?: Date | undefined;
|
|
22
|
+
limit?: number | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
input: string;
|
|
25
|
+
'include-group-dms'?: boolean | undefined;
|
|
26
|
+
since?: Date | undefined;
|
|
27
|
+
until?: Date | undefined;
|
|
28
|
+
limit?: number | undefined;
|
|
29
|
+
}>;
|
|
30
|
+
/** The tweet id (`id_str`) — Twitter's own id, identical through any tool. */
|
|
31
|
+
keyOf(record: Record): string | null;
|
|
32
|
+
extract(): AsyncGenerator<Record>;
|
|
33
|
+
private processTweets;
|
|
34
|
+
determineCount(): Promise<number | null>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=TwitterArchiveTweetsExtractor.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import TwitterTransformer from '../TwitterTransformer.js';
|
|
3
|
+
import { TwitterArchiveExtractor } from './TwitterArchiveExtractor.js';
|
|
4
|
+
export class TwitterArchiveTweetsExtractor extends TwitterArchiveExtractor {
|
|
5
|
+
static description = 'Tweets';
|
|
6
|
+
static recordTypes = ['tweets'];
|
|
7
|
+
static default = true;
|
|
8
|
+
static defaultTransformer = TwitterTransformer;
|
|
9
|
+
static schema = TwitterArchiveExtractor.schema;
|
|
10
|
+
/** The tweet id (`id_str`) — Twitter's own id, identical through any tool. */
|
|
11
|
+
keyOf(record) {
|
|
12
|
+
return record.data.id ?? null;
|
|
13
|
+
}
|
|
14
|
+
async *extract() {
|
|
15
|
+
const config = this.config;
|
|
16
|
+
try {
|
|
17
|
+
// Load account info first
|
|
18
|
+
await this.loadAccountInfoFrom(config.input);
|
|
19
|
+
yield* this.processTweets(`${config.input}/data/tweets.js`);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new Error(`Failed to process Twitter tweets: ${error}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async *processTweets(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
const content = await readFile(filePath, 'utf-8');
|
|
28
|
+
const jsData = this.parseTwitterJS(content, 'tweets');
|
|
29
|
+
let count = 0;
|
|
30
|
+
for (const item of jsData) {
|
|
31
|
+
const { tweet } = item;
|
|
32
|
+
if (tweet && this.isWithinDateRange(new Date(tweet.created_at))) {
|
|
33
|
+
if (this.shouldStopExtracting(count))
|
|
34
|
+
break;
|
|
35
|
+
yield this.createRecordWithArchiveContext({
|
|
36
|
+
type: 'tweet',
|
|
37
|
+
id: tweet.id_str,
|
|
38
|
+
text: tweet.full_text || tweet.text,
|
|
39
|
+
created_at: this.convertTwitterDateToISO(tweet.created_at),
|
|
40
|
+
favorite_count: Number.parseInt(tweet.favorite_count) || 0,
|
|
41
|
+
retweet_count: Number.parseInt(tweet.retweet_count) || 0,
|
|
42
|
+
in_reply_to_status_id: tweet.in_reply_to_status_id_str,
|
|
43
|
+
in_reply_to_user_id: tweet.in_reply_to_user_id_str,
|
|
44
|
+
entities: tweet.entities,
|
|
45
|
+
source: tweet.source,
|
|
46
|
+
retweeted: tweet.retweeted,
|
|
47
|
+
}, { originalData: tweet });
|
|
48
|
+
count++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.warn(`Could not process tweets: ${error}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async determineCount() {
|
|
57
|
+
const config = this.config;
|
|
58
|
+
try {
|
|
59
|
+
const tweetsContent = await readFile(`${config.input}/data/tweets.js`, 'utf-8');
|
|
60
|
+
const tweetsData = this.parseTwitterJS(tweetsContent, 'tweets');
|
|
61
|
+
return tweetsData.length;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { TwitterArchiveExtractor } from './connectors/archive/TwitterArchiveExtractor.js';
|
|
2
|
+
export { TwitterArchiveTweetsExtractor } from './connectors/archive/TwitterArchiveTweetsExtractor.js';
|
|
3
|
+
export { TwitterArchiveLikesExtractor } from './connectors/archive/TwitterArchiveLikesExtractor.js';
|
|
4
|
+
export { TwitterArchiveDirectMessagesExtractor } from './connectors/archive/TwitterArchiveDirectMessagesExtractor.js';
|
|
5
|
+
export { default as TwitterTransformer } from './connectors/TwitterTransformer.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { TwitterArchiveExtractor } from './connectors/archive/TwitterArchiveExtractor.js';
|
|
2
|
+
export { TwitterArchiveTweetsExtractor } from './connectors/archive/TwitterArchiveTweetsExtractor.js';
|
|
3
|
+
export { TwitterArchiveLikesExtractor } from './connectors/archive/TwitterArchiveLikesExtractor.js';
|
|
4
|
+
export { TwitterArchiveDirectMessagesExtractor } from './connectors/archive/TwitterArchiveDirectMessagesExtractor.js';
|
|
5
|
+
export { default as TwitterTransformer } from './connectors/TwitterTransformer.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chronicle.app/twitter",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/**/*.js",
|
|
8
|
+
"dist/**/*.d.ts"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -b tsconfig.lib.json",
|
|
12
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
13
|
+
"lint": "eslint src --ext .ts,.js",
|
|
14
|
+
"test": "node --test src/*.test.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"chronicle",
|
|
18
|
+
"twitter"
|
|
19
|
+
],
|
|
20
|
+
"author": "Andrew Louis",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"description": "Chronicle plugin for twitter",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@chronicle.app/etl": "0.3.0",
|
|
26
|
+
"@chronicle.app/schema": "0.3.0",
|
|
27
|
+
"@chronicle.app/tsconfig": "0.3.0",
|
|
28
|
+
"@types/node": "^22.10.1",
|
|
29
|
+
"typescript": "^5.7.2"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://chronicle.app",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/chronicle-app/chronicle.git",
|
|
35
|
+
"directory": "plugins/twitter"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"zod": "^3.24.1"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@chronicle.app/etl": ">=0.1.0 <1.0.0",
|
|
46
|
+
"@chronicle.app/schema": ">=0.1.0 <1.0.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22.13.0"
|
|
50
|
+
},
|
|
51
|
+
"exports": {
|
|
52
|
+
".": {
|
|
53
|
+
"types": "./dist/index.d.ts",
|
|
54
|
+
"import": "./dist/index.js"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"chronicle": {
|
|
58
|
+
"plugin": true,
|
|
59
|
+
"sources": {
|
|
60
|
+
"twitter": {
|
|
61
|
+
"strategies": {
|
|
62
|
+
"archive": {
|
|
63
|
+
"delivery": "export",
|
|
64
|
+
"recordTypes": [
|
|
65
|
+
"direct-messages",
|
|
66
|
+
"group-direct-messages",
|
|
67
|
+
"likes",
|
|
68
|
+
"tweets"
|
|
69
|
+
],
|
|
70
|
+
"default": true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|