├── .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 }; --------------------------------------------------------------------------------