├── .github
└── FUNDING.yml
├── .gitignore
├── .gitattributes
├── .env
├── src
├── redis
│ └── redis.js
├── slack
│ └── slack.js
└── api
│ └── auth.js
├── index.js
├── package.json
├── LICENSE
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: devios01
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | .env
3 | .vscode
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | GITHUB_CLIENT_ID=
2 | GITHUB_CLIENT_SECRET=
3 | SLACK_BOT_TOKEN=
4 | SLACK_APP_TOKEN=
5 | SLACK_SIGNING_SECRET=
6 |
--------------------------------------------------------------------------------
/src/redis/redis.js:
--------------------------------------------------------------------------------
1 | const { Redis } = require('ioredis');
2 |
3 | const REDIS_URL = process.env.REDIS_URL;
4 | const redis = new Redis(REDIS_URL);
5 |
6 | redis.on('connect', () => {
7 | console.log(`Connected to Redis at ${redis.options.host}:${redis.options.port}`);
8 | });
9 |
10 | redis.on('error', (error) => {
11 | console.error(`Error connecting to Redis: ${error.message}`);
12 | });
13 |
14 | module.exports = { redis };
15 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 |
3 | const { config } = require('dotenv');
4 | config();
5 |
6 | const { authRouter } = require('./src/api/auth');
7 | require('./src/redis/redis');
8 | require('./src/slack/slack');
9 |
10 | const app = express();
11 | app.use(express.json());
12 | app.use(express.urlencoded({ extended: true }));
13 |
14 | app.use('/auth', authRouter);
15 |
16 | const port = process.env.PORT || 3000;
17 |
18 | app.listen(port, () => {
19 | console.log(`Server started on port ${port}`);
20 | });
21 |
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starthisproject",
3 | "version": "1.0.0",
4 | "description": "react with star emoji to auto star the github project in gihtub through slack",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "DevIos",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@slack/bolt": "^3.12.2",
13 | "axios": "^1.3.4",
14 | "dotenv": "^16.0.3",
15 | "ioredis": "^5.3.1",
16 | "redis": "^4.6.5",
17 | "util": "^0.12.5"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 NOVA52
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitHub Star Bot
2 | This application allows Slack users to star GitHub repositories by reacting to messages with a :star: emoji inside Slack Channel.
3 |
4 | # Setup
5 | To run this application, you need to have the following:
6 |
7 | * A Slack workspace with administrative access
8 | * A Slack app configured in that workspace
9 | * A Slack bot token for the app
10 | * A Slack app token for the app
11 | * A Slack signing secret for the app
12 | * A GitHub account with administrative access
13 | * A GitHub OAuth app configured in that account
14 | * A GitHub client ID for the app
15 | * A GitHub client secret for the app
16 | * A Redis instance
17 |
18 | To set up the application, you need to take the following steps:
19 |
20 | 1. Clone this repository to your local machine.
21 | 2. Install the dependencies by running the following command in your terminal:
22 | ```bash
23 | npm install
24 | ```
25 | 3. Edit `.env` file in the project with the following variables:
26 | ```bash
27 | GITHUB_CLIENT_ID=
28 | GITHUB_CLIENT_SECRET=
29 | SLACK_BOT_TOKEN=
30 | SLACK_APP_TOKEN=
31 | SLACK_SIGNING_SECRET=
32 | ```
33 | 4.Start the Redis instance by running the following command in your terminal:
34 | ```bash
35 | docker-compose up -d
36 | ```
37 | 5.Start the application by running the following command in your terminal:
38 | ```bash
39 | npm start
40 | ```
41 | 6.In your Slack workspace, install the app and authorize it to access your Slack account.
42 |
43 | 7.In your GitHub account, configure the OAuth app and set the authorization callback URL to http://localhost:3000/auth/callback
44 |
45 | 8.In your Slack workspace, you can now react to a github link and if its not authorized , it will send you message with link to authorize.
46 |
47 | # Usage
48 | To use the application, you need to take the following steps:
49 |
50 | Post a message in a channel with a link to a GitHub repository.
51 | React to the message with a :star: emoji.
52 | The bot will star the repository on GitHub if you have authorized it to access your GitHub account.
53 |
54 | # License
55 | This project is licensed under the MIT License. See the LICENSE file for details.
56 |
--------------------------------------------------------------------------------
/src/slack/slack.js:
--------------------------------------------------------------------------------
1 | const { App, LogLevel } = require('@slack/bolt');
2 | const axios = require('axios');
3 | const { redis } = require('../redis/redis');
4 |
5 | const slackBotToken = process.env.SLACK_BOT_TOKEN;
6 | const slackAppToken = process.env.SLACK_APP_TOKEN;
7 | const slackSigningSecret = process.env.SLACK_SIGNING_SECRET;
8 | const githubClientId = process.env.GITHUB_CLIENT_ID;
9 |
10 | // Slack app
11 | const slackApp = new App({
12 | token: slackBotToken,
13 | appToken: slackAppToken,
14 | socketMode: true,
15 | signingSecret: slackSigningSecret,
16 | logLevel: LogLevel.DEBUG,
17 | });
18 |
19 | slackApp.event('reaction_added', async ({ event, context }) => {
20 |
21 | if (event.reaction !== 'star') {
22 | return;
23 | }
24 |
25 | const result = await slackApp.client.conversations.history({
26 | channel: event.item.channel,
27 | latest: event.item.ts,
28 | inclusive: true, // Limit the results to only one
29 | limit: 1
30 | });
31 |
32 | const message = result.messages[0];
33 |
34 | if (!message.text) {
35 | return;
36 | }
37 |
38 | const regex = /https:\/\/github.com\/(.+?)\/(.+?)(?:\s|>)/;
39 | const match = message.text.match(regex);
40 | if (!match) {
41 | return;
42 | }
43 |
44 | const githubOwner = match[1];
45 | const githubRepo = match[2];
46 |
47 | const state = {
48 | slackUserId: event.user,
49 | channel: event.item.channel,
50 | };
51 | const authorizationUrl = `https://github.com/login/oauth/authorize?client_id=${githubClientId}&scope=read:user,public_repo&state=${encodeURIComponent(
52 | JSON.stringify(state)
53 | )}`;
54 |
55 | const githubId = await redis.get(`slack-id-to-github-id:${event.user}`);
56 |
57 | if (!githubId) {
58 |
59 | await slackApp.client.chat.postEphemeral({
60 | channel: event.item.channel,
61 | user: event.user,
62 | text: `Seems like you haven\'t authorized the app to access your GitHub account yet. To do so, click the link <${authorizationUrl}|here> and follow the instructions.`,
63 | });
64 | return;
65 | }
66 |
67 | const accessToken = await redis.get(`github-token:${githubId}`);
68 |
69 | if (!accessToken) {
70 |
71 | await slackApp.client.chat.postEphemeral({
72 | channel: event.item.channel,
73 | user: event.user,
74 | text: `Seems like you haven\'t authorized the app to access your GitHub account yet. To do so, click the link <${authorizationUrl}|here> and follow the instructions.`,
75 | });
76 | return;
77 | }
78 |
79 | try {
80 | const response = await axios.put(
81 | `https://api.github.com/user/starred/${githubOwner}/${githubRepo}`,
82 | null,
83 | {
84 | headers: {
85 | Authorization: `Bearer ${accessToken}`,
86 | Accept: 'application/vnd.github.v3+json',
87 | },
88 | }
89 | );
90 |
91 | await slackApp.client.chat.postEphemeral({
92 | channel: event.item.channel,
93 | user: event.user,
94 | text: `You have successfully starred ${githubOwner}/${githubRepo}!`,
95 | });
96 |
97 | console.log(`Starred ${githubOwner}/${githubRepo} for user ${githubId}`);
98 | } catch (error) {
99 | console.error(`Error starring ${githubOwner}/${githubRepo} for user ${githubId}: ${error.message}`);
100 | }
101 | });
102 |
103 | // Start the Slack app
104 | const StartSlackApp = () => new Promise(async (resolve, reject) => {
105 | slackApp.start(process.env.SLACK_PORT || 4000)
106 | .then((result) => {
107 | console.log('slackApp is running! in port 4000');
108 | resolve();
109 | })
110 | .catch(reject);
111 | });
112 |
113 | StartSlackApp();
114 |
115 | module.exports = { slackApp };
--------------------------------------------------------------------------------
/src/api/auth.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express');
2 | const axios = require('axios');
3 | const { redis } = require('../redis/redis');
4 | const { slackApp } = require('../slack/slack');
5 |
6 | const githubClientId = process.env.GITHUB_CLIENT_ID;
7 | const githubClientSecret = process.env.GITHUB_CLIENT_SECRET;
8 |
9 | var authRouter = Router();
10 |
11 | authRouter.get("/callback", async (req, res) => {
12 | const { code, state } = req.query;
13 |
14 | console.log("code: " + code);
15 | console.log("state: " + state);
16 |
17 | res.send('GitHub authentication successful! You can now star GitHub repositories by reacting to messages with a :star: emoji.');
18 |
19 | try {
20 | const accessToken = await exchangeCodeForToken(JSON.parse(state), code);
21 | if (accessToken) {
22 |
23 | const stateObj = JSON.parse(state);
24 | const channelId = stateObj.channel;
25 | const slackUserId = stateObj.slackUserId;
26 |
27 | // post a message to the channel
28 | await slackApp.client.chat.postEphemeral({
29 | channel: channelId,
30 | user: slackUserId,
31 | text: 'GitHub authentication successful! You can now star GitHub repositories by reacting to messages with a :star: emoji.'
32 | });
33 |
34 | } else {
35 |
36 | const stateObj = JSON.parse(state);
37 | const channelId = stateObj.channel;
38 | const slackUserId = stateObj.slackUserId;
39 |
40 | // post a message to the channel
41 | await slackApp.client.chat.postEphemeral({
42 | channel: channelId,
43 | user: slackUserId,
44 | text: 'GitHub authentication failed. Please try again.'
45 | });
46 |
47 | res.send('GitHub authentication failed. Please try again.');
48 | }
49 | } catch (error) {
50 |
51 | const stateObj = JSON.parse(state);
52 | const channelId = stateObj.channel;
53 | const slackUserId = stateObj.slackUserId;
54 |
55 | console.log(channelId)
56 | console.log(slackUserId)
57 |
58 | // post a message to the channel
59 | await slackApp.client.chat.postEphemeral({
60 | channel: channelId,
61 | user: slackUserId,
62 | text: `Error authenticating with GitHub: ${error.message}`
63 | });
64 |
65 | res.send(`Error authenticating with GitHub: ${error.message}`);
66 | }
67 | });
68 |
69 | // exchanges a GitHub OAuth code for an access token
70 | async function exchangeCodeForToken(state, code) {
71 | try {
72 | const response = await axios.post(
73 | 'https://github.com/login/oauth/access_token',
74 | {
75 | client_id: githubClientId,
76 | client_secret: githubClientSecret,
77 | code,
78 | state,
79 | },
80 | {
81 | headers: {
82 | Accept: 'application/json',
83 | },
84 | }
85 | );
86 | const accessToken = response.data.access_token;
87 |
88 | // Use the access token to fetch the user's GitHub ID
89 | const userResponse = await axios.get('https://api.github.com/user', {
90 | headers: {
91 | Authorization: `Bearer ${accessToken}`,
92 | Accept: 'application/vnd.github.v3+json',
93 | },
94 | });
95 | const githubId = userResponse.data.id;
96 |
97 | // Save the access token, GitHub ID, and Slack ID to Redis
98 | await redis.set(`github-token:${githubId}`, accessToken);
99 | await redis.set(`github-id-to-slack-id:${githubId}`, state.slackUserId);
100 | await redis.set(`slack-id-to-github-id:${state.slackUserId}`, githubId);
101 |
102 | return accessToken;
103 | } catch (error) {
104 | console.error(`Error exchanging code for token: ${error.message}`);
105 | return null;
106 | }
107 | };
108 |
109 | module.exports = { authRouter };
--------------------------------------------------------------------------------