<commit_before>import { AddWorkspaceUserOptions, Logger, RolloutStrategy, WorkspaceRollout } from 'botpress/sdk'
import { CHAT_USER_ROLE, defaultPipelines, defaultWorkspace } from 'common/defaults'
import { AuthRole, CreateWorkspace, Pipeline, Workspace, WorkspaceUser } from 'common/typings'
import { WorkspaceInviteCode, WorkspaceInviteCodesRepository } from 'core/repositories'
import { StrategyUsersRepository } from 'core/repositories/strategy_users'
import { WorkspaceUserAttributes, WorkspaceUsersRepository } from 'core/repositories/workspace_users'
import { BadRequestError, ConflictError, NotFoundError } from 'core/routers/errors'
import { inject, injectable, tagged } from 'inversify'
import _ from 'lodash'
import nanoid from 'nanoid/generate'

import { TYPES } from '../types'

import { InvalidOperationError } from './auth/errors'
import { GhostService } from './ghost/service'

export const ROLLOUT_STRATEGIES: RolloutStrategy[] = [
  'anonymous',
  'anonymous-invite',
  'authenticated',
  'authenticated-invite',
  'authorized'
]

const UNLIMITED = -1

@injectable()
export class WorkspaceService {
  constructor(
    @inject(TYPES.Logger)
    @tagged('name', 'WorkspaceService')
    private logger: Logger,
    @inject(TYPES.GhostService) private ghost: GhostService,
    @inject(TYPES.WorkspaceUsersRepository) private workspaceRepo: WorkspaceUsersRepository,
    @inject(TYPES.StrategyUsersRepository) private usersRepo: StrategyUsersRepository,
    @inject(TYPES.WorkspaceInviteCodesRepository) private inviteCodesRepo: WorkspaceInviteCodesRepository
  ) {}

  async initialize(): Promise<void> {
    await this.getWorkspaces().catch(async () => {
      await this.save([defaultWorkspace])
      this.logger.info('Created workspace')
    })
  }

  async getWorkspaces(): Promise<Workspace[]> {
    const workspaces = await this.ghost.global().readFileAsObject<Workspace[]>('/', `workspaces.json`)
    if (!workspaces || !workspaces.length) {
      throw new Error('No workspace found in workspaces.json')
    }

    return workspaces
  }

  async save(workspaces: Workspace[]): Promise<void> {
    return this.ghost.global().upsertFile('/', `workspaces.json`, JSON.stringify(workspaces, undefined, 2))
  }

  async mergeWorkspaceConfig(workspaceId: string, partialData: Partial<Workspace>) {
    const workspaces = await this.getWorkspaces()
    if (!workspaces.find(x => x.id === workspaceId)) {
      throw new NotFoundError(`Workspace doesn't exist`)
    }

    return this.save(workspaces.map(wks => (wks.id === workspaceId ? { ...wks, ...partialData } : wks)))
  }

  async addBotRef(botId: string, workspaceId: string): Promise<void> {
    const workspaces = await this.getWorkspaces()
    const workspace = workspaces.find(x => x.id === workspaceId)

    if (!workspace) {
      throw new Error(`Specified workspace "${workspaceId}" doesn't exists`)
    }

    workspace.bots.push(botId)
    return this.save(workspaces)
  }

  async deleteBotRef(botId: any): Promise<void> {
    const workspaces = await this.getWorkspaces()

    const botWorkspaceId = await this.getBotWorkspaceId(botId)
    const workspace = workspaces.find(x => x.id === botWorkspaceId)

    if (!workspace) {
      throw new Error(`Specified workspace "${botWorkspaceId}" doesn't exists`)
    }

    const index = workspace.bots.findIndex(x => x === botId)
    workspace.bots.splice(index, 1)

    return this.save(workspaces)
  }

  async getBotRefs(workspaceId?: string): Promise<string[]> {
    if (!workspaceId) {
      const workspace = await this.getWorkspaces()
      return _.flatten(workspace.map(x => x.bots))
    } else {
      const workspace = await this.findWorkspace(workspaceId)
      return (workspace && workspace.bots) || []
    }
  }

  async findWorkspace(workspaceId: string): Promise<Workspace> {
    const workspaces = await this.getWorkspaces()

    const workspace = workspaces.find(x => x.id === workspaceId)
    if (!workspace) {
      throw new NotFoundError(`Unknown workspace`)
    }

    return workspace
  }

  async findWorkspaceName(workspaceId: string): Promise<string> {
    const all = await this.getWorkspaces()
    const workspace = all.find(x => x.id === workspaceId)
    return (workspace && workspace.name) || workspaceId
  }

  async createWorkspace(workspace: CreateWorkspace): Promise<void> {
    const workspaces = await this.getWorkspaces()
    if (workspaces.find(x => x.id === workspace.id)) {
      throw new ConflictError(`A workspace with that id "${workspace.id}" already exists`)
    }

    if (!defaultPipelines[workspace.pipelineId]) {
      throw new InvalidOperationError(`Invalid pipeline`)
    }

    const newWorkspace = {
      ...defaultWorkspace,
      ..._.pick(workspace, ['id', 'name', 'description', 'audience']),
      pipeline: defaultPipelines[workspace.pipelineId]
    }

    workspaces.push(newWorkspace)
    return this.save(workspaces)
  }

  async deleteWorkspace(workspaceId: string): Promise<void> {
    const workspaces = await this.getWorkspaces()
    if (!workspaces.find(x => x.id === workspaceId)) {
      throw new NotFoundError(`Workspace doesn't exist`)
    }

    return this.save(workspaces.filter(x => x.id !== workspaceId))
  }

  async addUserToWorkspace(email: string, strategy: string, workspaceId: string, options?: AddWorkspaceUserOptions) {
    if (!(await this.usersRepo.findUser(email, strategy))) {
      throw new Error(`Specified user doesn't exist`)
    }

    const workspace = await this.findWorkspace(workspaceId)
    let role = workspace.defaultRole

    if (options) {
      if (options.role) {
        role = options.role
      } else if (options.asAdmin) {
        role = workspace.adminRole
      } else if (options.asChatUser) {
        role = CHAT_USER_ROLE.id
      }
    }

    await this.workspaceRepo.createEntry({ email, strategy, workspace: workspaceId, role })
  }

  async getWorkspaceRollout(workspaceId: string): Promise<WorkspaceRollout> {
    const { rolloutStrategy } = await this.findWorkspace(workspaceId)

    if (rolloutStrategy === 'anonymous-invite' || rolloutStrategy === 'authenticated-invite') {
      let invite = await this.inviteCodesRepo.getWorkspaceCode(workspaceId)

      // Invite code can be empty if workspaces was modified manually
      if (!invite) {
        invite = await this.resetInviteCode(workspaceId)
      }

      return { rolloutStrategy, ...invite }
    }

    return { rolloutStrategy }
  }

  async resetInviteCode(workspaceId: string, allowedUsages: number = UNLIMITED): Promise<WorkspaceInviteCode> {
    const inviteCode = `INV-${nanoid('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', 10)}`
    const newEntry = { workspaceId, inviteCode, allowedUsages }

    if (await this.inviteCodesRepo.getWorkspaceCode(workspaceId)) {
      await this.inviteCodesRepo.replaceCode(newEntry)
    } else {
      await this.inviteCodesRepo.createEntry(newEntry)
    }<commit_msg>set default rollout strategy<commit_after>import { AddWorkspaceUserOptions, Logger, RolloutStrategy, WorkspaceRollout } from 'botpress/sdk'
import { CHAT_USER_ROLE, defaultPipelines, defaultWorkspace } from 'common/defaults'
import { AuthRole, CreateWorkspace, Pipeline, Workspace, WorkspaceUser } from 'common/typings'
import { WorkspaceInviteCode, WorkspaceInviteCodesRepository } from 'core/repositories'
import { StrategyUsersRepository } from 'core/repositories/strategy_users'
import { WorkspaceUserAttributes, WorkspaceUsersRepository } from 'core/repositories/workspace_users'
import { BadRequestError, ConflictError, NotFoundError } from 'core/routers/errors'
import { inject, injectable, tagged } from 'inversify'
import _ from 'lodash'
import nanoid from 'nanoid/generate'

import { TYPES } from '../types'

import { InvalidOperationError } from './auth/errors'
import { GhostService } from './ghost/service'

export const ROLLOUT_STRATEGIES: RolloutStrategy[] = [
  'anonymous',
  'anonymous-invite',
  'authenticated',
  'authenticated-invite',
  'authorized'
]

const UNLIMITED = -1

@injectable()
export class WorkspaceService {
  constructor(
    @inject(TYPES.Logger)
    @tagged('name', 'WorkspaceService')
    private logger: Logger,
    @inject(TYPES.GhostService) private ghost: GhostService,
    @inject(TYPES.WorkspaceUsersRepository) private workspaceRepo: WorkspaceUsersRepository,
    @inject(TYPES.StrategyUsersRepository) private usersRepo: StrategyUsersRepository,
    @inject(TYPES.WorkspaceInviteCodesRepository) private inviteCodesRepo: WorkspaceInviteCodesRepository
  ) {}

  async initialize(): Promise<void> {
    await this.getWorkspaces().catch(async () => {
      await this.save([defaultWorkspace])
      this.logger.info('Created workspace')
    })
  }

  async getWorkspaces(): Promise<Workspace[]> {
    const workspaces = await this.ghost.global().readFileAsObject<Workspace[]>('/', `workspaces.json`)
    if (!workspaces || !workspaces.length) {
      throw new Error('No workspace found in workspaces.json')
    }

    return workspaces
  }

  async save(workspaces: Workspace[]): Promise<void> {
    return this.ghost.global().upsertFile('/', `workspaces.json`, JSON.stringify(workspaces, undefined, 2))
  }

  async mergeWorkspaceConfig(workspaceId: string, partialData: Partial<Workspace>) {
    const workspaces = await this.getWorkspaces()
    if (!workspaces.find(x => x.id === workspaceId)) {
      throw new NotFoundError(`Workspace doesn't exist`)
    }

    return this.save(workspaces.map(wks => (wks.id === workspaceId ? { ...wks, ...partialData } : wks)))
  }

  async addBotRef(botId: string, workspaceId: string): Promise<void> {
    const workspaces = await this.getWorkspaces()
    const workspace = workspaces.find(x => x.id === workspaceId)

    if (!workspace) {
      throw new Error(`Specified workspace "${workspaceId}" doesn't exists`)
    }

    workspace.bots.push(botId)
    return this.save(workspaces)
  }

  async deleteBotRef(botId: any): Promise<void> {
    const workspaces = await this.getWorkspaces()

    const botWorkspaceId = await this.getBotWorkspaceId(botId)
    const workspace = workspaces.find(x => x.id === botWorkspaceId)

    if (!workspace) {
      throw new Error(`Specified workspace "${botWorkspaceId}" doesn't exists`)
    }

    const index = workspace.bots.findIndex(x => x === botId)
    workspace.bots.splice(index, 1)

    return this.save(workspaces)
  }

  async getBotRefs(workspaceId?: string): Promise<string[]> {
    if (!workspaceId) {
      const workspace = await this.getWorkspaces()
      return _.flatten(workspace.map(x => x.bots))
    } else {
      const workspace = await this.findWorkspace(workspaceId)
      return (workspace && workspace.bots) || []
    }
  }

  async findWorkspace(workspaceId: string): Promise<Workspace> {
    const workspaces = await this.getWorkspaces()

    const workspace = workspaces.find(x => x.id === workspaceId)
    if (!workspace) {
      throw new NotFoundError(`Unknown workspace`)
    }

    return workspace
  }

  async findWorkspaceName(workspaceId: string): Promise<string> {
    const all = await this.getWorkspaces()
    const workspace = all.find(x => x.id === workspaceId)
    return (workspace && workspace.name) || workspaceId
  }

  async createWorkspace(workspace: CreateWorkspace): Promise<void> {
    const workspaces = await this.getWorkspaces()
    if (workspaces.find(x => x.id === workspace.id)) {
      throw new ConflictError(`A workspace with that id "${workspace.id}" already exists`)
    }

    if (!defaultPipelines[workspace.pipelineId]) {
      throw new InvalidOperationError(`Invalid pipeline`)
    }

    const newWorkspace = {
      ...defaultWorkspace,
      ..._.pick(workspace, ['id', 'name', 'description', 'audience']),
      pipeline: defaultPipelines[workspace.pipelineId]
    }

    workspaces.push(newWorkspace)
    return this.save(workspaces)
  }

  async deleteWorkspace(workspaceId: string): Promise<void> {
    const workspaces = await this.getWorkspaces()
    if (!workspaces.find(x => x.id === workspaceId)) {
      throw new NotFoundError(`Workspace doesn't exist`)
    }

    return this.save(workspaces.filter(x => x.id !== workspaceId))
  }

  async addUserToWorkspace(email: string, strategy: string, workspaceId: string, options?: AddWorkspaceUserOptions) {
    if (!(await this.usersRepo.findUser(email, strategy))) {
      throw new Error(`Specified user doesn't exist`)
    }

    const workspace = await this.findWorkspace(workspaceId)
    let role = workspace.defaultRole

    if (options) {
      if (options.role) {
        role = options.role
      } else if (options.asAdmin) {
        role = workspace.adminRole
      } else if (options.asChatUser) {
        role = CHAT_USER_ROLE.id
      }
    }

    await this.workspaceRepo.createEntry({ email, strategy, workspace: workspaceId, role })
  }

  async getWorkspaceRollout(workspaceId: string): Promise<WorkspaceRollout> {
    const { rolloutStrategy } = await this.findWorkspace(workspaceId)

    if (!rolloutStrategy) {
      return { rolloutStrategy: 'anonymous' }
    }

    if (rolloutStrategy === 'anonymous-invite' || rolloutStrategy === 'authenticated-invite') {
      let invite = await this.inviteCodesRepo.getWorkspaceCode(workspaceId)

      // Invite code can be empty if workspaces was modified manually
      if (!invite) {
        invite = await this.resetInviteCode(workspaceId)
      }

      return { rolloutStrategy, ...invite }
    }

    return { rolloutStrategy }
  }

  async resetInviteCode(workspaceId: string, allowedUsages: number = UNLIMITED): Promise<WorkspaceInviteCode> {
    const inviteCode = `INV-${nanoid('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', 10)}`
    const newEntry = { workspaceId, inviteCode, allowedUsages }

    if (await this.inviteCodesRepo.getWorkspaceCode(workspaceId)) {
      await this.inviteCodesRepo.replaceCode(newEntry)
    } else {
      await this.inviteCodesRepo.createEntry(newEntry)
    }
{'fraction_non_alphanumeric': 0.1188118811881188, 'fraction_numerical': 0.002152389151958674, 'mean_word_length': 4.017638588912887, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 55, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '120392', 'n_tokens_mistral': 4233, 'n_tokens_neox': 4164, 'n_words': 932}