Skip to content

Commit

Permalink
Merge pull request #102 from funidata/nest
Browse files Browse the repository at this point in the history
Sync users between slack and app database
  • Loading branch information
joonashak committed Aug 7, 2023
2 parents 5627168 + 021c2a1 commit 0f5b7fb
Show file tree
Hide file tree
Showing 16 changed files with 320 additions and 26 deletions.
48 changes: 48 additions & 0 deletions app-nest/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
display_information:
name: Hybridilusmu
description: Hybridityöskentelyn tukisovellus
features:
app_home:
home_tab_enabled: true
messages_tab_enabled: true
messages_tab_read_only_enabled: false
bot_user:
display_name: hybridilusmu
always_online: true
oauth_config:
scopes:
bot:
- channels:history
- channels:join
- chat:write
- groups:history
- groups:read
- im:history
- im:read
- mpim:history
- channels:read
- reactions:read
- usergroups:read
- commands
- users:read
settings:
event_subscriptions:
bot_events:
- member_joined_channel
- member_left_channel
- message.channels
- message.groups
- message.im
- message.mpim
- reaction_added
- app_home_opened
- subteam_created
- subteam_members_changed
- subteam_updated
- channel_left
- user_profile_changed
interactivity:
is_enabled: true
org_deploy_enabled: false
socket_mode_enabled: true
token_rotation_enabled: false
4 changes: 4 additions & 0 deletions app-nest/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { TypeOrmModule } from "@nestjs/typeorm";
import { BoltModule } from "./bolt/bolt.module";
import configuration from "./config/configuration";
import { inDevelopmentEnvironment } from "./config/utils";
import { DevToolsModule } from "./dev-tools/dev-tools.module";
import { EntitiesModule } from "./entities/entities.module";
import { GuiModule } from "./gui/gui.module";
import { SyncModule } from "./sync/sync.module";

@Module({
imports: [
Expand All @@ -25,6 +27,8 @@ import { GuiModule } from "./gui/gui.module";
}),
GuiModule,
EntitiesModule,
SyncModule,
DevToolsModule,
],
})
export class AppModule {}
68 changes: 52 additions & 16 deletions app-nest/src/bolt/bolt-register.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { DiscoveryService } from "@golevelup/nestjs-discovery";
import {
DiscoveredMethod,
DiscoveryService,
} from "@golevelup/nestjs-discovery";
import { Injectable } from "@nestjs/common";
import { ModuleRef } from "@nestjs/core";
import { App, Middleware, SlackEventMiddlewareArgs } from "@slack/bolt";
import { App } from "@slack/bolt";
import { StringIndexed } from "@slack/bolt/dist/types/helpers";
import { BoltService } from "./bolt.service";
import { BOLT_ACTION_KEY } from "./decorators/bolt-action.decorator";
import { BOLT_EVENT_KEY } from "./decorators/bolt-event.decorator";

type EventType = typeof BOLT_ACTION_KEY | typeof BOLT_EVENT_KEY;

@Injectable()
export class BoltRegisterService {
private bolt: App<StringIndexed>;
Expand All @@ -18,27 +24,57 @@ export class BoltRegisterService {
this.bolt = boltService.getBolt();
}

async registerEvents() {
// TODO: This needs a type parameter.
/**
* Register all controller methods decorated with one of our Bolt event decorators.
*/
async registerAllHandlers() {
const eventTypes = [BOLT_ACTION_KEY, BOLT_EVENT_KEY] as const;

await Promise.all(
eventTypes.map((eventType) => this.registerHandlers(eventType)),
);
}

/**
* Register all handler functions of certain `EventType`.
*/
private async registerHandlers(eventType: EventType) {
const controllers =
await this.discoveryService.controllerMethodsWithMetaAtKey(
BOLT_EVENT_KEY,
);
await this.discoveryService.controllerMethodsWithMetaAtKey(eventType);

controllers.forEach((controller) => {
const { meta } = controller;
const cref = this.moduleRef.get(
controller.discoveredMethod.parentClass.injectType,
{ strict: false },
);
this.registerEventHandler(meta.toString(), cref.getView());
const { meta, discoveredMethod } = controller;
const eventName = meta.toString();
const handler = this.getHandler(discoveredMethod);

this.registerHandler(eventType, eventName, handler);
});
}

/**
* Get handler function via `ModuleRef` and bind it to correct context.
*/
private getHandler(discoveredMethod: DiscoveredMethod) {
const cref = this.moduleRef.get(discoveredMethod.parentClass.injectType, {
strict: false,
});
// N.B.: Take care where you call the discovered handler method. This is an easy
// place to bind it into a wrong context. Ask me how I know.
return cref[discoveredMethod.methodName]();
}

private registerEventHandler(
/**
* Register handler function with Bolt API.
*/
private registerHandler(
eventType: EventType,
eventName: string,
listener: Middleware<SlackEventMiddlewareArgs<string>, StringIndexed>,
handler: any,
) {
this.bolt.event(eventName, listener);
if (eventType === BOLT_ACTION_KEY) {
this.bolt.action(eventName, handler);
} else if (eventType === BOLT_EVENT_KEY) {
this.bolt.event(eventName, handler);
}
}
}
11 changes: 11 additions & 0 deletions app-nest/src/bolt/bolt-user.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Injectable } from "@nestjs/common";
import { BoltService } from "./bolt.service";

@Injectable()
export class BoltUserService {
constructor(private boltService: BoltService) {}

async getUsers() {
return this.boltService.getBolt().client.users.list();
}
}
20 changes: 15 additions & 5 deletions app-nest/src/bolt/bolt.module.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { DiscoveryModule } from "@golevelup/nestjs-discovery";
import { Global, Module, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import {
Global,
Module,
OnApplicationBootstrap,
OnModuleDestroy,
OnModuleInit,
} from "@nestjs/common";
import { BoltRegisterService } from "./bolt-register.service";
import { BoltUserService } from "./bolt-user.service";
import { ConfigurableBoltModule } from "./bolt.module-definition";
import { BoltService } from "./bolt.service";

@Global()
@Module({
imports: [DiscoveryModule],
providers: [BoltService, BoltRegisterService],
exports: [BoltService],
providers: [BoltService, BoltRegisterService, BoltUserService],
exports: [BoltService, BoltUserService],
})
export class BoltModule
extends ConfigurableBoltModule
implements OnModuleInit, OnModuleDestroy
implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy
{
constructor(
private boltService: BoltService,
Expand All @@ -23,7 +30,10 @@ export class BoltModule

async onModuleInit() {
await this.boltService.connect();
await this.boltRegisterService.registerEvents();
}

async onApplicationBootstrap() {
await this.boltRegisterService.registerAllHandlers();
}

async onModuleDestroy() {
Expand Down
8 changes: 8 additions & 0 deletions app-nest/src/bolt/decorators/bolt-action.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SetMetadata } from "@nestjs/common";

export const BOLT_ACTION_KEY = "BoltAction";

const BoltAction = (actionName: string) =>
SetMetadata(BOLT_ACTION_KEY, actionName);

export default BoltAction;
16 changes: 16 additions & 0 deletions app-nest/src/dev-tools/dev-tools.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Controller } from "@nestjs/common";
import BoltAction from "../bolt/decorators/bolt-action.decorator";
import { UserSyncService } from "../sync/user-sync.service";

@Controller()
export class DevToolsController {
constructor(private userSyncService: UserSyncService) {}

@BoltAction("sync_users")
syncUsers() {
return async ({ ack }) => {
await ack();
await this.userSyncService.syncUsers();
};
}
}
6 changes: 6 additions & 0 deletions app-nest/src/dev-tools/dev-tools.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Module } from "@nestjs/common";
import { SyncModule } from "../sync/sync.module";
import { DevToolsController } from "./dev-tools.controller";

@Module({ imports: [SyncModule], controllers: [DevToolsController] })
export class DevToolsModule {}
11 changes: 7 additions & 4 deletions app-nest/src/entities/user/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Column, Entity, PrimaryGeneratedColumn, Repository } from "typeorm";
import { Column, Entity, PrimaryColumn, Repository } from "typeorm";

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@PrimaryColumn()
slackId: string;

@Column()
slackId: string;
displayName: string;

@Column()
realName: string;
}

export type UserRepository = Repository<User>;
16 changes: 15 additions & 1 deletion app-nest/src/entities/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { DataSource } from "typeorm";
import { User, UserRepository } from "./user.entity";

@Injectable()
export class UserService {
constructor(@InjectRepository(User) private userRepository: UserRepository) {}
constructor(
@InjectRepository(User) private userRepository: UserRepository,
private dataSource: DataSource,
) {}

async findAll() {
return this.userRepository.find();
}

async upsert(users: User[]) {
return this.dataSource
.createQueryBuilder()
.insert()
.into(User)
.values(users)
.orUpdate(["displayName", "realName"], ["slackId"])
.execute();
}
}
36 changes: 36 additions & 0 deletions app-nest/src/gui/dev/dev-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const devTools = [
{
type: "header",
text: {
type: "plain_text",
text: ":wrench: Developer Tools",
},
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: ":recycle: Sync Users",
},
action_id: "sync_users",
},
],
},
{
type: "context",
elements: [
{
type: "plain_text",
text: "In development environment, users are not synchronized between local database and Slack on app start to avoid running into API rate limits due to hot-reloads. Sync users manually when necessary.",
},
],
},
{
type: "divider",
},
];

export default devTools;
3 changes: 3 additions & 0 deletions app-nest/src/gui/tabs/home-tab.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Controller } from "@nestjs/common";
import BoltEvent from "../../bolt/decorators/bolt-event.decorator";
import { UserService } from "../../entities/user/user.service";
import devTools from "../dev/dev-tools";

@Controller()
export class HomeTabController {
Expand All @@ -11,12 +12,14 @@ export class HomeTabController {
return async ({ event, client, logger }) => {
const users = await this.userService.findAll();
const { slackId } = users[0];

try {
const result = await client.views.publish({
user_id: event.user,
view: {
type: "home",
blocks: [
...devTools,
{
type: "section",
text: {
Expand Down
13 changes: 13 additions & 0 deletions app-nest/src/sync/sync.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Controller } from "@nestjs/common";
import BoltEvent from "../bolt/decorators/bolt-event.decorator";
import { UserSyncService } from "./user-sync.service";

@Controller()
export class SyncController {
constructor(private userSyncService: UserSyncService) {}

@BoltEvent("user_profile_changed")
userProfileChanged() {
return ({ event }) => this.userSyncService.syncUsers(event.user);
}
}
14 changes: 14 additions & 0 deletions app-nest/src/sync/sync.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from "@nestjs/common";
import { UserModule } from "../entities/user/user.module";
import { UserService } from "../entities/user/user.service";
import { SyncController } from "./sync.controller";
import { SyncService } from "./sync.service";
import { UserSyncService } from "./user-sync.service";

@Module({
imports: [UserModule],
providers: [SyncService, UserSyncService, UserService],
controllers: [SyncController],
exports: [SyncService, UserSyncService],
})
export class SyncModule {}
14 changes: 14 additions & 0 deletions app-nest/src/sync/sync.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Injectable, OnApplicationBootstrap } from "@nestjs/common";
import { inDevelopmentEnvironment } from "../config/utils";
import { UserSyncService } from "./user-sync.service";

@Injectable()
export class SyncService implements OnApplicationBootstrap {
constructor(private userSyncService: UserSyncService) {}

async onApplicationBootstrap() {
if (!inDevelopmentEnvironment) {
await this.userSyncService.syncUsers();
}
}
}
Loading

0 comments on commit 0f5b7fb

Please sign in to comment.