@splendidlabz/third-party 1.3.9 → 1.3.10
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/CHANGELOG.md +7 -0
- package/lib/logger.js +4 -1
- package/lib/social/README.md +107 -0
- package/lib/social/content-formatter.js +161 -0
- package/lib/social/example.js +49 -0
- package/lib/social/index.js +73 -0
- package/lib/social/instagram.js +105 -0
- package/lib/social/threads.js +110 -0
- package/lib/social/twitter.js +90 -0
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
package/lib/logger.js
CHANGED
|
@@ -3,7 +3,10 @@ import pino from 'pino'
|
|
|
3
3
|
// Potentially can put into logtail for production
|
|
4
4
|
// Potentially can use pino-http for logging requests
|
|
5
5
|
|
|
6
|
-
const isDev =
|
|
6
|
+
const isDev =
|
|
7
|
+
process.env.NODE_ENV === 'development' ||
|
|
8
|
+
import.meta.env?.DEV === true ||
|
|
9
|
+
import.meta.env?.MODE === 'development'
|
|
7
10
|
|
|
8
11
|
export function createLogger({ verbose = false, level = 'info' } = {}) {
|
|
9
12
|
return pino({
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Social Media Content Automation
|
|
2
|
+
|
|
3
|
+
Automate posting content across Twitter, Threads, and Instagram with platform-specific formatting.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Content formatting**: Automatically formats content for each platform's requirements
|
|
8
|
+
- **Thread support**: Splits long content into Twitter/Threads threads
|
|
9
|
+
- **Cross-platform**: Post to multiple platforms from one content object
|
|
10
|
+
- **Dry run mode**: Preview content before posting
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Basic Example
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { formatContent, postToPlatforms, createTwitter, createInstagram } from '@splendidlabz/third-party/social'
|
|
18
|
+
|
|
19
|
+
// Define your content once
|
|
20
|
+
const content = {
|
|
21
|
+
title: 'CSS trick that saved me 2 hours',
|
|
22
|
+
points: [
|
|
23
|
+
'Use `:has()` selector for conditional styling',
|
|
24
|
+
'No JavaScript needed',
|
|
25
|
+
'Works in all modern browsers',
|
|
26
|
+
],
|
|
27
|
+
hashtags: ['webdev', 'css', 'frontend'],
|
|
28
|
+
link: 'https://example.com/tutorial',
|
|
29
|
+
videoUrl: 'https://example.com/video.mp4', // For Instagram
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Format for all platforms
|
|
33
|
+
const formatted = formatContent(content)
|
|
34
|
+
console.log(formatted.twitter) // Thread format
|
|
35
|
+
console.log(formatted.instagram) // Caption + first comment
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Posting to Platforms
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// Set up clients
|
|
42
|
+
const twitter = createTwitter({
|
|
43
|
+
bearerToken: process.env.TWITTER_BEARER_TOKEN,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const instagram = createInstagram({
|
|
47
|
+
accessToken: process.env.INSTAGRAM_ACCESS_TOKEN,
|
|
48
|
+
instagramAccountId: process.env.INSTAGRAM_ACCOUNT_ID,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Post to all platforms
|
|
52
|
+
const results = await postToPlatforms(
|
|
53
|
+
content,
|
|
54
|
+
{ twitter, instagram },
|
|
55
|
+
{ dryRun: true } // Preview first
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Platform-Specific Formatting
|
|
60
|
+
|
|
61
|
+
**Twitter/Threads:**
|
|
62
|
+
- Auto-splits into threads if content is too long
|
|
63
|
+
- Adds hashtags at the end
|
|
64
|
+
- Includes link if provided
|
|
65
|
+
|
|
66
|
+
**Instagram:**
|
|
67
|
+
- Formats as caption with numbered points
|
|
68
|
+
- Adds hashtags (10-20 recommended)
|
|
69
|
+
- Generates first comment for engagement
|
|
70
|
+
- Requires video URL for Reels
|
|
71
|
+
|
|
72
|
+
## API Setup
|
|
73
|
+
|
|
74
|
+
### Twitter/X API
|
|
75
|
+
|
|
76
|
+
1. Create a Twitter Developer account
|
|
77
|
+
2. Create an app and get Bearer Token
|
|
78
|
+
3. For OAuth 1.0a, get API Key, Secret, Access Token, and Access Token Secret
|
|
79
|
+
|
|
80
|
+
### Instagram Graph API
|
|
81
|
+
|
|
82
|
+
1. Create a Facebook App
|
|
83
|
+
2. Get Instagram Business Account ID
|
|
84
|
+
3. Generate Access Token with `instagram_basic`, `instagram_content_publish`, `pages_read_engagement` permissions
|
|
85
|
+
4. Note: Video URLs must be publicly accessible
|
|
86
|
+
|
|
87
|
+
## Content Object Structure
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
{
|
|
91
|
+
title: string, // Main hook/title (required)
|
|
92
|
+
points: string[], // Key points (optional)
|
|
93
|
+
hashtags: string[], // Hashtags without # (optional)
|
|
94
|
+
link: string, // Link to full content (optional)
|
|
95
|
+
videoUrl: string, // Video URL for Instagram (optional)
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Notes
|
|
100
|
+
|
|
101
|
+
- Twitter API v2 requires authentication
|
|
102
|
+
- Instagram Reels require video to be publicly accessible
|
|
103
|
+
- Threads can use the same client as Twitter (Meta API)
|
|
104
|
+
- Use `dryRun: true` to preview before posting
|
|
105
|
+
- Rate limits apply - the system includes delays for threads
|
|
106
|
+
|
|
107
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats content for different social media platforms
|
|
3
|
+
* @param {Object} content - Base content object
|
|
4
|
+
* @param {string} content.title - Main title/hook
|
|
5
|
+
* @param {string[]} content.points - Array of key points
|
|
6
|
+
* @param {string} [content.videoUrl] - Video URL for Instagram
|
|
7
|
+
* @param {string[]} [content.hashtags] - Hashtags to include
|
|
8
|
+
* @param {string} [content.link] - Link to full content
|
|
9
|
+
* @param {Object} [options] - Formatting options
|
|
10
|
+
* @return {Object} Platform-specific formatted content
|
|
11
|
+
*/
|
|
12
|
+
export function formatContent(content, options = {}) {
|
|
13
|
+
const { title, points = [], videoUrl, hashtags = [], link } = content
|
|
14
|
+
const { maxThreadLength = 280, maxInstagramLength = 2200 } = options
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
twitter: formatTwitter({
|
|
18
|
+
title,
|
|
19
|
+
points,
|
|
20
|
+
hashtags,
|
|
21
|
+
link,
|
|
22
|
+
maxLength: maxThreadLength,
|
|
23
|
+
}),
|
|
24
|
+
threads: formatThreads({
|
|
25
|
+
title,
|
|
26
|
+
points,
|
|
27
|
+
hashtags,
|
|
28
|
+
link,
|
|
29
|
+
maxLength: maxThreadLength,
|
|
30
|
+
}),
|
|
31
|
+
instagram: formatInstagram({
|
|
32
|
+
title,
|
|
33
|
+
points,
|
|
34
|
+
videoUrl,
|
|
35
|
+
hashtags,
|
|
36
|
+
link,
|
|
37
|
+
maxLength: maxInstagramLength,
|
|
38
|
+
}),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Formats content for Twitter/X
|
|
44
|
+
* @param {Object} params
|
|
45
|
+
* @return {Object} Formatted Twitter content
|
|
46
|
+
*/
|
|
47
|
+
function formatTwitter({ title, points, hashtags, link, maxLength }) {
|
|
48
|
+
const hashtagStr =
|
|
49
|
+
hashtags.length > 0 ? '\n\n' + hashtags.map(h => `#${h}`).join(' ') : ''
|
|
50
|
+
const linkStr = link ? `\n\n${link}` : ''
|
|
51
|
+
|
|
52
|
+
// Single tweet if short enough
|
|
53
|
+
const singleTweet = title + hashtagStr + linkStr
|
|
54
|
+
if (singleTweet.length <= maxLength) {
|
|
55
|
+
return {
|
|
56
|
+
type: 'single',
|
|
57
|
+
content: [singleTweet],
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Thread format
|
|
62
|
+
const thread = [title]
|
|
63
|
+
|
|
64
|
+
points.forEach((point, i) => {
|
|
65
|
+
const num = `${i + 1}/${points.length}`
|
|
66
|
+
const tweet = `${num} ${point}`
|
|
67
|
+
thread.push(tweet)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (link) thread.push(`🔗 Full post: ${link}`)
|
|
71
|
+
if (hashtags.length > 0) {
|
|
72
|
+
const hashtagTweet = hashtags.map(h => `#${h}`).join(' ')
|
|
73
|
+
thread.push(hashtagTweet)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
type: 'thread',
|
|
78
|
+
content: thread,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Formats content for Threads (similar to Twitter but slightly different)
|
|
84
|
+
* @param {Object} params
|
|
85
|
+
* @return {Object} Formatted Threads content
|
|
86
|
+
*/
|
|
87
|
+
function formatThreads({ title, points, hashtags, link, maxLength }) {
|
|
88
|
+
// Threads allows longer posts, but similar format
|
|
89
|
+
return formatTwitter({
|
|
90
|
+
title,
|
|
91
|
+
points,
|
|
92
|
+
hashtags,
|
|
93
|
+
link,
|
|
94
|
+
maxLength: maxLength * 2,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Formats content for Instagram
|
|
100
|
+
* @param {Object} params
|
|
101
|
+
* @return {Object} Formatted Instagram content
|
|
102
|
+
*/
|
|
103
|
+
function formatInstagram({
|
|
104
|
+
title,
|
|
105
|
+
points,
|
|
106
|
+
videoUrl,
|
|
107
|
+
hashtags,
|
|
108
|
+
link,
|
|
109
|
+
maxLength,
|
|
110
|
+
}) {
|
|
111
|
+
let caption = title + '\n\n'
|
|
112
|
+
|
|
113
|
+
if (points.length > 0) {
|
|
114
|
+
caption += points.map((p, i) => `${i + 1}. ${p}`).join('\n\n') + '\n\n'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (link) caption += `🔗 Link in bio: ${link}\n\n`
|
|
118
|
+
|
|
119
|
+
// Instagram hashtags (10-20 recommended)
|
|
120
|
+
const instagramHashtags = hashtags.slice(0, 20)
|
|
121
|
+
if (instagramHashtags.length > 0) {
|
|
122
|
+
caption += instagramHashtags.map(h => `#${h}`).join(' ')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Instagram comment (first comment strategy)
|
|
126
|
+
const firstComment = link
|
|
127
|
+
? `Full tutorial: ${link}\n\nWhat would you like to see next? 👇`
|
|
128
|
+
: `What would you like to see next? 👇`
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
caption: caption.trim(),
|
|
132
|
+
firstComment,
|
|
133
|
+
videoUrl,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Splits long text into thread chunks
|
|
139
|
+
* @param {string} text - Text to split
|
|
140
|
+
* @param {number} maxLength - Maximum length per chunk
|
|
141
|
+
* @return {string[]} Array of text chunks
|
|
142
|
+
*/
|
|
143
|
+
export function splitIntoThread(text, maxLength = 280) {
|
|
144
|
+
const sentences = text.split(/[.!?]\s+/)
|
|
145
|
+
const chunks = []
|
|
146
|
+
let currentChunk = ''
|
|
147
|
+
|
|
148
|
+
sentences.forEach(sentence => {
|
|
149
|
+
const testChunk = currentChunk ? `${currentChunk}. ${sentence}` : sentence
|
|
150
|
+
|
|
151
|
+
if (testChunk.length <= maxLength) {
|
|
152
|
+
currentChunk = testChunk
|
|
153
|
+
} else {
|
|
154
|
+
if (currentChunk) chunks.push(currentChunk)
|
|
155
|
+
currentChunk = sentence
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (currentChunk) chunks.push(currentChunk)
|
|
160
|
+
return chunks
|
|
161
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of social media automation
|
|
3
|
+
*
|
|
4
|
+
* This shows how to format and post content across platforms
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatContent, postToPlatforms, createTwitter, createInstagram } from './index.js'
|
|
8
|
+
|
|
9
|
+
// Example: Create your content once
|
|
10
|
+
const content = {
|
|
11
|
+
title: 'CSS trick that saved me 2 hours yesterday',
|
|
12
|
+
points: [
|
|
13
|
+
'Use `:has()` selector to style parent based on child',
|
|
14
|
+
'No JavaScript needed for conditional styling',
|
|
15
|
+
'Works in all modern browsers now',
|
|
16
|
+
],
|
|
17
|
+
hashtags: ['webdev', 'css', 'frontend', 'coding'],
|
|
18
|
+
link: 'https://example.com/full-tutorial',
|
|
19
|
+
videoUrl: 'https://example.com/video.mp4', // For Instagram
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Format for all platforms
|
|
23
|
+
const formatted = formatContent(content)
|
|
24
|
+
console.log('Twitter:', formatted.twitter)
|
|
25
|
+
console.log('Instagram:', formatted.instagram)
|
|
26
|
+
|
|
27
|
+
// Example: Set up clients
|
|
28
|
+
const twitter = createTwitter({
|
|
29
|
+
bearerToken: process.env.TWITTER_BEARER_TOKEN,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const instagram = createInstagram({
|
|
33
|
+
accessToken: process.env.INSTAGRAM_ACCESS_TOKEN,
|
|
34
|
+
instagramAccountId: process.env.INSTAGRAM_ACCOUNT_ID,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Post to all platforms (dry run first)
|
|
38
|
+
const results = await postToPlatforms(
|
|
39
|
+
content,
|
|
40
|
+
{ twitter, instagram },
|
|
41
|
+
{ dryRun: true }
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
console.log('Preview results:', results)
|
|
45
|
+
|
|
46
|
+
// When ready, post for real
|
|
47
|
+
// const realResults = await postToPlatforms(content, { twitter, instagram })
|
|
48
|
+
|
|
49
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social media content automation
|
|
3
|
+
* Formats and posts content across multiple platforms
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { formatContent, splitIntoThread } from './content-formatter.js'
|
|
7
|
+
export { createTwitter } from './twitter.js'
|
|
8
|
+
export { createThreads } from './threads.js'
|
|
9
|
+
export { createInstagram } from './instagram.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Orchestrates posting to multiple platforms
|
|
13
|
+
* @param {Object} content - Base content (see content-formatter.js)
|
|
14
|
+
* @param {Object} clients - Platform clients
|
|
15
|
+
* @param {Object} [clients.twitter] - Twitter client instance
|
|
16
|
+
* @param {Object} [clients.threads] - Threads client (can use Twitter client)
|
|
17
|
+
* @param {Object} [clients.instagram] - Instagram client instance
|
|
18
|
+
* @param {Object} [options] - Posting options
|
|
19
|
+
* @param {boolean} [options.dryRun] - Preview without posting
|
|
20
|
+
* @return {Promise<Object>} Results from each platform
|
|
21
|
+
*/
|
|
22
|
+
export async function postToPlatforms(content, clients, options = {}) {
|
|
23
|
+
const { dryRun = false } = options
|
|
24
|
+
const formatted = formatContent(content)
|
|
25
|
+
const results = {}
|
|
26
|
+
|
|
27
|
+
// Post to Twitter
|
|
28
|
+
if (clients.twitter && formatted.twitter) {
|
|
29
|
+
if (dryRun) {
|
|
30
|
+
results.twitter = { preview: formatted.twitter }
|
|
31
|
+
} else {
|
|
32
|
+
// Twitter client handles both single tweets and arrays
|
|
33
|
+
results.twitter = await clients.twitter.post(formatted.twitter.content)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Post to Threads
|
|
38
|
+
if (clients.threads && formatted.threads) {
|
|
39
|
+
if (dryRun) {
|
|
40
|
+
results.threads = { preview: formatted.threads }
|
|
41
|
+
} else {
|
|
42
|
+
const threadsClient = clients.threads
|
|
43
|
+
// Threads content is same format as Twitter
|
|
44
|
+
// For single post, join content; for thread, use array
|
|
45
|
+
const threadsContent = formatted.threads.type === 'single'
|
|
46
|
+
? formatted.threads.content[0]
|
|
47
|
+
: formatted.threads.content
|
|
48
|
+
results.threads = await threadsClient.post(threadsContent)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Post to Instagram
|
|
53
|
+
if (clients.instagram && formatted.instagram) {
|
|
54
|
+
if (dryRun) {
|
|
55
|
+
results.instagram = { preview: formatted.instagram }
|
|
56
|
+
} else {
|
|
57
|
+
const { caption, videoUrl, firstComment } = formatted.instagram
|
|
58
|
+
|
|
59
|
+
// Post the reel
|
|
60
|
+
const reel = await clients.instagram.postReel(videoUrl, caption)
|
|
61
|
+
|
|
62
|
+
// Add first comment if provided
|
|
63
|
+
if (firstComment && reel.id) {
|
|
64
|
+
await clients.instagram.addComment(reel.id, firstComment)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
results.instagram = reel
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return results
|
|
72
|
+
}
|
|
73
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createZlFetch } from 'zl-fetch'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates an Instagram Graph API client
|
|
5
|
+
* Note: Instagram API requires Facebook App setup and access tokens
|
|
6
|
+
* @param {Object} config - Configuration
|
|
7
|
+
* @param {string} config.accessToken - Instagram Graph API Access Token
|
|
8
|
+
* @param {string} config.instagramAccountId - Instagram Business Account ID
|
|
9
|
+
* @return {Object} Instagram client
|
|
10
|
+
*/
|
|
11
|
+
export function createInstagram({ accessToken, instagramAccountId }) {
|
|
12
|
+
const BASE_URL = 'https://graph.facebook.com/v19.0'
|
|
13
|
+
|
|
14
|
+
const instagram = createZlFetch(BASE_URL, {
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
returnError: true,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
/**
|
|
23
|
+
* Create a media container (step 1 of posting)
|
|
24
|
+
* @param {string} videoUrl - URL to video file
|
|
25
|
+
* @param {string} caption - Post caption
|
|
26
|
+
* @return {Promise<Object>} Container creation response
|
|
27
|
+
*/
|
|
28
|
+
async createVideoContainer(videoUrl, caption) {
|
|
29
|
+
const { response, error } = await instagram.post(
|
|
30
|
+
`/${instagramAccountId}/media`,
|
|
31
|
+
{
|
|
32
|
+
query: {
|
|
33
|
+
access_token: accessToken,
|
|
34
|
+
media_type: 'REELS',
|
|
35
|
+
video_url: videoUrl,
|
|
36
|
+
caption,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if (response) return response.body
|
|
42
|
+
if (error) throw error
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Publish a media container (step 2 of posting)
|
|
47
|
+
* @param {string} creationId - Container creation ID from step 1
|
|
48
|
+
* @return {Promise<Object>} Publishing response
|
|
49
|
+
*/
|
|
50
|
+
async publishContainer(creationId) {
|
|
51
|
+
const { response, error } = await instagram.post(
|
|
52
|
+
`/${instagramAccountId}/media_publish`,
|
|
53
|
+
{
|
|
54
|
+
query: {
|
|
55
|
+
access_token: accessToken,
|
|
56
|
+
creation_id: creationId,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (response) return response.body
|
|
62
|
+
if (error) throw error
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Post a Reel (two-step process)
|
|
67
|
+
* @param {string} videoUrl - URL to video file (must be publicly accessible)
|
|
68
|
+
* @param {string} caption - Post caption
|
|
69
|
+
* @return {Promise<Object>} Post response
|
|
70
|
+
*/
|
|
71
|
+
async postReel(videoUrl, caption) {
|
|
72
|
+
// Step 1: Create container
|
|
73
|
+
const container = await this.createVideoContainer(videoUrl, caption)
|
|
74
|
+
|
|
75
|
+
// Step 2: Publish (may need to wait for processing)
|
|
76
|
+
// In production, you'd want to poll status or use webhooks
|
|
77
|
+
const published = await this.publishContainer(container.id)
|
|
78
|
+
|
|
79
|
+
return published
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add a comment to a post
|
|
84
|
+
* @param {string} mediaId - Instagram media ID
|
|
85
|
+
* @param {string} text - Comment text
|
|
86
|
+
* @return {Promise<Object>} Comment response
|
|
87
|
+
*/
|
|
88
|
+
async addComment(mediaId, text) {
|
|
89
|
+
const { response, error } = await instagram.post(
|
|
90
|
+
`/${mediaId}/comments`,
|
|
91
|
+
{
|
|
92
|
+
query: {
|
|
93
|
+
access_token: accessToken,
|
|
94
|
+
message: text,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if (response) return response.body
|
|
100
|
+
if (error) throw error
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createZlFetch } from 'zl-fetch'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Threads API client
|
|
5
|
+
* Note: Threads API is part of Meta's Graph API
|
|
6
|
+
* @param {Object} config - Configuration
|
|
7
|
+
* @param {string} config.accessToken - Meta Access Token
|
|
8
|
+
* @param {string} config.threadsUserId - Threads User ID (your account ID)
|
|
9
|
+
* @return {Object} Threads client
|
|
10
|
+
*/
|
|
11
|
+
export function createThreads({ accessToken, threadsUserId }) {
|
|
12
|
+
const BASE_URL = 'https://graph.threads.net/v1.0'
|
|
13
|
+
|
|
14
|
+
const threads = createZlFetch(BASE_URL, {
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
returnError: true,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
/**
|
|
23
|
+
* Create a media container (step 1 of posting)
|
|
24
|
+
* @param {string} text - Thread text
|
|
25
|
+
* @param {string} [mediaType] - 'TEXT' or 'IMAGE' (default: 'TEXT')
|
|
26
|
+
* @return {Promise<Object>} Container creation response
|
|
27
|
+
*/
|
|
28
|
+
async createContainer(text, mediaType = 'TEXT') {
|
|
29
|
+
const { response, error } = await threads.post(
|
|
30
|
+
`/${threadsUserId}/threads`,
|
|
31
|
+
{
|
|
32
|
+
query: {
|
|
33
|
+
access_token: accessToken,
|
|
34
|
+
media_type: mediaType,
|
|
35
|
+
text,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if (response) return response.body
|
|
41
|
+
if (error) throw error
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Publish a media container (step 2 of posting)
|
|
46
|
+
* @param {string} creationId - Container creation ID from step 1
|
|
47
|
+
* @return {Promise<Object>} Publishing response
|
|
48
|
+
*/
|
|
49
|
+
async publishContainer(creationId) {
|
|
50
|
+
const { response, error } = await threads.post(
|
|
51
|
+
`/${threadsUserId}/threads_publish`,
|
|
52
|
+
{
|
|
53
|
+
query: {
|
|
54
|
+
access_token: accessToken,
|
|
55
|
+
creation_id: creationId,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (response) return response.body
|
|
61
|
+
if (error) throw error
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Post a thread (two-step process)
|
|
66
|
+
* @param {string} text - Thread text
|
|
67
|
+
* @return {Promise<Object>} Post response
|
|
68
|
+
*/
|
|
69
|
+
async postThread(text) {
|
|
70
|
+
// Step 1: Create container
|
|
71
|
+
const container = await this.createContainer(text)
|
|
72
|
+
|
|
73
|
+
// Step 2: Publish
|
|
74
|
+
const published = await this.publishContainer(container.id)
|
|
75
|
+
|
|
76
|
+
return published
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Post content (can be string or array for thread replies)
|
|
81
|
+
* @param {string|string[]} content - Single post or array for replies
|
|
82
|
+
* @return {Promise<Object|Object[]>} Post response(s)
|
|
83
|
+
*/
|
|
84
|
+
async post(content) {
|
|
85
|
+
if (Array.isArray(content)) {
|
|
86
|
+
// For threads, you'd need to reply to previous post
|
|
87
|
+
// This is a simplified version - full implementation would track reply IDs
|
|
88
|
+
const results = []
|
|
89
|
+
let parentId = null
|
|
90
|
+
|
|
91
|
+
for (const text of content) {
|
|
92
|
+
// Note: Threads API reply structure may differ
|
|
93
|
+
// This is a placeholder for the actual implementation
|
|
94
|
+
const result = await this.postThread(text)
|
|
95
|
+
results.push(result)
|
|
96
|
+
// Add delay between posts
|
|
97
|
+
if (content.length > 1) {
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return results
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return this.postThread(content)
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createZlFetch } from 'zl-fetch'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Twitter/X API client
|
|
5
|
+
* @param {Object} config - Configuration
|
|
6
|
+
* @param {string} config.bearerToken - Twitter Bearer Token
|
|
7
|
+
* @param {string} [config.apiKey] - API Key (for OAuth 1.0a)
|
|
8
|
+
* @param {string} [config.apiSecret] - API Secret (for OAuth 1.0a)
|
|
9
|
+
* @param {string} [config.accessToken] - Access Token (for OAuth 1.0a)
|
|
10
|
+
* @param {string} [config.accessTokenSecret] - Access Token Secret (for OAuth 1.0a)
|
|
11
|
+
* @return {Object} Twitter client
|
|
12
|
+
*/
|
|
13
|
+
export function createTwitter({
|
|
14
|
+
bearerToken,
|
|
15
|
+
apiKey,
|
|
16
|
+
apiSecret,
|
|
17
|
+
accessToken,
|
|
18
|
+
accessTokenSecret,
|
|
19
|
+
}) {
|
|
20
|
+
const BASE_URL = 'https://api.twitter.com/2'
|
|
21
|
+
|
|
22
|
+
// Use OAuth 1.0a if credentials provided, otherwise Bearer token
|
|
23
|
+
const useOAuth = apiKey && apiSecret && accessToken && accessTokenSecret
|
|
24
|
+
|
|
25
|
+
const twitter = createZlFetch(BASE_URL, {
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
...(bearerToken && !useOAuth
|
|
29
|
+
? { Authorization: `Bearer ${bearerToken}` }
|
|
30
|
+
: {}),
|
|
31
|
+
},
|
|
32
|
+
returnError: true,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
/**
|
|
37
|
+
* Post a single tweet
|
|
38
|
+
* @param {string} text - Tweet text
|
|
39
|
+
* @param {Object} [options] - Additional options
|
|
40
|
+
* @return {Promise<Object>} Tweet response
|
|
41
|
+
*/
|
|
42
|
+
async postTweet(text, options = {}) {
|
|
43
|
+
const { response, error } = await twitter.post('/tweets', {
|
|
44
|
+
body: {
|
|
45
|
+
text,
|
|
46
|
+
...options,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (response) return response.body
|
|
51
|
+
if (error) throw error
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Post a thread (reply chain)
|
|
56
|
+
* @param {string[]} tweets - Array of tweet texts
|
|
57
|
+
* @return {Promise<Object[]>} Array of tweet responses
|
|
58
|
+
*/
|
|
59
|
+
async postThread(tweets) {
|
|
60
|
+
const results = []
|
|
61
|
+
let replyToId = null
|
|
62
|
+
|
|
63
|
+
for (const tweet of tweets) {
|
|
64
|
+
const options = replyToId
|
|
65
|
+
? { reply: { in_reply_to_tweet_id: replyToId } }
|
|
66
|
+
: {}
|
|
67
|
+
const result = await this.postTweet(tweet, options)
|
|
68
|
+
replyToId = result.data?.id
|
|
69
|
+
results.push(result)
|
|
70
|
+
|
|
71
|
+
// Small delay between tweets to avoid rate limits
|
|
72
|
+
if (tweets.length > 1) {
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return results
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Post content (auto-detects single vs thread)
|
|
82
|
+
* @param {string|string[]} content - Single tweet or array for thread
|
|
83
|
+
* @return {Promise<Object|Object[]>} Tweet response(s)
|
|
84
|
+
*/
|
|
85
|
+
async post(content) {
|
|
86
|
+
if (Array.isArray(content)) return this.postThread(content)
|
|
87
|
+
return this.postTweet(content)
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@splendidlabz/third-party",
|
|
3
3
|
"prettier": "@splendidlabz/prettier-config",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.10",
|
|
5
5
|
"description": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"sideEffects": false,
|
|
9
9
|
"exports": {
|
|
10
|
-
"./*": "./lib/*.js"
|
|
10
|
+
"./*": "./lib/*.js",
|
|
11
|
+
"./social": "./lib/social/index.js"
|
|
11
12
|
},
|
|
12
13
|
"scripts": {
|
|
13
14
|
"lint": "eslint . --fix",
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
},
|
|
17
18
|
"author": "Zell Liew <zellwk@gmail.com>",
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@splendidlabz/utils": "1.10.
|
|
20
|
+
"@splendidlabz/utils": "1.10.4",
|
|
20
21
|
"googleapis": "^148.0.0",
|
|
21
22
|
"http-errors": "^2.0.0",
|
|
22
23
|
"pino": "^9.7.0",
|