Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client-reddit): Eliza client for reddit #2538

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/plugin-reddit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@ai16z/plugin-reddit",
"version": "0.1.0",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"dependencies": {
"@ai16z/eliza": "workspace:*",
"snoowrap": "^1.23.0" // Reddit API wrapper
wtfsayo marked this conversation as resolved.
Show resolved Hide resolved
},
"devDependencies": {
"vitest": "^2.1.4",
"@types/node": "^20.0.0",
"tsup": "8.3.5"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch",
"test": "vitest run",
"test:watch": "vitest watch"
}
}
Empty file.
54 changes: 54 additions & 0 deletions packages/plugin-reddit/src/actions/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Action, IAgentRuntime, Memory } from "@ai16z/eliza";

export const createPost: Action = {
name: "CREATE_REDDIT_POST",
similes: ["POST_TO_REDDIT", "SUBMIT_REDDIT_POST"],
validate: async (runtime: IAgentRuntime, message: Memory) => {
const hasCredentials = !!runtime.getSetting("REDDIT_CLIENT_ID") &&
!!runtime.getSetting("REDDIT_CLIENT_SECRET") &&
!!runtime.getSetting("REDDIT_REFRESH_TOKEN");
return hasCredentials;
},
wtfsayo marked this conversation as resolved.
Show resolved Hide resolved
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: any,
options: any
) => {
const { reddit } = await runtime.getProvider("redditProvider");

// Extract subreddit and content from message
const subreddit = "test"; // You would parse this from message
const title = "Test Post";
const content = message.content.text;
wtfsayo marked this conversation as resolved.
Show resolved Hide resolved

try {
await reddit.submitSelfpost({
subredditName: subreddit,
title: title,
text: content
});
return true;
} catch (error) {
console.error("Failed to create Reddit post:", error);
return false;
}
},
examples: [
[
{
user: "{{user1}}",
content: {
text: "Post this to r/test: Hello Reddit!"
},
},
{
user: "{{agentName}}",
content: {
text: "I'll post that to Reddit for you",
action: "CREATE_REDDIT_POST",
},
},
],
],
};
Empty file.
15 changes: 15 additions & 0 deletions packages/plugin-reddit/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Plugin } from "@ai16z/eliza";
import { createPost } from "./actions/post";
import { createComment } from "./actions/comment";
import { vote } from "./actions/vote";
import { redditProvider } from "./providers/redditProvider";

export const redditPlugin: Plugin = {
name: "reddit",
description: "Reddit Plugin for Eliza - Interact with Reddit posts, comments and voting",
actions: [createPost, createComment, vote],
providers: [redditProvider],
evaluators: []
};
wtfsayo marked this conversation as resolved.
Show resolved Hide resolved

export default redditPlugin;
26 changes: 26 additions & 0 deletions packages/plugin-reddit/src/providers/redditProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Provider, IAgentRuntime } from "@ai16z/eliza";
import Snoowrap from 'snoowrap';

export const redditProvider: Provider = {
name: "redditProvider",
description: "Provides Reddit API functionality",
get: async (runtime: IAgentRuntime) => {
const clientId = runtime.getSetting("REDDIT_CLIENT_ID");
const clientSecret = runtime.getSetting("REDDIT_CLIENT_SECRET");
const refreshToken = runtime.getSetting("REDDIT_REFRESH_TOKEN");
const userAgent = runtime.getSetting("REDDIT_USER_AGENT");

if (!clientId || !clientSecret || !refreshToken || !userAgent) {
throw new Error("Missing Reddit credentials");
}

const reddit = new Snoowrap({
userAgent,
clientId,
clientSecret,
refreshToken
});

wtfsayo marked this conversation as resolved.
Show resolved Hide resolved
return { reddit };
}
};
18 changes: 18 additions & 0 deletions packages/plugin-reddit/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface RedditPost {
id: string;
subreddit: string;
title: string;
content: string;
author: string;
score: number;
created: Date;
}
wtfsayo marked this conversation as resolved.
Show resolved Hide resolved

export interface RedditComment {
id: string;
postId: string;
content: string;
author: string;
score: number;
created: Date;
}
wtfsayo marked this conversation as resolved.
Show resolved Hide resolved
69 changes: 69 additions & 0 deletions packages/plugin-reddit/tests/actions/post.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, it, expect, vi } from 'vitest';
import { createPost } from '../../src/actions/post';
import { mockRuntime, mockMemory, mockRedditClient } from '../setup';

describe('CREATE_REDDIT_POST Action', () => {
it('should validate with correct credentials', async () => {
const result = await createPost.validate(mockRuntime, mockMemory);
expect(result).toBe(true);
});

it('should fail validation with missing credentials', async () => {
const runtimeWithoutCreds = {
...mockRuntime,
getSetting: vi.fn(() => undefined),
};

const result = await createPost.validate(runtimeWithoutCreds, mockMemory);
expect(result).toBe(false);
});

it('should successfully create a post', async () => {
mockRedditClient.submitSelfpost.mockResolvedValueOnce({
id: 'test_post_id',
url: 'https://reddit.com/r/test/comments/test_post_id',
});

const result = await createPost.handler(
mockRuntime,
mockMemory,
{},
{}
);

expect(result).toBe(true);
expect(mockRedditClient.submitSelfpost).toHaveBeenCalledWith({
subredditName: expect.any(String),
title: expect.any(String),
text: expect.any(String),
});
});

it('should handle post creation errors', async () => {
mockRedditClient.submitSelfpost.mockRejectedValueOnce(
new Error('Failed to create post')
);

const result = await createPost.handler(
mockRuntime,
mockMemory,
{},
{}
);

expect(result).toBe(false);
});

it('should have correct action name and similes', () => {
expect(createPost.name).toBe('CREATE_REDDIT_POST');
expect(createPost.similes).toContain('POST_TO_REDDIT');
expect(createPost.similes).toContain('SUBMIT_REDDIT_POST');
});

it('should have valid examples', () => {
expect(createPost.examples).toBeDefined();
expect(createPost.examples.length).toBeGreaterThan(0);
expect(createPost.examples[0]).toHaveLength(2);
expect(createPost.examples[0][1].content.action).toBe('CREATE_REDDIT_POST');
});
});
75 changes: 75 additions & 0 deletions packages/plugin-reddit/tests/providers/redditProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, it, expect, vi } from 'vitest';
import { redditProvider } from '../../src/providers/redditProvider';
import { mockRuntime } from '../setup';

describe('Reddit Provider', () => {
it('should initialize with correct configuration', async () => {
const data = await redditProvider.get(mockRuntime);
expect(data).toBeDefined();
expect(data.reddit).toBeDefined();
});

it('should throw error when missing credentials', async () => {
const runtimeWithoutCreds = {
...mockRuntime,
getSetting: vi.fn(() => undefined),
};

await expect(
redditProvider.get(runtimeWithoutCreds)
).rejects.toThrow('Missing Reddit credentials');
});

it('should use correct credentials from runtime settings', async () => {
const data = await redditProvider.get(mockRuntime);

expect(mockRuntime.getSetting).toHaveBeenCalledWith('REDDIT_CLIENT_ID');
expect(mockRuntime.getSetting).toHaveBeenCalledWith('REDDIT_CLIENT_SECRET');
expect(mockRuntime.getSetting).toHaveBeenCalledWith('REDDIT_REFRESH_TOKEN');
expect(mockRuntime.getSetting).toHaveBeenCalledWith('REDDIT_USER_AGENT');
});

it('should return reddit client instance', async () => {
const data = await redditProvider.get(mockRuntime);
expect(data).toHaveProperty('reddit');
});

it('should cache reddit client instance', async () => {
// First call
const data1 = await redditProvider.get(mockRuntime);
// Second call
const data2 = await redditProvider.get(mockRuntime);

expect(data1.reddit).toBe(data2.reddit);
expect(mockRuntime.getSetting).toHaveBeenCalledTimes(4); // Only called once for each setting
});

it('should handle network errors gracefully', async () => {
vi.mock('snoowrap', () => {
return {
default: vi.fn().mockImplementation(() => {
throw new Error('Network Error');
}),
};
});

await expect(
redditProvider.get(mockRuntime)
).rejects.toThrow('Failed to initialize Reddit client');
});

it('should validate user agent format', async () => {
const runtimeWithInvalidUserAgent = {
...mockRuntime,
getSetting: vi.fn((key: string) => {
if (key === 'REDDIT_USER_AGENT') return '';
return 'test_value';
}),
};

await expect(
redditProvider.get(runtimeWithInvalidUserAgent)
).rejects.toThrow('Invalid user agent');
});
});

45 changes: 45 additions & 0 deletions packages/plugin-reddit/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { beforeAll, afterAll, vi } from 'vitest';
import { IAgentRuntime, Memory } from '@ai16z/eliza';

export const mockRedditClient = {
submitSelfpost: vi.fn(),
getSubreddit: vi.fn(),
getSubmission: vi.fn(),
getComment: vi.fn(),
};

export const mockRuntime: IAgentRuntime = {
getSetting: vi.fn((key: string) => {
switch (key) {
case 'REDDIT_CLIENT_ID':
return 'test_client_id';
case 'REDDIT_CLIENT_SECRET':
return 'test_client_secret';
case 'REDDIT_REFRESH_TOKEN':
return 'test_refresh_token';
case 'REDDIT_USER_AGENT':
return 'test_user_agent';
default:
return undefined;
}
}),
getProvider: vi.fn().mockResolvedValue({ reddit: mockRedditClient }),
};

export const mockMemory: Memory = {
id: 'test-memory',
roomId: 'test-room',
timestamp: new Date(),
user: 'test-user',
content: {
text: 'Post this to r/test: Hello Reddit!',
},
};

beforeAll(() => {
vi.clearAllMocks();
});

afterAll(() => {
vi.resetAllMocks();
});
Loading