Skip to content

Commit

Permalink
Merge pull request #61 from botpress/rl_create_bot_v2
Browse files Browse the repository at this point in the history
feat(core): Add bot creation ui and default template
  • Loading branch information
rndlaine committed Sep 26, 2018
2 parents d366fc8 + c1f3859 commit 20b267f
Show file tree
Hide file tree
Showing 22 changed files with 504 additions and 260 deletions.
26 changes: 23 additions & 3 deletions gulpfile.js
Expand Up @@ -32,10 +32,14 @@ const createDirectories = () => {
.pipe(gulp.dest('./out/bp/data/storage'))
}

const copyStatic = () => {
const copyVanilla = () => {
return gulp.src('./src/bp/vanilla/**/*').pipe(gulp.dest('./out/bp/data'))
}

const copyTempates = () => {
return gulp.src('./src/templates/**/*').pipe(gulp.dest('./out/templates'))
}

const copyAdmin = () => {
return gulp.src('./src/bp/ui-admin/build/**/*').pipe(gulp.dest('./out/bp/ui-admin/public'))
}
Expand All @@ -60,5 +64,21 @@ process.on('uncaughtException', err => {
})

gulp.task('test', gulp.series([buildTs, runTests]))
gulp.task('default', gulp.series([clean, buildTs, buildSchemas, createDirectories, copyStatic, copyAdmin, copyStudio]))
gulp.task('dev', gulp.series([(clean, buildSchemas, createDirectories, copyStatic, copyAdmin, copyStudio, watch)]))
gulp.task(
'default',
gulp.series([clean, buildTs, buildSchemas, createDirectories, copyVanilla, copyAdmin, copyStudio, copyTempates])
)
gulp.task(
'dev',
gulp.series([
clean,
buildTs,
buildSchemas,
createDirectories,
copyVanilla,
copyAdmin,
copyStudio,
copyTempates,
watch
])
)
11 changes: 10 additions & 1 deletion src/bp/core/app.inversify.ts
@@ -1,7 +1,6 @@
import { Container } from 'inversify'
import path from 'path'
import yn from 'yn'
import { Logger } from 'botpress/sdk'

import { BotLoader } from './bot-loader'
import { Botpress } from './botpress'
Expand All @@ -15,6 +14,8 @@ import { ModuleLoader } from './module-loader'
import { RepositoriesContainerModule } from './repositories/repositories.inversify'
import HTTPServer from './server'
import { ServicesContainerModule } from './services/services.inversify'
import { Logger } from 'botpress/sdk'
import { BotConfigFactory, BotConfigWriter } from './config'

const container = new Container({ autoBindInjectable: true })

Expand Down Expand Up @@ -75,6 +76,14 @@ container
.bind<BotLoader>(TYPES.BotLoader)
.to(BotLoader)
.inSingletonScope()
container
.bind<BotConfigFactory>(TYPES.BotConfigFactory)
.to(BotConfigFactory)
.inSingletonScope()
container
.bind<BotConfigWriter>(TYPES.BotConfigWriter)
.to(BotConfigWriter)
.inSingletonScope()

const isPackaged = !!eval('process.pkg')
const isProduction = !yn(process.env.DEBUG) && (isPackaged || process.env.NODE_ENV == 'production')
Expand Down
2 changes: 2 additions & 0 deletions src/bp/core/config/bot.config.ts
Expand Up @@ -7,6 +7,8 @@ export interface DialogConfig {
}

export type BotConfig = {
$schema?: string
id: string
name: string
active: boolean
description?: string
Expand Down
74 changes: 74 additions & 0 deletions src/bp/core/config/config-builder.ts
@@ -0,0 +1,74 @@
import { BotConfig } from './bot.config'

export class BotConfigBuilder {
private schema = '../../bot.config.schema.json'
private active = false
private version = '0.0.1'
private license: string | undefined
private author: string | undefined
private contentTypes: string[] = []
private modules: string[] = []
private outgoingMiddleware: string[] = []
private incomingMiddleware: string[] = []

constructor(private name: string, private id: string, private description: string) {}

withlicense(license: string): this {
this.license = license
return this
}

withAuthor(author: string): this {
this.author = author
return this
}

withContentTypes(...contentTypes: string[]): this {
this.contentTypes = contentTypes
return this
}

withModules(...modules: string[]): this {
this.modules = modules
return this
}

withVersion(version: string): this {
this.version = version
return this
}

withIncomingMiddleware(...incoming: string[]) {
this.incomingMiddleware = incoming
return this
}

withOugoingMiddleware(...outgoing: string[]) {
this.outgoingMiddleware = outgoing
return this
}

enabled(active: boolean): this {
this.active = active
return this
}

build(): BotConfig {
return {
$schema: this.schema,
active: this.active,
id: this.id,
name: this.name,
description: this.description,
version: this.version,
license: this.license,
author: this.author,
imports: {
modules: this.modules,
contentTypes: this.contentTypes,
incomingMiddleware: this.incomingMiddleware,
outgoingMiddleware: this.outgoingMiddleware
}
}
}
}
29 changes: 29 additions & 0 deletions src/bp/core/config/config-factory.ts
@@ -0,0 +1,29 @@
import { injectable } from 'inversify'
import { BotConfig } from './bot.config'
import { BotConfigBuilder } from '.'

export type DefaultArguments = {
id: string
name: string
description: string
}

@injectable()
export class BotConfigFactory {
createDefault(args: DefaultArguments): BotConfig {
const builder = new BotConfigBuilder(args.name, args.id, args.description)

builder.withModules('channel-web')
builder.withContentTypes(
'builtin_text',
'builtin_single-choice',
'builtin_image',
'builtin_carousel',
'builtin_card'
)
builder.withOugoingMiddleware('web.sendMessages')
builder.enabled(true)

return builder.build()
}
}
22 changes: 22 additions & 0 deletions src/bp/core/config/config-writer.ts
@@ -0,0 +1,22 @@
import fse from 'fs-extra'
import path from 'path'
import { VError } from 'verror'
import { BotConfig } from './bot.config'
import { injectable } from 'inversify'

@injectable()
export class BotConfigWriter {
async writeToFile(config: BotConfig) {
const filePath = path.join(process.cwd(), `./data/bots/${config.id}/`)
const templatePath = path.join(process.cwd(), '../templates')
const fileName = 'bot.config.json'

try {
await fse.ensureDir(filePath)
await fse.copySync(templatePath, filePath)
await fse.writeFile(filePath + fileName, JSON.stringify(config, undefined, 2))
} catch (e) {
throw new VError(e, `Error writing file "${filePath}"`)
}
}
}
3 changes: 3 additions & 0 deletions src/bp/core/config/index.ts
@@ -0,0 +1,3 @@
export * from './config-builder'
export * from './config-factory'
export * from './config-writer'
10 changes: 5 additions & 5 deletions src/bp/core/misc/interfaces.ts
Expand Up @@ -61,12 +61,12 @@ export type RequestWithUser = Request & {

export interface Bot {
id: string
name?: string
name: string
team: number
description: string
created_at: string
updated_at: string
version?: string
description?: string
author?: string
license?: string
created_at: string
updated_at: string
team: number
}
10 changes: 8 additions & 2 deletions src/bp/core/routers/admin/teams.ts
Expand Up @@ -7,7 +7,8 @@ import AuthService from '../../services/auth/auth-service'
import { InvalidOperationError } from '../../services/auth/errors'
import TeamsService from '../../services/auth/teams-service'

import { asyncMiddleware, success as sendSuccess, validateBodySchema } from '../util'
import { Bot } from '../../misc/interfaces'
import { asyncMiddleware, success as sendSuccess, error as sendError, validateBodySchema } from '../util'
import { Logger } from 'botpress/sdk'

export class TeamsRouter implements CustomRouter {
Expand Down Expand Up @@ -271,10 +272,15 @@ export class TeamsRouter implements CustomRouter {
'/:teamId/bots', // Add new bot
this.asyncMiddleware(async (req, res) => {
const { teamId } = req.params
const bot = <Bot>req.body
const userId = req.dbUser.id
await svc.assertUserMember(userId, teamId)
await svc.assertUserPermission(userId, teamId, 'admin.team.bots', 'write')
const bot = await svc.addBot(teamId)
try {
await svc.addBot(teamId, bot)
} catch (err) {
return sendError(res, 400, undefined, err.message)
}

return sendSuccess(res, 'Added new bot', {
botId: bot.id,
Expand Down
8 changes: 1 addition & 7 deletions src/bp/core/routers/util.ts
Expand Up @@ -38,13 +38,7 @@ export const success = (res: Response, message: string = 'Success', payload = {}
})
}

export const error = (
res: Response,
status = 400,
code: string | null,
message: string | null,
docs: string | null
) => {
export const error = (res: Response, status = 400, code?: string, message?: string, docs?: string) => {
res.status(status).json({
status: 'error',
type: 'Error',
Expand Down
41 changes: 24 additions & 17 deletions src/bp/core/services/auth/teams-service.ts
Expand Up @@ -4,28 +4,40 @@ import nanoid from 'nanoid'
import Knex from 'knex'

import Database from '../../database'

import { AuthRole, AuthRoleDb, AuthRule, AuthTeam, AuthTeamMembership, AuthUser, Bot } from '../../misc/interfaces'
import { TYPES } from '../../types'
import { InvalidOperationError, NotFoundError, UnauthorizedAccessError } from '../auth/errors'

import defaultRoles from './default-roles'
import { TYPES } from '../../types'
import { Logger } from 'botpress/sdk'
import { checkRule } from 'core/misc/auth'
import { checkRule } from '../../misc/auth'
import { BotConfigFactory, BotConfigWriter } from '../../config'
import Joi from 'joi'

const TEAMS_TABLE = 'auth_teams'
const MEMBERS_TABLE = 'auth_team_members'
const ROLES_TABLE = 'auth_roles'
const USERS_TABLE = 'auth_users'
const BOTS_TABLE = 'srv_bots'

const BotValidationSchema = Joi.object().keys({
id: Joi.string()
.regex(/^[a-zA-Z0-9-_]+$/)
.required(),
name: Joi.string().required(),
description: Joi.string().required(),
team: Joi.number().required()
})

@injectable()
export default class TeamsService {
constructor(
@inject(TYPES.Logger)
@tagged('name', 'Auth Teams')
private logger: Logger,
@inject(TYPES.Database) private db: Database
@inject(TYPES.Database) private db: Database,
@inject(TYPES.BotConfigFactory) private botConfigFactory: BotConfigFactory,
@inject(TYPES.BotConfigWriter) private botConfigWriter: BotConfigWriter
) {}

get knex() {
Expand Down Expand Up @@ -280,21 +292,16 @@ export default class TeamsService {
.then()
}

async addBot(teamId: number) {
const id = nanoid(8)
const bot: Partial<Bot> = {
id,
team: teamId,
name: `Bot ${id}`
async addBot(teamId: number, bot: Bot): Promise<void> {
bot.team = teamId
const { error } = Joi.validate(bot, BotValidationSchema)
if (error) {
throw new Error(`An error occurred while creating the bot: ${error.message}`)
}

await this.knex(BOTS_TABLE)
.insert(bot)
.then()

// TODO: we also want to create the bot skeleton files now

return bot
await this.knex(BOTS_TABLE).insert(bot)
const botConfig = this.botConfigFactory.createDefault({ id: bot.id, name: bot.name, description: bot.description })
await this.botConfigWriter.writeToFile(botConfig)
}

async getBotTeam(botId: string) {
Expand Down
1 change: 1 addition & 0 deletions src/bp/core/services/services.inversify.ts
Expand Up @@ -28,6 +28,7 @@ import { NotificationsService } from './notification/service'
import { Queue } from './queue'
import MemoryQueue from './queue/memory-queue'
import RealtimeService from './realtime'
import { BotConfigFactory, BotConfigWriter } from '../config'

export const ServicesContainerModule = new ContainerModule((bind: interfaces.Bind) => {
bind<CacheInvalidators.FileChangedInvalidator>(TYPES.FileCacheInvalidator)
Expand Down
4 changes: 3 additions & 1 deletion src/bp/core/types.ts
Expand Up @@ -51,7 +51,9 @@ const TYPES = {
LogJanitorRunner: Symbol.for('LogJanitorRunner'),
NotificationsRepository: Symbol.for('NotificationsRepository'),
NotificationsService: Symbol.for('NotificationsService'),
FileCacheInvalidator: Symbol.for('FileCacheInvalidator')
FileCacheInvalidator: Symbol.for('FileCacheInvalidator'),
BotConfigFactory: Symbol.for('BotConfigFactory'),
BotConfigWriter: Symbol.for('BotConfigWriter')
}

export { TYPES }
27 changes: 1 addition & 26 deletions src/bp/ui-admin/.gitignore
@@ -1,26 +1 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
**/node_modules

# testing
**/coverage

# production
**/build

# misc
**/.DS_Store
**/.env.local
**/.env.development.local
**/.env.test.local
**/.env.production.local

**/npm-debug.log*
**/yarn-debug.log*
**/yarn-error.log*

back/dist
back/static

front/**/*.css
/build
Empty file modified src/bp/ui-admin/public/favicon.ico 100644 → 100755
Empty file.

0 comments on commit 20b267f

Please sign in to comment.