Skip to content

Commit

Permalink
refactor: improve code accroding best practises
Browse files Browse the repository at this point in the history
  • Loading branch information
KacperKoza343 committed Jan 21, 2025
1 parent 2d95829 commit 315a132
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 182 deletions.
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,6 @@ INSTAGRAM_MAX_ACTIONS=1 # Maximum number of actions to process at once

# LinkedIn Configuration
LINKEDIN_ACCESS_TOKEN= # LinkedIn access token
LINKEDIN_POST_INTERVAL_MIN=1 # Default: 60 minutes
LINKEDIN_POST_INTERVAL_MAX=2 # Default: 120 minutes

LINKEDIN_POST_INTERVAL_MIN=60 # Optional, default: 60 minutes
LINKEDIN_POST_INTERVAL_MAX=120 # Optional, default: 120 minutes
LINKEDIN_API_URL=https://api.linkedin.com # Optional, default: https://api.linkedin.com
7 changes: 4 additions & 3 deletions packages/client-linkedin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
"dist"
],
"dependencies": {
"@elizaos/core": "workspace:*"
"@elizaos/core": "workspace:*",
"axios": "^1.7.9"
},
"devDependencies": {
"@vitest/coverage-v8": "1.1.3",
"tsup": "8.3.5",
"vitest": "1.1.3",
"@vitest/coverage-v8": "1.1.3"
"vitest": "1.1.3"
},
"scripts": {
"build": "tsup --format esm --dts",
Expand Down
14 changes: 14 additions & 0 deletions packages/client-linkedin/src/helpers/get-random-integer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const getRandomInteger = (min: number, max: number) => {
if (Number.isNaN(min) || Number.isNaN(max)) {
throw new Error("Invalid range: min and max must be valid numbers");
}

if (min > max) {
throw new Error("Min value cannot be greater than max value");
}

const lower = Math.floor(min);
const upper = Math.floor(max);

return Math.floor(Math.random() * (upper - lower + 1)) + lower;
};
3 changes: 0 additions & 3 deletions packages/client-linkedin/src/helpers/get-random-number.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AxiosError } from "axios";

export const prepareAxiosErrorMessage = (error: AxiosError) => {
return JSON.stringify(
{
message: error.message,
status: error.response?.status,
data: error.response?.data,
code: error.code,
},
null,
2
);
};
86 changes: 60 additions & 26 deletions packages/client-linkedin/src/helpers/validate-config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { z, ZodError } from "zod";
import { IAgentRuntime } from "@elizaos/core";

const checkIfIsNumber = (val: string | number | null, ctx: z.RefinementCtx, path: string) => {
const DEFAULT_LINKEDIN_API_URL = "https://api.linkedin.com";
const DEFAULT_LINKEDIN_POST_INTERVAL_MIN = 60;
const DEFAULT_LINKEDIN_POST_INTERVAL_MAX = 120;

const parseNumber = (
val: string | number | null,
ctx: z.RefinementCtx,
path: string
) => {
const num = Number(val);

if(Number.isNaN(num)) {
if (Number.isNaN(num)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid number: ${val}`,
Expand All @@ -13,43 +21,69 @@ const checkIfIsNumber = (val: string | number | null, ctx: z.RefinementCtx, path
}

return num;
}

export const configSchema = z.object({
LINKEDIN_ACCESS_TOKEN: z.string(),
LINKEDIN_POST_INTERVAL_MIN: z.union([z.number(), z.string()])
.nullable()
.optional()
.default(60)
.transform((val, ctx) => checkIfIsNumber(val, ctx, 'LINKEDIN_POST_INTERVAL_MIN')),
LINKEDIN_POST_INTERVAL_MAX: z.union([z.number(), z.string()])
.nullable()
.optional()
.default(120)
.transform((val, ctx) => checkIfIsNumber(val, ctx, 'LINKEDIN_POST_INTERVAL_MAX')),
});
};

export const configSchema = z
.object({
LINKEDIN_ACCESS_TOKEN: z.string(),
LINKEDIN_POST_INTERVAL_MIN: z
.union([z.number(), z.string(), z.null(), z.undefined()])
.transform((val, ctx) => {
if (val === null || val === undefined) {
return DEFAULT_LINKEDIN_POST_INTERVAL_MIN;
}
return parseNumber(val, ctx, "LINKEDIN_POST_INTERVAL_MIN");
}),
LINKEDIN_POST_INTERVAL_MAX: z
.union([z.number(), z.string(), z.null(), z.undefined()])
.transform((val, ctx) => {
if (val === null || val === undefined) {
return DEFAULT_LINKEDIN_POST_INTERVAL_MAX;
}
return parseNumber(val, ctx, "LINKEDIN_POST_INTERVAL_MAX");
}),
LINKEDIN_API_URL: z
.union([z.string(), z.null(), z.undefined()])
.transform((val) => val ?? DEFAULT_LINKEDIN_API_URL),
})
.superRefine((data, ctx) => {
if (data.LINKEDIN_POST_INTERVAL_MIN > data.LINKEDIN_POST_INTERVAL_MAX) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Min value cannot be greater than max value",
path: ["LINKEDIN_POST_INTERVAL_MIN"],
});
}
});

export const validateConfig = (runtime: IAgentRuntime) => {
const LINKEDIN_ACCESS_TOKEN = runtime.getSetting("LINKEDIN_ACCESS_TOKEN");
const LINKEDIN_POST_INTERVAL_MIN = runtime.getSetting('LINKEDIN_POST_INTERVAL_MIN');
const LINKEDIN_POST_INTERVAL_MAX = runtime.getSetting("LINKEDIN_POST_INTERVAL_MAX");
const LINKEDIN_POST_INTERVAL_MIN = runtime.getSetting(
"LINKEDIN_POST_INTERVAL_MIN"
);
const LINKEDIN_POST_INTERVAL_MAX = runtime.getSetting(
"LINKEDIN_POST_INTERVAL_MAX"
);
const LINKEDIN_API_URL = runtime.getSetting("LINKEDIN_API_URL");

try {
const envs = configSchema.parse({
LINKEDIN_ACCESS_TOKEN,
LINKEDIN_POST_INTERVAL_MIN,
LINKEDIN_POST_INTERVAL_MAX,
LINKEDIN_API_URL,
});

return envs;
} catch (error) {
if(error instanceof ZodError) {
throw new Error(`Invalid environment variables. Validating envs failed with error: ${
error.issues.map(issue => issue.path.join('.') + ': ' + issue.message).join(' , ')
}`);
if (error instanceof ZodError) {
throw new Error(
`Invalid environment variables. Validating envs failed with error: ${error.issues
.map((issue) => issue.path.join(".") + ": " + issue.message)
.join(" , ")}`
);
} else {
throw new Error('Invalid environment variables.');
throw new Error("Invalid environment variables.");
}

}
}
};
14 changes: 6 additions & 8 deletions packages/client-linkedin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
import { Client, elizaLogger, IAgentRuntime } from "@elizaos/core";
import { validateConfig } from "./helpers/validate-config";
import axios from "axios";
import { LinkedInUserInfoFetcher } from "./services/LinkedinUserInfoFetcher";
import { PostsManager } from "./services/PostsManager";

const LINKEDIN_API_URL = "https://api.linkedin.com";
import { LinkedInUserInfoFetcher } from "./repositories/LinkedinUserInfoFetcher";
import { LinkedInPostScheduler } from "./services/LinkedInPostScheduler";

export const LinkedInClient: Client = {
async start(runtime: IAgentRuntime) {
const envs = validateConfig(runtime);

const axiosInstance = axios.create({
baseURL: LINKEDIN_API_URL,
baseURL: envs.LINKEDIN_API_URL,
headers: {
"Authorization": `Bearer ${envs.LINKEDIN_ACCESS_TOKEN}`,
},
});

const postManager = await PostsManager.create({
const linkedInPostScheduler = await LinkedInPostScheduler.createPostScheduler({
axiosInstance,
userInfoFetcher: new LinkedInUserInfoFetcher(axiosInstance),
runtime: this.runtime,
runtime,
config: {
LINKEDIN_POST_INTERVAL_MIN: envs.LINKEDIN_POST_INTERVAL_MIN,
LINKEDIN_POST_INTERVAL_MAX: envs.LINKEDIN_POST_INTERVAL_MAX,
}
});
postManager.createPostPublicationLoop();
linkedInPostScheduler.createPostPublicationLoop();


return this;
Expand Down
43 changes: 27 additions & 16 deletions packages/client-linkedin/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@ import { configSchema } from "./helpers/validate-config";
import { z } from "zod";

export interface UserInfo {
sub: string;
email_verified: boolean;
name: string;
locale: { country: string; language: string };
given_name: string;
family_name: string,
email: string,
picture?: string;
};
sub: string;
email_verified: boolean;
name: string;
locale: { country: string; language: string };
given_name: string;
family_name: string;
email: string;
picture?: string;
}

export interface MediaUploadUrl {
value: {
uploadUrlExpiresAt: number,
uploadUrl: string,
image: string
}
};
value: {
uploadUrlExpiresAt: number;
uploadUrl: string;
image: string;
};
}

export interface BasePostRequest {
author: string;
Expand All @@ -33,6 +33,14 @@ export interface BasePostRequest {
isReshareDisabledByAuthor: boolean;
}

export interface PublishPostParams {
postText: string;
media?: {
title: string;
id: string;
};
}

export interface PostRequestWithMedia extends BasePostRequest {
content?: {
media: {
Expand All @@ -45,4 +53,7 @@ export interface PostRequestWithMedia extends BasePostRequest {
export const API_VERSION_HEADER = "LinkedIn-Version";
export const API_VERSION = "202411";
export type Envs = z.infer<typeof configSchema>;
export type IntervalsConfig = Omit<Envs, "LINKEDIN_ACCESS_TOKEN">;
export type IntervalsConfig = Pick<
Envs,
"LINKEDIN_POST_INTERVAL_MAX" | "LINKEDIN_POST_INTERVAL_MIN"
>;
65 changes: 65 additions & 0 deletions packages/client-linkedin/src/repositories/LinkedinFileUploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { AxiosInstance, AxiosError } from "axios";
import { API_VERSION, API_VERSION_HEADER, MediaUploadUrl } from "../interfaces";
import { prepareAxiosErrorMessage } from "../helpers/prepare-axios-error-message";

export class LinkedInFileUploader {
constructor(
private readonly axios: AxiosInstance,
readonly userId: string
) {}

async uploadAsset(imageBlob: Blob) {
const { uploadUrl, imageId } = await this.createMediaUploadUrl();
await this.uploadMedia(uploadUrl, imageBlob);

return imageId;
}

async createMediaUploadUrl() {
try {
const initResponse = await this.axios.post<MediaUploadUrl>(
"/rest/images",
{
initializeUploadRequest: {
owner: `urn:li:person:${this.userId}`,
},
},
{
headers: {
[API_VERSION_HEADER]: [API_VERSION],
},
params: {
action: "initializeUpload",
},
}
);

return {
uploadUrl: initResponse.data.value.uploadUrl,
imageId: initResponse.data.value.image,
};
} catch (error) {
const isAxiosError = error instanceof AxiosError;

throw new Error(
`Failed create media upload url: ${isAxiosError ? prepareAxiosErrorMessage(error) : error}`
);
}
}

async uploadMedia(uploadUrl: string, imageBlob: Blob) {
try {
await this.axios.put(uploadUrl, imageBlob, {
headers: {
"Content-Type": "application/octet-stream",
},
});
} catch (error) {
const isAxiosError = error instanceof AxiosError;

throw new Error(
`Failed to upload media: ${isAxiosError ? prepareAxiosErrorMessage(error) : error}`
);
}
}
}
54 changes: 54 additions & 0 deletions packages/client-linkedin/src/repositories/LinkedinPostPublisher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AxiosInstance, AxiosError } from "axios";
import {
BasePostRequest,
PostRequestWithMedia,
API_VERSION_HEADER,
API_VERSION,
PublishPostParams,
} from "../interfaces";
import { prepareAxiosErrorMessage } from "../helpers/prepare-axios-error-message";

export class LinkedInPostPublisher {
constructor(
private readonly axios: AxiosInstance,
readonly userId: string
) {}

async publishPost({ postText, media }: PublishPostParams) {
const requestBody = this.buildPostRequest({ postText, media });

try {
await this.axios.post("/rest/posts", requestBody, {
headers: {
[API_VERSION_HEADER]: [API_VERSION],
},
});
} catch (error) {
const isAxiosError = error instanceof AxiosError;

throw new Error(
`Failed to publish LinkedIn post: ${isAxiosError ? prepareAxiosErrorMessage(error) : error}`
);
}
}

private buildPostRequest({
postText,
media,
}: PublishPostParams): BasePostRequest | PostRequestWithMedia {
const baseRequest: BasePostRequest = {
author: `urn:li:person:${this.userId}`,
commentary: postText,
visibility: "PUBLIC",
distribution: {
feedDistribution: "MAIN_FEED",
targetEntities: [],
thirdPartyDistributionChannels: [],
},
lifecycleState: "PUBLISHED",
isReshareDisabledByAuthor: false,
};

return media ? { ...baseRequest, content: { media } } : baseRequest;
}
}
Loading

0 comments on commit 315a132

Please sign in to comment.