Skip to content

Commit

Permalink
feat: grok functionality (#45)
Browse files Browse the repository at this point in the history
* feat: add basic grok implementation

* restructure layout

* adjust imports

* adjust package

* update readme

* adjust readme

* adjust package

* adjust test asset

* adjust package

* adjust readme

* adjust package

* log adjustments

* adjust logs

* adjust readme

* adjust readme

* adjust readme

* add grok support

* adjust sample

* fix getArticle deletion

* fix link preview
  • Loading branch information
0x4337 authored Jan 9, 2025
1 parent a9b4ec3 commit 6678581
Show file tree
Hide file tree
Showing 8 changed files with 374 additions and 16 deletions.
Binary file modified .DS_Store
Binary file not shown.
107 changes: 98 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,11 @@ const tweet = await scraper.getTweet('1234567890123456789');
const sendTweetResults = await scraper.sendTweet('Hello world!');

// Send a quote tweet - Media files are optional
const sendQuoteTweetResults = await scraper.sendQuoteTweet('Hello world!', '1234567890123456789', ['mediaFile1', 'mediaFile2']);
const sendQuoteTweetResults = await scraper.sendQuoteTweet(
'Hello world!',
'1234567890123456789',
['mediaFile1', 'mediaFile2'],
);

// Retweet a tweet
const retweetResults = await scraper.retweet('1234567890123456789');
Expand All @@ -228,48 +232,133 @@ const likeTweetResults = await scraper.likeTweet('1234567890123456789');
## Sending Tweets with Media

### Media Handling

The scraper requires media files to be processed into a specific format before sending:

- Media must be converted to Buffer format
- Each media file needs its MIME type specified
- This helps the scraper distinguish between image and video processing models

### Basic Tweet with Media

```ts
// Example: Sending a tweet with media attachments
const mediaData = [
{
data: fs.readFileSync('path/to/image.jpg'),
mediaType: 'image/jpeg'
mediaType: 'image/jpeg',
},
{
data: fs.readFileSync('path/to/video.mp4'),
mediaType: 'video/mp4'
}
mediaType: 'video/mp4',
},
];

await scraper.sendTweet('Hello world!', undefined, mediaData);
```

### Supported Media Types

```ts
// Image formats and their MIME types
const imageTypes = {
'.jpg': 'image/jpeg',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif'
'.png': 'image/png',
'.gif': 'image/gif',
};

// Video format
const videoTypes = {
'.mp4': 'video/mp4'
'.mp4': 'video/mp4',
};
```


### Media Upload Limitations

- Maximum 4 images per tweet
- Only 1 video per tweet
- Maximum video file size: 512MB
- Supported image formats: JPG, PNG, GIF
- Supported video format: MP4

## Grok Integration

This client provides programmatic access to Grok through Twitter's interface, offering a unique capability that even Grok's official API cannot match - access to real-time Twitter data. While Grok has a standalone API, only by interacting with Grok through Twitter can you leverage its ability to analyze and respond to live Twitter content. This makes it the only way to programmatically access an LLM with direct insight into Twitter's real-time information. [@grokkyAi](https://x.com/grokkyAi)

### Basic Usage

```ts
const scraper = new Scraper();
await scraper.login('username', 'password');

// Start a new conversation
const response = await scraper.grokChat({
messages: [{ role: 'user', content: 'What are your thoughts on AI?' }],
});

console.log(response.message); // Grok's response
console.log(response.messages); // Full conversation history
```

If no `conversationId` is provided, the client will automatically create a new conversation.

### Handling Rate Limits

Grok has rate limits of 25 messages every 2 hours for non-premium accounts. The client provides rate limit information in the response:

```ts
const response = await scraper.grokChat({
messages: [{ role: 'user', content: 'Hello!' }],
});

if (response.rateLimit?.isRateLimited) {
console.log(response.rateLimit.message);
console.log(response.rateLimit.upsellInfo); // Premium upgrade information
}
```

### Response Types

The Grok integration includes TypeScript types for better development experience:

```ts
interface GrokChatOptions {
messages: GrokMessage[];
conversationId?: string;
returnSearchResults?: boolean;
returnCitations?: boolean;
}

interface GrokChatResponse {
conversationId: string;
message: string;
messages: GrokMessage[];
webResults?: any[];
metadata?: any;
rateLimit?: GrokRateLimit;
}
```

### Advanced Usage

```ts
const response = await scraper.grokChat({
messages: [{ role: 'user', content: 'Research quantum computing' }],
returnSearchResults: true, // Include web search results
returnCitations: true, // Include citations for information
});

// Access web results if available
if (response.webResults) {
console.log('Sources:', response.webResults);
}

// Full conversation with history
console.log('Conversation:', response.messages);
```

### Limitations

- Message history prefilling is currently limited due to unofficial API usage
- Rate limits are enforced (25 messages/2 hours for non-premium)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,4 @@
"prettier --write"
]
}
}
}
Binary file added src/.DS_Store
Binary file not shown.
54 changes: 48 additions & 6 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export async function requestApi<T>(
auth: TwitterAuth,
method: 'GET' | 'POST' = 'GET',
platform: PlatformExtensions = new Platform(),
body?: any,
): Promise<RequestApiResult<T>> {
const headers = new Headers();
await auth.installTo(headers, url);
Expand All @@ -63,12 +64,12 @@ export async function requestApi<T>(
method,
headers,
credentials: 'include',
...(body && { body: JSON.stringify(body) }),
});
} catch (err) {
if (!(err instanceof Error)) {
throw err;
}

return {
success: false,
err: new Error('Failed to perform request.'),
Expand Down Expand Up @@ -103,13 +104,54 @@ export async function requestApi<T>(
};
}

const value: T = await res.json();
if (res.headers.get('x-rate-limit-incoming') == '0') {
auth.deleteToken();
return { success: true, value };
} else {
// Check if response is chunked
const transferEncoding = res.headers.get('transfer-encoding');
if (transferEncoding === 'chunked') {
// Handle streaming response
const reader = res.body?.getReader();
if (!reader) {
return {
success: false,
err: new Error('No readable stream available'),
};
}

let chunks: any = '';
// Read all chunks before attempting to parse
while (true) {
const { done, value } = await reader.read();
if (done) break;

// Convert chunk to text and append
chunks += new TextDecoder().decode(value);

// Log chunk for debugging (optional)
// console.log('Received chunk:', new TextDecoder().decode(value));
}

// Now try to parse the complete accumulated response
try {
// console.log('attempting to parse chunks', chunks);
const value = JSON.parse(chunks);
return { success: true, value };
} catch (e) {
// console.log('parsing chunks failed, sending as raw text');
// If we can't parse as JSON, return the raw text
return { success: true, value: { text: chunks } as any };
}
}

// Handle non-streaming responses as before
const contentType = res.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const value: T = await res.json();
if (res.headers.get('x-rate-limit-incoming') == '0') {
auth.deleteToken();
}
return { success: true, value };
}

return { success: true, value: {} as T };
}

/** @internal */
Expand Down
Loading

0 comments on commit 6678581

Please sign in to comment.