Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions apps/github/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PRIVATE_REPO_NAME=
PUBLIC_REPO_NAME=
ORGANIZATION_NAME=
TOKEN=
10 changes: 7 additions & 3 deletions apps/github/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
]
},
"devDependencies": {
"@types/express": "^4.17.15",
"@types/node": "^15.3.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
Expand All @@ -36,12 +37,15 @@
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-no-loops": "^0.3.0",
"eslint-plugin-prettier": "^4.2.1",
"nodemon": "^2.0.7",
"nodemon": "^2.0.20",
"prettier": "^2.8.3",
"ts-node": "^9.1.1",
"ts-node": "^10.9.1",
"tsconfig": "*",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.2.4"
},
"dependencies": {}
"dependencies": {
"axios": "^1.2.3",
"express": "^4.18.2"
}
}
8 changes: 8 additions & 0 deletions apps/github/src/@types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type Payload = {
action: string;
sender: Record<string, unknown>;
repository: Record<string, unknown>;
organization: Record<string, unknown>;
installation: Record<string, unknown>;
[key: string]: unknown;
};
54 changes: 54 additions & 0 deletions apps/github/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import AskBuddieBot from 'src/libs/askbuddiebot';
import express from 'express';

const app = express();
const port = 3000;

const askBuddieBot = AskBuddieBot.getInstance();

app.use(express.json());

app.post('/payload', async (req, res) => {
const payload = req.body as Payload;

// get event name and action from the request
const event = req.headers['x-github-event'] ?? null;

if (event === 'ping') return res.status(200).send('OK');

if (event === null) return res.status(400).send('Unknown event.');

const eventName = `${event}.${payload.action}`;

// find the registered event in the bot
let botEvent;
try {
botEvent = askBuddieBot.getEvent(eventName);
} catch (e: unknown) {
if (e instanceof Error) {
console.error(e.message);
} else {
console.error('Something went wrong!');
}
return;
}

// check if the event is from registered repository
if (
!askBuddieBot.isValidRepository(
(payload.repository?.name as string) ?? ''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work?

Suggested change
(payload.repository?.name as string) ?? ''
payload.repository?.name ?? ''

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
i get this error without typing it as string.

)
)
return res.status(400).send('Not allowed.');

// execute the event
const success = await botEvent?.handleEvent(payload);

if (!success) return res.status(500).send('Something went wrong!');

res.status(200).send('OK');
});

app.listen(port, () => {
console.log(`App started on port ${port}.`);
});
15 changes: 15 additions & 0 deletions apps/github/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
type Config = {
PRIVATE_REPO_NAME?: string;
PUBLIC_REPO_NAME?: string;
ORGANIZATION_NAME?: string;
TOKEN?: string;
};

const config: Config = {
PRIVATE_REPO_NAME: process.env.PRIVATE_REPO_NAME,
PUBLIC_REPO_NAME: process.env.PUBLIC_REPO_NAME,
ORGANIZATION_NAME: process.env.ORGANIZATION_NAME,
TOKEN: process.env.TOKEN
};

export default config;
1 change: 1 addition & 0 deletions apps/github/src/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as IssuesOpened } from './issues-opened';
26 changes: 26 additions & 0 deletions apps/github/src/events/issues-opened.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Event from '../libs/event';
import AskBuddieBot from 'src/libs/askbuddiebot';

type Issue = {
body: string | null;
title: string;
[key: string]: unknown;
};

class IssuesOpened implements Event {
name = 'issues.opened';

async handleEvent(payload: Payload): Promise<boolean> {
const issue = payload.issue as Issue;

console.info(`Issue opened: ${issue.title}`);

const body = issue.body + `<br/><br/>Ref: ${issue.title}-${issue.id}`;

return await AskBuddieBot.getInstance()
.getRepository()
.createIssue(issue.title, body);
}
}

export default IssuesOpened;
73 changes: 73 additions & 0 deletions apps/github/src/libs/askbuddiebot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as Events from '../events';
import Event from './event';
import config from 'src/config';
import GithubRepository from './github_repository';

type EventList = {
[key: string]: Event;
};

class AskBuddieBot {
private events: EventList = {};
private static instance: AskBuddieBot;
private repository: GithubRepository | undefined;

private constructor() {
console.info('Loading config...');
Object.entries(config).forEach(([, value]) => {
if (value == undefined) throw new Error('Invalid env file.');
});

console.info('Config loaded.');

console.info('Loading events...');
this.loadEvents();
console.info('Events loaded.');

console.info('Loading repositories');
this.loadRepository().then(() => {
console.info('Repository loaded');
});
}

public static getInstance(): AskBuddieBot {
if (!this.instance) this.instance = new AskBuddieBot();
return this.instance;
}

// load all the events from events directory with the key ${event}.{action}
private loadEvents(): void {
Object.entries(Events).forEach(([, Event]) => {
const e = new Event();
this.events[e.name] = e;
});
}

// get id of the github repos and create a graphql repository for all the requests
private async loadRepository(): Promise<void> {
const repo = new GithubRepository();
await repo.init();
this.repository = repo;
}

public getRepository(): GithubRepository {
if (!this.repository) throw new Error('Repository is loading!');

return this.repository;
}

// find the event from the key
public getEvent(e: string): Event {
if (!(e in this.events)) {
throw new Error(e + ' is not registered in events!');
}

return this.events[e];
}

public isValidRepository(name: string): boolean {
return name === config.PRIVATE_REPO_NAME;
}
}

export default AskBuddieBot;
6 changes: 6 additions & 0 deletions apps/github/src/libs/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
interface Event {
name: string;
handleEvent(payload: Payload): Promise<boolean>;
}

export default Event;
86 changes: 86 additions & 0 deletions apps/github/src/libs/github_repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import config from 'src/config';
import RequestHandler from './request_handler';

type OrganizationRepo = {
private: {
id: string;
};
public: {
id: string;
};
};

class GithubRepository {
private publicRepoId = '';
private privateRepoId = '';
private requestHandler: RequestHandler;

constructor(requestHanlder: RequestHandler = new RequestHandler()) {
this.requestHandler = requestHanlder;
}

public async init() {
await this.getRepositoryId();
}

public async getRepositoryId(): Promise<void> {
const origanization: string = config.ORGANIZATION_NAME ?? '';
const privateRepo: string = config.PRIVATE_REPO_NAME ?? '';
const publicRepo: string = config.PUBLIC_REPO_NAME ?? '';

const query = `
query GetRepo($org:String!, $private:String!, $public:String!) {
organization(login: $org) {
private: repository(name: $private) {
id
}
public: repository(name: $public) {
id
}
}
}
`;

const res = await this.requestHandler.post(query, {
org: origanization,
private: privateRepo,
public: publicRepo
});

if (res.data.errors || !res.data.data)
throw new Error(res.data.errors.message ?? 'Something went wrong!');

const repo = res.data.data.organization as OrganizationRepo;

this.privateRepoId = repo.private.id;
this.publicRepoId = repo.public.id;
}

public async createIssue(title: string, body: string): Promise<boolean> {
const repo = this.publicRepoId;

const query = `
mutation CreateIssue($repositoryId: ID!, $title:String!, $body:String!) {
createIssue(input:{repositoryId: $repositoryId, title: $title, body: $body}) {
issue {
id
}
}
}

`;

const res = await this.requestHandler.post(query, {
repositoryId: repo,
title,
body
});

if (res.data.errors || !res.data.data)
throw new Error('Something went wrong!');

return true;
}
}

export default GithubRepository;
18 changes: 18 additions & 0 deletions apps/github/src/libs/request_handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Axios from 'axios';
import config from 'src/config';

class RequestHandler {
private endpoint = 'https://api.github.com/graphql';

public async post(query: string, variables: Record<string, unknown>) {
return await Axios.post(
this.endpoint,
{ query, variables },
{
headers: { Authorization: `Bearer ${config.TOKEN}` }
}
);
}
}

export default RequestHandler;
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"prepare": "husky install"
"prepare": "husky install",
"dev:github": "turbo run dev --filter github",
"dev:discord": "turbo run dev --filter discord"
},
"packageManager": "yarn@1.22.19",
"devDependencies": {
Expand Down
Loading