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
10 changes: 4 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc -p .",
"clean": "rm -rf dist",
"build": "npm run clean && tsc -p .",
"build:watch": "tsc -w -p .",
"start": "npm run build && node dist/app.js",
"test": "echo \"Error: no test specified\" && exit 1"
Expand All @@ -14,17 +15,14 @@
"dependencies": {
"@slack/bolt": "^4.7.0",
"@slack/web-api": "^7.15.0",
"args": "^5.0.3",
"async": "^3.2.4",
"dotenv": "^16.3.1",
"express": "^5.2.1",
"googleapis": "^173.0.0",
"mongodb": "^4.8.1",
"nanoid": "^3.3.4",
"node-fetch": "^2.6.7"
},
"devDependencies": {
"@types/args": "^5.0.1",
"@types/async": "^3.2.21",
"@types/express": "^5.0.6",
"@types/node": "^17.0.25",
"@types/node-fetch": "^2.6.2",
"ts-node": "^10.7.0",
Expand Down
134 changes: 59 additions & 75 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,70 @@
import { config } from 'dotenv';
config();
import { loadConfig } from './config';
import { connectDb } from './db';
import { App } from '@slack/bolt';
import * as mongoDB from 'mongodb';
import { getBoltLogLevel } from './utils/slack';
import { SlackStorageModule } from './slack-storage';
import { AppHomeModule } from './app-home';

import 'dotenv/config'
import { ReactionCheckModule } from './reaction-check';
import { GoogleDriveCheckModule } from './google-drive-check';
import { google } from 'googleapis';

const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
appToken: process.env.SLACK_APP_TOKEN,
logLevel: getBoltLogLevel(process.env.LOG_LEVEL),
socketMode: true,
ignoreSelf: false,
});

app.use(async ({ next }) => {
await next!();
});
import { SlackStorageModule } from './modules/slack-storage';
import { AppHomeModule } from './modules/app-home';
import { ReactionCheckModule } from './modules/reaction-check';
import { GoogleDriveCheckModule } from './modules/google-drive/handler';
import { SettingsStore } from './modules/settings/store';
import { createWebServer } from './web/server';

(async () => {
const config = loadConfig();

const { client: dbClient, collections } = await connectDb(config.db.connString);

const settingsStore = new SettingsStore(collections.settings);
await settingsStore.load({
useReactionCheck: config.features.useReactionCheck,
useGoogleDriveCheck: config.features.useGoogleDriveCheck,
autoJoinChannels: config.app.autoJoinChannels,
reactionUserIgnoreList: config.features.reactionUserIgnoreList,
googleDriveReportOnlyViolations: config.features.googleDriveReportOnlyViolations,
googleDriveAuditChannel: config.features.googleDriveAuditChannel,
googleDriveAllowedSharedDriveIds: config.features.googleDriveAllowedSharedDriveIds,
googleDriveAllowedFolderIds: config.features.googleDriveAllowedFolderIds,
});

const app = new App({
token: config.slack.botToken,
signingSecret: config.slack.signingSecret,
appToken: config.slack.appToken,
logLevel: config.slack.logLevel,
socketMode: true,
ignoreSelf: false,
});

app.use(async ({ next }) => { await next!(); });

if (!process.env.DB_CONN_STRING) throw new Error('Env DB_CONN_STRING is required.');
const client = new mongoDB.MongoClient(process.env.DB_CONN_STRING);
await client.connect();

const msgCollection = client.db().collection('messages');
const changedMsgCollection = client.db().collection('changedMessages');
const deletedMsgCollection = client.db().collection('deletedMessages');
const driveJobCollection = client.db().collection('driveComplianceJobs');

const fileSavePrefix = process.env.SLACK_FILE_SAVE_PREFIX;
if (!fileSavePrefix) throw new Error('Env SLACK_FILE_SAVE_PREFIX is required.');

const slackUserToken = process.env.SLACK_USER_TOKEN
if (!slackUserToken) throw new Error('Env SLACK_USER_TOKEN is required.');

const useReactionCheck = process.env.USE_REACTION_CHECK === 'true';

// Start your app
await app.start();

const autoJoinChannels = process.env.AUTO_JOIN_CHANNELS === 'true';

const slackStorageModule = new SlackStorageModule(
app,
msgCollection,
changedMsgCollection,
deletedMsgCollection,
fileSavePrefix,
slackUserToken,
{
autoJoinChannels,
}
collections.messages,
collections.changedMessages,
collections.deletedMessages,
config.app.fileSavePrefix,
config.slack.userToken,
{ autoJoinChannels: config.app.autoJoinChannels },
);
await slackStorageModule.init();

const appHomeModule = new AppHomeModule(app);
await appHomeModule.init();

if (useReactionCheck) {
let ignoredUsers: string[] = [];
if (process.env.REACTION_USER_IGN_LIST) {
ignoredUsers = process.env.REACTION_USER_IGN_LIST.split(',');
}
const settings = settingsStore.get();

const reactionCheckModule = new ReactionCheckModule(app, ignoredUsers);
if (settings.useReactionCheck) {
const reactionCheckModule = new ReactionCheckModule(app, settings.reactionUserIgnoreList);
await reactionCheckModule.init();
}

const useGoogleDriveCheck = process.env.USE_GOOGLE_DRIVE_CHECK === 'true';
if (useGoogleDriveCheck) {
if (!process.env.GOOGLE_SERVICE_ACCOUNT_KEY_FILE && !process.env.GOOGLE_SERVICE_ACCOUNT_KEY_JSON) {
throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY_FILE or GOOGLE_SERVICE_ACCOUNT_KEY_JSON is required when USE_GOOGLE_DRIVE_CHECK=true.');
}

if (settings.useGoogleDriveCheck) {
const auth = new google.auth.GoogleAuth({
keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_KEY_FILE,
credentials: process.env.GOOGLE_SERVICE_ACCOUNT_KEY_JSON
? JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_KEY_JSON)
keyFile: config.features.googleServiceAccountKeyFile,
credentials: config.features.googleServiceAccountKeyJson
? JSON.parse(config.features.googleServiceAccountKeyJson)
: undefined,
scopes: ['https://www.googleapis.com/auth/drive.readonly'],
});
Expand All @@ -92,28 +73,31 @@ app.use(async ({ next }) => {
const gdCheckModule = new GoogleDriveCheckModule(
app,
driveClient,
driveJobCollection,
collections.driveComplianceJobs,
{
allowedSharedDriveIds: (process.env.GOOGLE_DRIVE_ALLOWED_SHARED_DRIVE_IDS || '').split(',').filter(Boolean),
allowedFolderIds: (process.env.GOOGLE_DRIVE_ALLOWED_FOLDER_IDS || '').split(',').filter(Boolean),
allowDomainSharing: process.env.GOOGLE_DRIVE_ALLOW_DOMAIN_SHARING === 'true',
maxParentTraversalDepth: 10,
allowedSharedDriveIds: config.features.googleDriveAllowedSharedDriveIds,
allowedFolderIds: config.features.googleDriveAllowedFolderIds,
allowDomainSharing: config.features.googleDriveAllowDomainSharing,
maxParentTraversalDepth: config.features.googleDriveMaxParentTraversalDepth,
},
process.env.GOOGLE_DRIVE_REPORT_ONLY_VIOLATIONS !== 'false',
process.env.GOOGLE_DRIVE_AUDIT_CHANNEL || undefined,
config.features.googleDriveReportOnlyViolations,
config.features.googleDriveAuditChannel,
);
await gdCheckModule.init();
}

const httpServer = createWebServer(config, collections, settingsStore);

console.log('⚡️ Bolt app is running!');

const shutdown = async () => {
console.log('Shutting down...');
httpServer.close();
await app.stop();
await client.close();
await dbClient.close();
process.exit(0);
};

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
})();
})();
92 changes: 92 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { config } from 'dotenv';
config();

import { getBoltLogLevel } from './utils/slack';

export interface Config {
slack: {
botToken: string;
signingSecret: string;
appToken: string;
userToken: string;
logLevel: ReturnType<typeof getBoltLogLevel>;
};
db: {
connString: string;
};
app: {
fileSavePrefix: string;
autoJoinChannels: boolean;
};
features: {
useReactionCheck: boolean;
reactionUserIgnoreList: string[];
useGoogleDriveCheck: boolean;
googleDriveAllowedSharedDriveIds: string[];
googleDriveAllowedFolderIds: string[];
googleDriveAllowDomainSharing: boolean;
googleDriveMaxParentTraversalDepth: number;
googleDriveReportOnlyViolations: boolean;
googleDriveAuditChannel?: string;
googleServiceAccountKeyFile?: string;
googleServiceAccountKeyJson?: string;
};
api: {
port: number;
apiKey: string;
};
}

function required(name: string): string {
const val = process.env[name];
if (!val) throw new Error(`Env ${name} is required.`);
return val;
}

function optional(name: string): string | undefined {
return process.env[name] || undefined;
}

export function loadConfig(): Config {
const useGoogleDriveCheck = process.env.USE_GOOGLE_DRIVE_CHECK === 'true';

if (useGoogleDriveCheck && !process.env.GOOGLE_SERVICE_ACCOUNT_KEY_FILE && !process.env.GOOGLE_SERVICE_ACCOUNT_KEY_JSON) {
throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY_FILE or GOOGLE_SERVICE_ACCOUNT_KEY_JSON is required when USE_GOOGLE_DRIVE_CHECK=true.');
}

return {
slack: {
botToken: required('SLACK_BOT_TOKEN'),
signingSecret: required('SLACK_SIGNING_SECRET'),
appToken: required('SLACK_APP_TOKEN'),
userToken: required('SLACK_USER_TOKEN'),
logLevel: getBoltLogLevel(process.env.LOG_LEVEL),
},
db: {
connString: required('DB_CONN_STRING'),
},
app: {
fileSavePrefix: required('SLACK_FILE_SAVE_PREFIX'),
autoJoinChannels: process.env.AUTO_JOIN_CHANNELS !== 'false',
},
features: {
useReactionCheck: process.env.USE_REACTION_CHECK === 'true',
reactionUserIgnoreList: process.env.REACTION_USER_IGN_LIST
? process.env.REACTION_USER_IGN_LIST.split(',')
: [],
useGoogleDriveCheck,
googleDriveAllowedSharedDriveIds: (process.env.GOOGLE_DRIVE_ALLOWED_SHARED_DRIVE_IDS || '').split(',').filter(Boolean),
googleDriveAllowedFolderIds: (process.env.GOOGLE_DRIVE_ALLOWED_FOLDER_IDS || '').split(',').filter(Boolean),
googleDriveAllowDomainSharing: process.env.GOOGLE_DRIVE_ALLOW_DOMAIN_SHARING === 'true',
googleDriveMaxParentTraversalDepth: 10,
googleDriveReportOnlyViolations: process.env.GOOGLE_DRIVE_REPORT_ONLY_VIOLATIONS !== 'false',
googleDriveAuditChannel: optional('GOOGLE_DRIVE_AUDIT_CHANNEL'),
googleServiceAccountKeyFile: optional('GOOGLE_SERVICE_ACCOUNT_KEY_FILE'),
googleServiceAccountKeyJson: optional('GOOGLE_SERVICE_ACCOUNT_KEY_JSON'),
},
api: {
port: parseInt(process.env.API_PORT || '3000', 10),
apiKey: required('API_KEY'),
},
};
}
25 changes: 25 additions & 0 deletions src/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as mongoDB from 'mongodb';

export interface DbCollections {
messages: mongoDB.Collection;
changedMessages: mongoDB.Collection;
deletedMessages: mongoDB.Collection;
driveComplianceJobs: mongoDB.Collection;
settings: mongoDB.Collection;
}

export async function connectDb(connString: string): Promise<{ client: mongoDB.MongoClient; collections: DbCollections }> {
const client = new mongoDB.MongoClient(connString);
await client.connect();
const db = client.db();
return {
client,
collections: {
messages: db.collection('messages'),
changedMessages: db.collection('changedMessages'),
deletedMessages: db.collection('deletedMessages'),
driveComplianceJobs: db.collection('driveComplianceJobs'),
settings: db.collection('settings'),
},
};
}
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { App } from '@slack/bolt';
import { KnownBlock } from '@slack/types';
import { drive_v3 } from 'googleapis';
import { Collection } from 'mongodb';
import { checkDriveCompliance, ComplianceCheckerOptions, ComplianceResult } from '../google-drive-checker';
import { checkDriveCompliance, ComplianceCheckerOptions, ComplianceResult } from './checker';

const DRIVE_LINK_PATTERNS = [
/https?:\/\/drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)/g,
Expand Down
File renamed without changes.
File renamed without changes.
50 changes: 50 additions & 0 deletions src/modules/settings/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Collection } from 'mongodb';

export interface BotSettings {
useReactionCheck: boolean;
useGoogleDriveCheck: boolean;
autoJoinChannels: boolean;
reactionUserIgnoreList: string[];
googleDriveReportOnlyViolations: boolean;
googleDriveAuditChannel?: string;
googleDriveAllowedSharedDriveIds: string[];
googleDriveAllowedFolderIds: string[];
updatedAt: Date;
}

export type PatchSettingsBody = Partial<Omit<BotSettings, 'updatedAt'>>;

const SETTINGS_DOC_ID = 'main';

export class SettingsStore {
private cache?: BotSettings;

constructor(private col: Collection) {}

async load(defaults: Omit<BotSettings, 'updatedAt'>): Promise<BotSettings> {
const existing = await this.col.findOne<BotSettings>({ _id: SETTINGS_DOC_ID as any });
if (existing) {
this.cache = existing;
} else {
const initial: BotSettings = { ...defaults, updatedAt: new Date() };
await this.col.insertOne({ _id: SETTINGS_DOC_ID as any, ...initial });
this.cache = initial;
}
return this.cache;
}

get(): BotSettings {
if (!this.cache) throw new Error('SettingsStore not loaded. Call load() first.');
return this.cache;
}

async patch(update: PatchSettingsBody): Promise<BotSettings> {
const now = new Date();
await this.col.updateOne(
{ _id: SETTINGS_DOC_ID as any },
{ $set: { ...update, updatedAt: now } },
);
this.cache = { ...this.get(), ...update, updatedAt: now };
return this.cache;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { App, ignoreSelf, KnownEventFromType, subtype } from "@slack/bolt";
import { mkdir } from "fs/promises";
import { Collection } from "mongodb";
import { downloadFileFromSlack } from "../utils/slack";
import { downloadFileFromSlack } from "../../utils/slack";

export type SlackStorageModuleOptions = {
autoJoinChannels: boolean;
Expand Down
Loading