social-light 0.0.1

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.
@@ -0,0 +1,302 @@
1
+ import { SocialPlatform } from './base.mjs';
2
+ import fetch from 'node-fetch';
3
+
4
+ /**
5
+ * Bluesky Platform API Implementation
6
+ * Uses Bluesky's AT Protocol for posting and managing content
7
+ */
8
+ export class BlueskyPlatform extends SocialPlatform {
9
+ /**
10
+ * Constructor for Bluesky platform
11
+ * @param {Object} config - Platform-specific configuration
12
+ * @param {string} config.handle - Bluesky handle (username)
13
+ * @param {string} config.password - Bluesky app password
14
+ * @param {string} config.service - Bluesky service URL (default: https://bsky.social)
15
+ */
16
+ constructor(config = {}) {
17
+ super(config);
18
+ this.name = 'bluesky';
19
+ this.service = config.service || 'https://bsky.social';
20
+ this.authenticated = false;
21
+ this.session = null;
22
+ }
23
+
24
+ /**
25
+ * Check if platform is properly configured
26
+ * @returns {boolean} True if platform is configured
27
+ */
28
+ isConfigured() {
29
+ // Check config for credentials
30
+ const hasConfigCreds = Boolean(
31
+ this.config.handle &&
32
+ this.config.password
33
+ );
34
+
35
+ // Also check environment variables
36
+ const hasEnvCreds = Boolean(
37
+ process.env.BLUESKY_HANDLE &&
38
+ process.env.BLUESKY_APP_PASSWORD
39
+ );
40
+
41
+ return hasConfigCreds || hasEnvCreds;
42
+ }
43
+
44
+ /**
45
+ * Authenticate with the Bluesky API
46
+ * @returns {Promise<boolean>} True if authentication successful
47
+ */
48
+ async authenticate() {
49
+ if (!this.isConfigured()) {
50
+ throw new Error('Bluesky API not properly configured');
51
+ }
52
+
53
+ // Get credentials from config or environment variables
54
+ const handle = this.config.handle || process.env.BLUESKY_HANDLE;
55
+ const password = this.config.password || process.env.BLUESKY_APP_PASSWORD;
56
+ const service = this.config.service || process.env.BLUESKY_SERVICE || this.service;
57
+
58
+ try {
59
+ const response = await fetch(`${service}/xrpc/com.atproto.server.createSession`, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/json'
63
+ },
64
+ body: JSON.stringify({
65
+ identifier: handle,
66
+ password: password
67
+ })
68
+ });
69
+
70
+ if (!response.ok) {
71
+ const error = await response.json();
72
+ throw new Error(`Bluesky authentication failed: ${JSON.stringify(error)}`);
73
+ }
74
+
75
+ this.session = await response.json();
76
+ this.authenticated = true;
77
+ return true;
78
+ } catch (error) {
79
+ console.error('Bluesky authentication error:', error);
80
+ this.authenticated = false;
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Post content to Bluesky
87
+ * @param {Object} post - Post content and metadata
88
+ * @param {string} post.text - Text content of the post (required)
89
+ * @param {Array<string>} post.mediaUrls - URLs of media to attach (optional)
90
+ * @param {Object} post.options - Bluesky-specific options
91
+ * @returns {Promise<Object>} Response including post URI and CID
92
+ */
93
+ async post(post) {
94
+ if (!this.authenticated && !await this.authenticate()) {
95
+ throw new Error('Bluesky authentication required');
96
+ }
97
+
98
+ if (!post.text) {
99
+ throw new Error('Post text is required');
100
+ }
101
+
102
+ try {
103
+ // Create basic post record
104
+ const record = {
105
+ $type: 'app.bsky.feed.post',
106
+ text: post.text,
107
+ createdAt: new Date().toISOString()
108
+ };
109
+
110
+ // Handle languages if specified
111
+ if (post.options && post.options.langs) {
112
+ record.langs = Array.isArray(post.options.langs) ? post.options.langs : [post.options.langs];
113
+ }
114
+
115
+ // Handle media attachments if provided
116
+ if (post.mediaUrls && post.mediaUrls.length > 0) {
117
+ const images = await Promise.all(
118
+ post.mediaUrls.map(url => this._uploadImage(url))
119
+ );
120
+
121
+ if (images.length > 0) {
122
+ record.embed = {
123
+ $type: 'app.bsky.embed.images',
124
+ images: images.map(img => ({
125
+ alt: post.options?.imageAlt || 'Image',
126
+ image: img
127
+ }))
128
+ };
129
+ }
130
+ }
131
+
132
+ // Handle external link embedding if provided
133
+ if (post.options && post.options.externalLink) {
134
+ record.embed = {
135
+ $type: 'app.bsky.embed.external',
136
+ external: {
137
+ uri: post.options.externalLink.uri,
138
+ title: post.options.externalLink.title || '',
139
+ description: post.options.externalLink.description || ''
140
+ }
141
+ };
142
+
143
+ if (post.options.externalLink.thumbnailUrl) {
144
+ const thumb = await this._uploadImage(post.options.externalLink.thumbnailUrl);
145
+ record.embed.external.thumb = thumb;
146
+ }
147
+ }
148
+
149
+ // Create the post
150
+ const response = await fetch(`${this.service}/xrpc/com.atproto.repo.createRecord`, {
151
+ method: 'POST',
152
+ headers: {
153
+ 'Authorization': `Bearer ${this.session.accessJwt}`,
154
+ 'Content-Type': 'application/json'
155
+ },
156
+ body: JSON.stringify({
157
+ repo: this.config.handle,
158
+ collection: 'app.bsky.feed.post',
159
+ record: record
160
+ })
161
+ });
162
+
163
+ if (!response.ok) {
164
+ const error = await response.json();
165
+ throw new Error(`Bluesky post failed: ${JSON.stringify(error)}`);
166
+ }
167
+
168
+ const result = await response.json();
169
+ return {
170
+ id: result.uri.split('/').pop(),
171
+ uri: result.uri,
172
+ cid: result.cid
173
+ };
174
+ } catch (error) {
175
+ console.error('Bluesky post error:', error);
176
+ throw error;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Get status of a post
182
+ * @param {string} postUri - URI of the post to check
183
+ * @returns {Promise<Object>} Post status information
184
+ */
185
+ async getPostStatus(postUri) {
186
+ if (!this.authenticated && !await this.authenticate()) {
187
+ throw new Error('Bluesky authentication required');
188
+ }
189
+
190
+ try {
191
+ // Parse URI to get repo and record ID
192
+ const parts = postUri.split('/');
193
+ const repo = parts[2];
194
+ const recordId = parts.pop();
195
+ const collection = 'app.bsky.feed.post';
196
+
197
+ const response = await fetch(`${this.service}/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${collection}&rkey=${recordId}`, {
198
+ method: 'GET',
199
+ headers: {
200
+ 'Authorization': `Bearer ${this.session.accessJwt}`
201
+ }
202
+ });
203
+
204
+ if (!response.ok) {
205
+ const error = await response.json();
206
+ throw new Error(`Failed to get post status: ${JSON.stringify(error)}`);
207
+ }
208
+
209
+ const result = await response.json();
210
+ return {
211
+ uri: postUri,
212
+ cid: result.cid,
213
+ record: result.value
214
+ };
215
+ } catch (error) {
216
+ console.error('Bluesky get status error:', error);
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Delete a post from Bluesky
223
+ * @param {string} postUri - URI of the post to delete
224
+ * @returns {Promise<boolean>} True if deletion successful
225
+ */
226
+ async deletePost(postUri) {
227
+ if (!this.authenticated && !await this.authenticate()) {
228
+ throw new Error('Bluesky authentication required');
229
+ }
230
+
231
+ try {
232
+ // Parse URI to get repo and record ID
233
+ const parts = postUri.split('/');
234
+ const repo = parts[2];
235
+ const recordId = parts.pop();
236
+ const collection = 'app.bsky.feed.post';
237
+
238
+ const response = await fetch(`${this.service}/xrpc/com.atproto.repo.deleteRecord`, {
239
+ method: 'POST',
240
+ headers: {
241
+ 'Authorization': `Bearer ${this.session.accessJwt}`,
242
+ 'Content-Type': 'application/json'
243
+ },
244
+ body: JSON.stringify({
245
+ repo,
246
+ collection,
247
+ rkey: recordId
248
+ })
249
+ });
250
+
251
+ if (!response.ok) {
252
+ const error = await response.json();
253
+ throw new Error(`Failed to delete post: ${JSON.stringify(error)}`);
254
+ }
255
+
256
+ return true;
257
+ } catch (error) {
258
+ console.error('Bluesky delete error:', error);
259
+ throw error;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Upload an image to Bluesky
265
+ * @param {string} imageUrl - URL of the image to upload
266
+ * @returns {Promise<Object>} Blob reference
267
+ * @private
268
+ */
269
+ async _uploadImage(imageUrl) {
270
+ try {
271
+ // Fetch the image data
272
+ const imageResponse = await fetch(imageUrl);
273
+ if (!imageResponse.ok) {
274
+ throw new Error(`Failed to fetch image: ${imageResponse.statusText}`);
275
+ }
276
+
277
+ const imageBuffer = await imageResponse.buffer();
278
+ const contentType = imageResponse.headers.get('content-type') || 'image/jpeg';
279
+
280
+ // Upload the image blob
281
+ const uploadResponse = await fetch(`${this.service}/xrpc/com.atproto.repo.uploadBlob`, {
282
+ method: 'POST',
283
+ headers: {
284
+ 'Authorization': `Bearer ${this.session.accessJwt}`,
285
+ 'Content-Type': contentType
286
+ },
287
+ body: imageBuffer
288
+ });
289
+
290
+ if (!uploadResponse.ok) {
291
+ const error = await uploadResponse.json();
292
+ throw new Error(`Failed to upload image: ${JSON.stringify(error)}`);
293
+ }
294
+
295
+ const result = await uploadResponse.json();
296
+ return result.blob;
297
+ } catch (error) {
298
+ console.error('Bluesky image upload error:', error);
299
+ throw error;
300
+ }
301
+ }
302
+ }
@@ -0,0 +1,300 @@
1
+ import { PlatformFactory } from "./base.mjs";
2
+ import { getConfig, getCredentials } from "../config.mjs";
3
+ import { logAction } from "../db.mjs";
4
+ import dotenv from 'dotenv';
5
+
6
+ // Load environment variables
7
+ dotenv.config();
8
+
9
+ /**
10
+ * Social API manager for handling multiple platforms
11
+ */
12
+ export class SocialAPI {
13
+ /**
14
+ * Constructor for the Social API manager
15
+ * @param {Object} options - Configuration options
16
+ */
17
+ constructor(options = {}) {
18
+ this.platforms = new Map();
19
+ this.config = getConfig();
20
+ this.options = options;
21
+ }
22
+
23
+ /**
24
+ * Initialize a specific platform
25
+ * @param {string} platform - Platform name
26
+ * @param {Object} config - Platform-specific configuration
27
+ * @returns {Promise<boolean>} True if initialization successful
28
+ */
29
+ async initPlatform(platform, config = {}) {
30
+ try {
31
+ // Get credentials from config.json
32
+ const platformCredentials = getCredentials(platform);
33
+
34
+ // Merge provided config and stored credentials
35
+ const mergedConfig = {
36
+ ...platformCredentials,
37
+ ...config
38
+ };
39
+
40
+ // Get platform instance from factory
41
+ const platformInstance = await PlatformFactory.create(platform, mergedConfig);
42
+
43
+ // Store the platform instance
44
+ this.platforms.set(platform.toLowerCase(), platformInstance);
45
+
46
+ // Attempt authentication if configured
47
+ if (platformInstance.isConfigured()) {
48
+ await platformInstance.authenticate();
49
+ }
50
+
51
+ return true;
52
+ } catch (error) {
53
+ console.error(`Failed to initialize ${platform}:`, error);
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get a platform instance by name
60
+ * @param {string} platform - Platform name
61
+ * @returns {SocialPlatform|null} Platform instance or null if not found
62
+ */
63
+ getPlatform(platform) {
64
+ return this.platforms.get(platform.toLowerCase()) || null;
65
+ }
66
+
67
+ /**
68
+ * Check if a platform is initialized and authenticated
69
+ * @param {string} platform - Platform name
70
+ * @returns {boolean} True if platform is ready
71
+ */
72
+ isPlatformReady(platform) {
73
+ const platformInstance = this.getPlatform(platform);
74
+ return platformInstance && platformInstance.authenticated;
75
+ }
76
+
77
+ /**
78
+ * Post content to multiple platforms
79
+ * @param {Object} post - Post content and metadata
80
+ * @param {string} post.text - Text content of the post
81
+ * @param {string} post.title - Title/caption of the post (optional)
82
+ * @param {Array<string>} post.mediaUrls - Media URLs to attach (optional)
83
+ * @param {Array<string>} post.platforms - Platforms to post to
84
+ * @param {Object} post.options - Platform-specific options
85
+ * @returns {Promise<Object>} Results for each platform
86
+ */
87
+ async post(post) {
88
+ if (!post.platforms || post.platforms.length === 0) {
89
+ throw new Error("No platforms specified for posting");
90
+ }
91
+
92
+ const results = {};
93
+ const errors = [];
94
+
95
+ // Post to each platform
96
+ for (const platform of post.platforms) {
97
+ try {
98
+ const platformLower = platform.toLowerCase();
99
+ let platformInstance = this.getPlatform(platformLower);
100
+
101
+ if (!platformInstance) {
102
+ await this.initPlatform(platformLower);
103
+ platformInstance = this.getPlatform(platformLower);
104
+ }
105
+
106
+ if (!platformInstance) {
107
+ throw new Error(`Platform ${platform} not initialized`);
108
+ }
109
+
110
+ if (!platformInstance.authenticated) {
111
+ await platformInstance.authenticate();
112
+ }
113
+
114
+ // Get platform-specific options if provided
115
+ const platformOptions =
116
+ post.options && post.options[platformLower]
117
+ ? post.options[platformLower]
118
+ : {};
119
+
120
+ // Create platform-specific post object
121
+ const platformPost = {
122
+ text: post.text,
123
+ title: post.title,
124
+ mediaUrls: post.mediaUrls,
125
+ options: platformOptions,
126
+ };
127
+
128
+ // Post to the platform
129
+ const result = await platformInstance.post(platformPost);
130
+
131
+ // Store result
132
+ results[platformLower] = {
133
+ success: true,
134
+ postId: result.id || result.uri || result.publishId,
135
+ ...result,
136
+ };
137
+
138
+ // Log the action
139
+ logAction("post_created", {
140
+ platform: platformLower,
141
+ postId: result.id || result.uri || result.publishId,
142
+ content: post.text?.substring(0, 100),
143
+ });
144
+ } catch (error) {
145
+ console.error(`Error posting to ${platform}:`, error);
146
+
147
+ // Store error
148
+ results[platform.toLowerCase()] = {
149
+ success: false,
150
+ error: error.message,
151
+ };
152
+
153
+ errors.push({
154
+ platform,
155
+ message: error.message,
156
+ });
157
+
158
+ // Log the error
159
+ logAction("post_error", {
160
+ platform: platform.toLowerCase(),
161
+ error: error.message,
162
+ content: post.text?.substring(0, 100),
163
+ });
164
+ }
165
+ }
166
+
167
+ return {
168
+ results,
169
+ success: errors.length === 0,
170
+ errors,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Get status of posts across platforms
176
+ * @param {Object} postIds - Map of platform to post ID
177
+ * @returns {Promise<Object>} Status for each platform
178
+ */
179
+ async getPostStatus(postIds) {
180
+ const results = {};
181
+ const errors = [];
182
+
183
+ for (const [platform, postId] of Object.entries(postIds)) {
184
+ try {
185
+ const platformLower = platform.toLowerCase();
186
+ const platformInstance = this.getPlatform(platformLower);
187
+
188
+ if (!platformInstance) {
189
+ throw new Error(`Platform ${platform} not initialized`);
190
+ }
191
+
192
+ if (!platformInstance.authenticated) {
193
+ await platformInstance.authenticate();
194
+ }
195
+
196
+ const status = await platformInstance.getPostStatus(postId);
197
+
198
+ results[platformLower] = {
199
+ success: true,
200
+ status,
201
+ };
202
+ } catch (error) {
203
+ console.error(`Error getting status from ${platform}:`, error);
204
+
205
+ results[platform.toLowerCase()] = {
206
+ success: false,
207
+ error: error.message,
208
+ };
209
+
210
+ errors.push({
211
+ platform,
212
+ message: error.message,
213
+ });
214
+ }
215
+ }
216
+
217
+ return {
218
+ results,
219
+ success: errors.length === 0,
220
+ errors,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Delete posts across platforms
226
+ * @param {Object} postIds - Map of platform to post ID
227
+ * @returns {Promise<Object>} Results for each platform
228
+ */
229
+ async deletePosts(postIds) {
230
+ const results = {};
231
+ const errors = [];
232
+
233
+ for (const [platform, postId] of Object.entries(postIds)) {
234
+ try {
235
+ const platformLower = platform.toLowerCase();
236
+ const platformInstance = this.getPlatform(platformLower);
237
+
238
+ if (!platformInstance) {
239
+ throw new Error(`Platform ${platform} not initialized`);
240
+ }
241
+
242
+ if (!platformInstance.authenticated) {
243
+ await platformInstance.authenticate();
244
+ }
245
+
246
+ const success = await platformInstance.deletePost(postId);
247
+
248
+ results[platformLower] = {
249
+ success,
250
+ };
251
+
252
+ // Log the action
253
+ logAction("post_deleted", {
254
+ platform: platformLower,
255
+ postId,
256
+ });
257
+ } catch (error) {
258
+ console.error(`Error deleting post from ${platform}:`, error);
259
+
260
+ results[platform.toLowerCase()] = {
261
+ success: false,
262
+ error: error.message,
263
+ };
264
+
265
+ errors.push({
266
+ platform,
267
+ message: error.message,
268
+ });
269
+
270
+ // Log the error
271
+ logAction("delete_error", {
272
+ platform: platform.toLowerCase(),
273
+ postId,
274
+ error: error.message,
275
+ });
276
+ }
277
+ }
278
+
279
+ return {
280
+ results,
281
+ success: errors.length === 0,
282
+ errors,
283
+ };
284
+ }
285
+ }
286
+
287
+ // Export a singleton instance
288
+ let instance = null;
289
+
290
+ /**
291
+ * Get the Social API instance
292
+ * @param {Object} options - Configuration options
293
+ * @returns {SocialAPI} SocialAPI instance
294
+ */
295
+ export const getSocialAPI = (options = {}) => {
296
+ if (!instance) {
297
+ instance = new SocialAPI(options);
298
+ }
299
+ return instance;
300
+ };