// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VTreeView.ts should emit opened node when using open-on-click and load-children 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
      >
        $subgroup
      </button>

          Root

`;

exports[`VTreeView.ts should emit opened node when using open-on-click and load-children 2`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--loading"
      >
        $loading
      </button>

          Root

`;

exports[`VTreeView.ts should filter items 1`] = `

          one

          two

`;

exports[`VTreeView.ts should filter items 2`] = `

          two

`;

exports[`VTreeView.ts should filter items using custom item filter 1`] = `

`;

exports[`VTreeView.ts should filter items using custom item filter 2`] = `

          one

`;

exports[`VTreeView.ts should handle initial active/open/selected values when using return-object prop 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light accent--text"
      >
        $checkboxOn
      </button>

          One

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
      >
        $subgroup
      </button>
      <button type="button"
              class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light"
      >
        $checkboxOff
      </button>

          Two

          <button type="button"
                  class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light"
          >
            $checkboxOff
          </button>

              Three

`;

exports[`VTreeView.ts should handle replacing items with new array of equal length 1`] = `

          one

          two

`;

exports[`VTreeView.ts should handle replacing items with new array of equal length 2`] = `

          one

          three

`;

exports[`VTreeView.ts should load children when expanding 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
      >
        $subgroup
      </button>

          Root

`;

exports[`VTreeView.ts should load children when expanding 2`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--loading"
      >
        $loading
      </button>

          Root

`;

exports[`VTreeView.ts should load children when selecting, but not render 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
      >
        $subgroup
      </button>
      <button type="button"
              class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light"
      >
        $checkboxOff
      </button>

          Root

`;

exports[`VTreeView.ts should load children when selecting, but not render 2`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
      >
        $subgroup
      </button>
      <button type="button"
              class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light accent--text"
      >
        $checkboxOn
      </button>

          Root

`;

exports[`VTreeView.ts should not show expand icon when children is empty 1`] = `

`;

exports[`VTreeView.ts should open all children when using open-all prop 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
      >
        $subgroup
      </button>

          Root

          <button type="button"
                  class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
          >
            $subgroup
          </button>

              Child

                  Grandchild

              Child

`;

exports[`VTreeView.ts should react to open changes 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
      >
        $subgroup
      </button>

          Root

          <button type="button"
                  class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
          >
            $subgroup
          </button>

              Child

                  Grandchild

              Child

`;

exports[`VTreeView.ts should react to open changes 2`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
      >
        $subgroup
      </button>

          Root

          <button type="button"
                  class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
          >
            $subgroup
          </button>

              Child

              Child

`;

exports[`VTreeView.ts should react to open changes 3`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
      >
        $subgroup
      </button>

          Root

          <button type="button"
                  class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
          >
            $subgroup
          </button>

              Child

                  Grandchild

              Child

`;

exports[`VTreeView.ts should react to open changes 4`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
      >
        $subgroup
      </button>

          Root

`;

exports[`VTreeView.ts should recalculate tree when loading async children using custom key 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--loading"
      >
        $loading
      </button>

          One

`;

exports[`VTreeView.ts should remove old nodes 1`] = `

          one

          two

`;

exports[`VTreeView.ts should remove old nodes 2`] = `

          one

`;

exports[`VTreeView.ts should remove old nodes 3`] = `

          one

          three

`;

exports[`VTreeView.ts should render items 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
      >
        $subgroup
      </button>

          Root

`;

exports[`VTreeView.ts should render items in dense mode 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
      >
        $subgroup
      </button>

          Root

`;

exports[`VTreeView.ts should select all leaf nodes 1`] = `

        arrow_drop_down

        check_box

          Root

`;

exports[`VTreeView.ts should select only leaf nodes 1`] = `

        arrow_drop_down

        $checkboxIndeterminate

          Root

            arrow_drop_down

            check_box_outline_blank

              Child

            check_box

              Child

`;

exports[`VTreeView.ts should select only root node 1`] = `

        arrow_drop_down

        check_box

          Root

`;

exports[`VTreeView.ts should show expand icon when children is empty and load-children prop used 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
      >
        $subgroup
      </button>

`;

exports[`VTreeView.ts should update selection when selected prop changes 1`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light"
      >
        $subgroup
      </button>
      <button type="button"
              class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light"
      >
        $checkboxOff
      </button>

          Root

`;

exports[`VTreeView.ts should update selection when selected prop changes 2`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
      >
        $subgroup
      </button>
      <button type="button"
              class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light accent--text"
      >
        $checkboxOn
      </button>

          Root

          <button type="button"
                  class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light accent--text"
          >
            $checkboxOn
          </button>

              Child

`;

exports[`VTreeView.ts should update selection when selected prop changes 3`] = `

      <button type="button"
              class="v-icon notranslate v-treeview-node__toggle v-icon--link material-icons theme--light v-treeview-node__toggle--open"
      >
        $subgroup
      </button>
      <button type="button"
              class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light"
      >
        $checkboxOff
      </button>

          Root

          <button type="button"
                  class="v-icon notranslate v-treeview-node__checkbox v-icon--link material-icons theme--light"
          >
            $checkboxOff
          </button>

              Child

`;


color={ typeof props.loading === 'boolean' ? undefined : props.loading }

@use '../../styles/settings'
@use '../../styles/tools'
@use './variables' as *

.v-data-table
  width: 100%

.v-data-table__table
  width: 100%
  border-collapse: separate
  border-spacing: 0

.v-data-table__tr
  &--focus
    border: 1px dotted black

  &--clickable
    cursor: pointer

.v-data-table
  .v-table__wrapper
    > table
      > thead,
        tbody
        > tr
          > td,
            th
            background: rgb(var(--v-theme-surface))

            &.v-data-table-column--align-end
              text-align: end

              .v-data-table-header__content
                flex-direction: row-reverse

            &.v-data-table-column--align-center
              text-align: center

              .v-data-table-header__content
                justify-content: center

            &.v-data-table-column--no-padding
              padding: 0 8px

          > th
            align-items: center

          > th.v-data-table__th--sortable:hover
              cursor: pointer
              color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))

          > th:not(.v-data-table__th--sorted)
              .v-data-table-header__sort-icon
                opacity: 0

              &:hover
                .v-data-table-header__sort-icon
                  opacity: 0.5

.v-data-table-column--fixed
  position: sticky
  z-index: 2
  left: 0

.v-data-table-column--last-fixed
  border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity))

.v-data-table-group-header-row
  td
    background: lightgrey

    > span
      padding-left: 5px

.v-data-table--loading
  .v-data-table__td
    opacity: 0.3

.v-data-table__progress
  th
    position: relative
    border: none !important
    height: auto !important

.v-data-table-group-header-row__column
  padding-left: calc(var(--v-data-table-group-header-row-depth) * 16px) !important

.v-data-table-header__content
  display: flex
  align-items: center

.v-data-table-header__sort-badge
  display: inline-flex
  justify-content: center
  align-items: center
  font-size: 0.875rem
  padding: 4px
  border-radius: 50%
  background: $data-table-header-sort-badge-color
  min-width: $data-table-header-sort-badge-size
  min-height: $data-table-header-sort-badge-size
  width: $data-table-header-sort-badge-size
  height: $data-table-header-sort-badge-size

.v-data-table-headers__loader
  bottom: auto
  top: 0
  left: 0
  position: absolute
  right: 0
  width: 100%
  z-index: 1

.v-data-table-rows-loading,
.v-data-table-rows-no-data
  text-align: center


<template>
  <v-card>
    <v-card-text>
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi, ratione debitis quis est labore voluptatibus! Eaque cupiditate minima, at placeat totam, magni doloremque veniam neque porro libero rerum unde voluptatem!
    </v-card-text>

    <v-card-title>Title</v-card-title>
  </v-card>
</template>


// Utilities
import {
  HexToHSV,
  HSLtoHSV,
  HSVtoHex,
  HSVtoHSL,
  HSVtoRGB,
  RGBtoHSV,
} from '@/util/colorUtils'
import { has } from '@/util/helpers'

// Types
import type { HSL, HSV, RGB } from '@/util/colorUtils'

function stripAlpha (color: any, stripAlpha: boolean) {
  if (stripAlpha) {
    const { a, ...rest } = color

    return rest
  }

  return color
}

export function extractColor (color: HSV, input: any) {
  if (input == null || typeof input === 'string') {
    const hex = HSVtoHex(color)

    if (color.a === 1) return hex.slice(0, 7)
    else return hex
  }

  if (typeof input === 'object') {
    let converted

    if (has(input, ['r', 'g', 'b'])) converted = HSVtoRGB(color)
    else if (has(input, ['h', 's', 'l'])) converted = HSVtoHSL(color)
    else if (has(input, ['h', 's', 'v'])) converted = color

    return stripAlpha(converted, !has(input, ['a']) && color.a === 1)
  }

  return color
}

export function hasAlpha (color: any) {
  if (!color) return false

  if (typeof color === 'string') {
    return color.length > 7
  }

  if (typeof color === 'object') {
    return has(color, ['a']) || has(color, ['alpha'])
  }

  return false
}

export const nullColor = { h: 0, s: 0, v: 1, a: 1 }

export type ColorPickerMode = {
  inputProps: Record<string, unknown>
  inputs: {
    [key: string]: any
    getValue: (color: any) => number | string
    getColor: (color: any, v: string) => any
  }[]
  from: (color: any) => HSV
  to: (color: HSV) => any
}

const rgba: ColorPickerMode = {
  inputProps: {
    type: 'number',
    min: 0,
  },
  inputs: [
    {
      label: 'R',
      max: 255,
      step: 1,
      getValue: (c: RGB) => Math.round(c.r),
      getColor: (c: RGB, v: string): RGB => ({ ...c, r: Number(v) }),
    },
    {
      label: 'G',
      max: 255,
      step: 1,
      getValue: (c: RGB) => Math.round(c.g),
      getColor: (c: RGB, v: string): RGB => ({ ...c, g: Number(v) }),
    },
    {
      label: 'B',
      max: 255,
      step: 1,
      getValue: (c: RGB) => Math.round(c.b),
      getColor: (c: RGB, v: string): RGB => ({ ...c, b: Number(v) }),
    },
    {
      label: 'A',
      max: 1,
      step: 0.01,
      getValue: ({ a }: RGB) => a != null ? Math.round(a * 100) / 100 : 1,
      getColor: (c: RGB, v: string): RGB => ({ ...c, a: Number(v) }),
    },
  ],
  to: HSVtoRGB,
  from: RGBtoHSV,
}

const rgb = {
  ...rgba,
  inputs: rgba.inputs?.slice(0, 3),
}

const hsla: ColorPickerMode = {
  inputProps: {
    type: 'number',
    min: 0,
  },
  inputs: [
    {
      label: 'H',
      max: 360,
      step: 1,
      getValue: (c: HSL) => Math.round(c.h),
      getColor: (c: HSL, v: string): HSL => ({ ...c, h: Number(v) }),
    },
    {
      label: 'S',
      max: 1,
      step: 0.01,
      getValue: (c: HSL) => Math.round(c.s * 100) / 100,
      getColor: (c: HSL, v: string): HSL => ({ ...c, s: Number(v) }),
    },
    {
      label: 'L',
      max: 1,
      step: 0.01,
      getValue: (c: HSL) => Math.round(c.l * 100) / 100,
      getColor: (c: HSL, v: string): HSL => ({ ...c, l: Number(v) }),
    },
    {
      label: 'A',
      max: 1,
      step: 0.01,
      getValue: ({ a }: HSL) => a != null ? Math.round(a * 100) / 100 : 1,
      getColor: (c: HSL, v: string): HSL => ({ ...c, a: Number(v) }),
    },
  ],
  to: HSVtoHSL,
  from: HSLtoHSV,
}

const hsl = {
  ...hsla,
  inputs: hsla.inputs.slice(0, 3),
}

const hexa: ColorPickerMode = {
  inputProps: {
    type: 'text',
  },
  inputs: [
    {
      label: 'HEXA',
      getValue: (c: string) => c,
      getColor: (c: string, v: string) => v,
    },
  ],
  to: HSVtoHex,
  from: HexToHSV,
}

const hex = {
  ...hexa,
  inputs: [
    {
      label: 'HEX',
      getValue: (c: string) => c.slice(0, 7),
      getColor: (c: string, v: string) => v,
    },
  ],
}

export const modes = {
  rgb,
  rgba,
  hsl,
  hsla,
  hex,
  hexa,
} satisfies Record<string, ColorPickerMode>


MISSING DESCRIPTION ([edit in github](https://github.com/vuetifyjs/vuetify/tree/master/packages/api-generator/src/locale/en/slider.json))


record.editedAt = new Date
record.editedBy =
_id: Meteor.userId()
username: me.username
message.pinned = record.pinned
message.pinnedAt = record.pinnedAt
message.pinnedBy =
_id: record.pinnedBy?._id
username: record.pinnedBy?.username
delete record._id

keep keep keep keep replace replace replace keep keep keep

record.pinned = record.pinned
record.pinnedAt = record.pinnedAt
record.pinnedBy =

Meteor.startup ->
	RocketChat.MessageAction.addButton
		id: 'pin-message'
		icon: 'icon-pin'
		i18nLabel: 'Pin_Message'
		action: (event, instance) ->
			message = @_arguments[1]
			message.pinned = true
			Meteor.call 'pinMessage', message, (error, result) ->
				if error
					return Errors.throw error.reason
		validation: (message) ->
			if message.pinned or not RocketChat.settings.get('Message_AllowPinning')
				return false

			if RocketChat.settings.get('Message_AllowPinningByAnyone') or RocketChat.authz.hasRole Meteor.userId(), 'admin'
				return true

			return room.u?._id is Meteor.userId()
		order: 20

	RocketChat.MessageAction.addButton
		id: 'unpin-message'
		icon: 'icon-pin'
		i18nLabel: 'Unpin_Message'
		action: (event, instance) ->
			message = @_arguments[1]
			message.pinned = false
			Meteor.call 'unpinMessage', message, (error, result) ->
				if error
					return Errors.throw error.reason
		validation: (message) ->
			if not message.pinned or not RocketChat.settings.get('Message_AllowPinning')
				return false

			if RocketChat.settings.get('Message_AllowPinningByAnyone') or RocketChat.authz.hasRole Meteor.userId(), 'admin'
				return true

			return room.u?._id is Meteor.userId()
		order: 21


Meteor.methods
	deleteUser: (userId) ->
		if not Meteor.userId()
			throw new Meteor.Error('invalid-user', "[methods] deleteUser -> Invalid user")

		user = RocketChat.models.Users.findOneById Meteor.userId()

		unless RocketChat.authz.hasPermission(Meteor.userId(), 'delete-user') is true
			throw new Meteor.Error 'not-authorized', '[methods] deleteUser -> Not authorized'

		user = RocketChat.models.Users.findOneById userId
		unless user?
			throw new Meteor.Error 'not-found', '[methods] deleteUser -> User not found'

		RocketChat.models.Messages.removeByUserId userId # Remove user messages

		RocketChat.models.Subscriptions.findByUserId(userId).forEach (subscription) ->
			room = RocketChat.models.Rooms.findOneById subscription.rid
			if room.t isnt 'c' and room.usernames.length is 1
				RocketChat.models.Rooms.removeById subscription.rid # Remove non-channel rooms with only 1 user (the one being deleted)
			if room.t is 'd'
				RocketChat.models.Subscriptions.removeByRoomId subscription.rid
				RocketChat.models.Messages.removeByRoomId subscription.rid

		RocketChat.models.Subscriptions.removeByUserId userId # Remove user subscriptions
		RocketChat.models.Rooms.removeByTypeContainingUsername 'd', user.username # Remove direct rooms with the user
		RocketChat.models.Rooms.removeUsernameFromAll user.username # Remove user from all other rooms
		RocketChat.models.Users.removeById userId # Remove user from users database

		return true


###
# Me is a named function that will replace /me commands
# @param {Object} message - The message object
###

class Me
	constructor: (command, params, item) ->
		if(command == "me")
			if _.trim params
				currentUser = Meteor.user()
				msg = item
				msg.msg = '_' + params + '_'
				Meteor.call 'sendMessage', msg

RocketChat.slashCommands.add 'me', Me


if RocketChat.settings.get('Message_AllowPinningByAnyone') or RocketChat.authz.hasRole Meteor.userId(), 'admin'

keep keep keep keep replace keep keep keep keep keep

return ChatRoom.findOne(message.rid).u?._id is Meteor.userId()

RocketChat.models.Messages = new class extends RocketChat.models._Base
	constructor: ->
		@_initModel 'message'

		@tryEnsureIndex { 'rid': 1, 'ts': 1 }
		@tryEnsureIndex { 'editedAt': 1 }, { sparse: 1 }
		@tryEnsureIndex { 'editedBy._id': 1 }, { sparse: 1 }
		@tryEnsureIndex { 'rid': 1, 't': 1, 'u._id': 1 }
		@tryEnsureIndex { 'expireAt': 1 }, { expireAfterSeconds: 0 }
		@tryEnsureIndex { 'msg': 'text' }

	# FIND ONE
	findOneById: (_id, options) ->
		query =
			_id: _id

		return @findOne query, options

	# FIND
	findByMention: (username, options) ->
		query =
			"mentions.username": username

		return @find query, options

	findVisibleByRoomId: (roomId, options) ->
		query =
			_hidden:
				$ne: true
			rid: roomId

		return @find query, options

	findInvisibleByRoomId: (roomId, options) ->
		query =
			_hidden: true
			rid: roomId

		return @find query, options

	findVisibleByRoomIdAfterTimestamp: (roomId, timestamp, options) ->
		query =
			_hidden:
				$ne: true
			rid: roomId
			ts:
				$gt: timestamp

		return @find query, options

	findVisibleByRoomIdBeforeTimestamp: (roomId, timestamp, options) ->
		query =
			_hidden:
				$ne: true
			rid: roomId
			ts:
				$lt: timestamp

		return @find query, options

	findVisibleByRoomIdBetweenTimestamps: (roomId, afterTimestamp, beforeTimestamp, options) ->
		query =
			_hidden:
				$ne: true
			rid: roomId
			ts:
				$gt: afterTimestamp
				$lt: beforeTimestamp

		return @find query, options

	findVisibleCreatedOrEditedAfterTimestamp: (timestamp, options) ->
		query =
			_hidden: { $ne: true }
			$or: [
				ts:
					$gt: timestamp
			,
				'editedAt':
					$gt: timestamp
			]

		return @find query, options

	findStarredByUserAtRoom: (userId, roomId, options) ->
		query =
			_hidden: { $ne: true }
			'starred._id': userId
			rid: roomId

		console.log 'findStarredByUserAtRoom', arguments

		return @find query, options

	findPinnedByRoom: (roomId, options) ->
		query =
			_hidden: { $ne: true }
			pinned: true
			rid: roomId

		return @find query, options

	cloneAndSaveAsHistoryById: (_id) ->
		me = RocketChat.models.Users.findOneById Meteor.userId()
		record = @findOneById _id
		record._hidden = true
		record.parent = record._id
		record.editedAt = new Date
		record.editedBy =
			_id: Meteor.userId()
			username: me.username
		message.pinned = record.pinned
		message.pinnedAt = record.pinnedAt
		message.pinnedBy =
			_id: record.pinnedBy?._id
			username: record.pinnedBy?.username
		delete record._id

		return @insert record

	# UPDATE
	setHiddenById: (_id, hidden=true) ->
		query =
			_id: _id

		update =
			$set:
				_hidden: hidden

		return @update query, update

	setAsDeletedById: (_id) ->
		me = RocketChat.models.Users.findOneById Meteor.userId()
		query =
			_id: _id

		update =
			$set:
				msg: ''
				t: 'rm'
				editedAt: new Date()
				editedBy:
					_id: Meteor.userId()
					username: me.username

		return @update query, update

	setPinnedByIdAndUserId: (_id, pinnedBy, pinned=true) ->
		query =
			_id: _id

		update =
			$set:
				pinned: pinned
				pinnedAt: new Date
				pinnedBy: pinnedBy

		return @update query, update

	setUrlsById: (_id, urls) ->
		query =
			_id: _id

		update =
			$set:
				urls: urls

		return @update query, update

	updateAllUsernamesByUserId: (userId, username) ->
		query =
			'u._id': userId

		update =
			$set:
				"u.username": username

		return @update query, update, { multi: true }

	updateUsernameOfEditByUserId: (userId, username) ->
		query =
			'editedBy._id': userId

		update =
			$set:
				"editedBy.username": username

		return @update query, update, { multi: true }

	updateUsernameAndMessageOfMentionByIdAndOldUsername: (_id, oldUsername, newUsername, newMessage) ->
		query =
			_id: _id
			"mentions.username": oldUsername

		update =
			$set:
				"mentions.$.username": newUsername
				"msg": newMessage

		return @update query, update

	updateUserStarById: (_id, userId, starred) ->
		query =
			_id: _id

		if starred
			update =
				$addToSet:
					starred: { _id: userId }
		else
			update =
				$pull:
					starred: { _id: Meteor.userId() }

		return @update query, update

	upgradeEtsToEditAt: ->
		query =
			ets: { $exists: 1 }

		update =
			$rename:
				"ets": "editedAt"

		return @update query, update, { multi: true }

	# INSERT
	createWithTypeRoomIdMessageAndUser: (type, roomId, message, user, extraData) ->
		record =
			t: type
			rid: roomId
			ts: new Date
			msg: message
			u:
				_id: user._id
				username: user.username

		_.extend record, extraData

		record._id = @insert record
		return record

	createUserJoinWithRoomIdAndUser: (roomId, user, extraData) ->
		message = user.username
		return @createWithTypeRoomIdMessageAndUser 'uj', roomId, message, user, extraData

	createUserLeaveWithRoomIdAndUser: (roomId, user, extraData) ->
		message = user.username
		return @createWithTypeRoomIdMessageAndUser 'ul', roomId, message, user, extraData

	createUserRemovedWithRoomIdAndUser: (roomId, user, extraData) ->
		message = user.username
		return @createWithTypeRoomIdMessageAndUser 'ru', roomId, message, user, extraData

	createUserAddedWithRoomIdAndUser: (roomId, user, extraData) ->
		message = user.username
		return @createWithTypeRoomIdMessageAndUser 'au', roomId, message, user, extraData

	createRoomRenamedWithRoomIdRoomNameAndUser: (roomId, roomName, user, extraData) ->
		return @createWithTypeRoomIdMessageAndUser 'r', roomId, roomName, user, extraData

	# REMOVE
	removeById: (_id) ->
		query =
			_id: _id

		return @remove query

	removeByRoomId: (roomId) ->
		query =
			rid: roomId

		return @remove query

	removeByUserId: (userId) ->
		query =
			"u._id": userId

		return @remove query


<template name="createChannelFlex">

{{_ "Channels"}}

{{_ "Create_new_public_channel" }}

				{{_ "Name"}}
				<input type="text" id="channel-name" class="required" dir="auto">

				{{_ "Add_Members"}}
				{{> inputAutocomplete settings=autocompleteSettings id="channel-members" class="search" placeholder=tRoomMembers autocomplete="off"}}

					{{#each selectedUsers}}

{{name}}

					{{/each}}

			{{#if error.fields}}

					{{_ "Oops!"}}
					{{#each error.fields}}

{{_ "The_field_is_required" .}}

					{{/each}}

			{{/if}}
			{{#if error.invalid}}

					{{_ "Oops!"}}
					{{{_ "Invalid_room_name" roomName}}}

			{{/if}}
			{{#if error.duplicate}}

					{{_ "Oops!"}}
					{{{_ "Duplicate_channel_name" roomName}}}

			{{/if}}

				<button class="button clean primary save-channel">{{_ "Save" }}</button>
				<button class="button clean cancel-channel">{{_ "Cancel" }}</button>

			<button class="button clean all">{{_ "All_channels"}}</button>

</template>


Meteor.methods
	logoutCleanUp: (user) ->
		console.log '[methods] logoutCleanUp -> '.green, 'userId:', user._id

		Meteor.defer ->

			RocketChat.callbacks.run 'afterLogoutCleanUp', user


Meteor.methods
	addOAuthService: (name) ->
		if not Meteor.userId()
			throw new Meteor.Error('invalid-user', "[methods] addOAuthService -> Invalid user")

		console.log '[methods] addOAuthService -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments

		unless RocketChat.authz.hasPermission( Meteor.userId(), 'add-oauth-service') is true
			throw new Meteor.Error 'not-authorized', '[methods] addOAuthService -> Not authorized'

		name = name.toLowerCase().replace(/[^a-z0-9]/g, '')
		name = s.capitalize(name)
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}"                   , false             , { type: 'boolean', group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_Enable'}
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}_url"               , ''                , { type: 'string' , group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_URL'}
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}_token_path"        , '/oauth/token'    , { type: 'string' , group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_Token_Path'}
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}_identity_path"     , '/me'             , { type: 'string' , group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_Identity_Path'}
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}_authorize_path"    , '/oauth/authorize', { type: 'string' , group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_Authorize_Path'}
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}_id"                , ''                , { type: 'string' , group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_id'}
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}_secret"            , ''                , { type: 'string' , group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_Secret'}
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}_button_label_text" , ''                , { type: 'string' , group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text'}
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}_button_label_color", '#FFFFFF'         , { type: 'string' , group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color'}
		RocketChat.settings.add "Accounts_OAuth_Custom_#{name}_button_color"      , '#13679A'         , { type: 'string' , group: 'Accounts', section: "Custom OAuth: #{name}", i18nLabel: 'Accounts_OAuth_Custom_Button_Color'}

	removeOAuthService: (name) ->
		if not Meteor.userId()
			throw new Meteor.Error('invalid-user', "[methods] addOAuthService -> Invalid user")

		console.log '[methods] addOAuthService -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments

		unless RocketChat.authz.hasPermission( Meteor.userId(), 'add-oauth-service') is true
			throw new Meteor.Error 'not-authorized', '[methods] addOAuthService -> Not authorized'

		name = name.toLowerCase().replace(/[^a-z0-9]/g, '')
		name = s.capitalize(name)
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}"
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}_url"
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}_token_path"
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}_identity_path"
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}_authorize_path"
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}_id"
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}_secret"
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}_button_label_text"
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}_button_label_color"
		RocketChat.settings.removeById "Accounts_OAuth_Custom_#{name}_button_color"


RocketChat.authz.hasRole = (userId, roleName, scope=Roles.GLOBAL_GROUP) ->
	unless Meteor.userId()
		return false

	roleName = [].concat roleName

	# per alanning:roles, returns true if user is in ANY roles
	return Roles.userIsInRole(userId, roleName, scope)


<template name="modal">

				<form id="{{this.id}}-form" class="modal form-horizontal">

{{this.title}}

						{{> UI.contentBlock}}

						{{#if this.save}}
							<button class="-btn primary" type="submit" data-loading-text="{{_ "Please_wait"}}...">{{this.save}}</button>
						{{/if}}
						{{#if this.close}}
							<button class="-btn delete">{{this.close}}</button>
						{{/if}}

				</form>

</template>


permissions = await getPermissionsFromZippedApp(appFile);

confirm={(permissions) => sendFile(permissions, appFile)}

keep keep keep replace replace replace replace replace replace replace keep keep keep keep keep

if (!permissions || permissions.length === 0) {

import { Box, Button, Icon, Throbber } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import React, { useCallback, useState, memo } from 'react';

import { useTranslation } from '../../../contexts/TranslationContext';
import { appButtonProps, appStatusSpanProps, handleAPIError, warnStatusChange } from './helpers';
import { Apps } from '../../../../app/apps/client/orchestrator';
import IframeModal from './IframeModal';
import CloudLoginModal from './CloudLoginModal';
import AppPermissionsReviewModal from './AppPermissionsReviewModal';
import { useSetModal } from '../../../contexts/ModalContext';
import { useMethod } from '../../../contexts/ServerContext';

const installApp = async ({ id, name, version, permissionsGranted }) => {
	try {
		const { status } = await Apps.installApp(id, version, permissionsGranted);
		warnStatusChange(name, status);
	} catch (error) {
		handleAPIError(error);
	}
};

const actions = {
	purchase: installApp,
	install: installApp,
	update: async ({ id, name, marketplaceVersion, permissionsGranted }) => {
		try {
			const { status } = await Apps.updateApp(id, marketplaceVersion, permissionsGranted);
			warnStatusChange(name, status);
		} catch (error) {
			handleAPIError(error);
		}
	},
};

const AppStatus = ({ app, showStatus = true, ...props }) => {
	const t = useTranslation();
	const [loading, setLoading] = useSafely(useState());
	const [isAppPurchased, setPurchased] = useSafely(useState(app.isPurchased));
	const setModal = useSetModal();

	const button = appButtonProps(app);
	const status = !button && appStatusSpanProps(app);

	const action = button?.action || '';
	const confirmAction = useCallback((permissionsGranted) => {
		setModal(null);

		actions[action]({ ...app, permissionsGranted }).then(() => {
			setLoading(false);
		});
	}, [setModal, action, app, setLoading]);

	const cancelAction = useCallback(() => {
		setLoading(false);
		setModal(null);
	}, [setLoading, setModal]);

	const showAppPermissionsReviewModal = () => {
		if (!isAppPurchased) {
			setPurchased(true);
		}

		if (!Array.isArray(app.permissions) || !app.permissions.length) {
			return confirmAction();
		}

		return setModal(<AppPermissionsReviewModal appPermissions={app.permissions} cancel={cancelAction} confirm={confirmAction} />);
	};

	const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn');

	const handleClick = async (e) => {
		e.preventDefault();
		e.stopPropagation();

		setLoading(true);

		const isLoggedIn = await checkUserLoggedIn();

		if (!isLoggedIn) {
			setLoading(false);
			setModal(<CloudLoginModal />);
			return;
		}

		if (action === 'purchase' && !isAppPurchased) {
			try {
				const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false);
				setModal(<IframeModal url={data.url} cancel={cancelAction} confirm={showAppPermissionsReviewModal}/>);
			} catch (error) {
				handleAPIError(error);
			}
			return;
		}

		showAppPermissionsReviewModal();
	};

	return <Box {...props}>
		{button && <Button primary disabled={loading} invisible={!showStatus && !loading} minHeight='x40' onClick={handleClick}>
			{loading
				? <Throbber inheritColor />
				: <>
					{button.icon && <Icon name={button.icon} />}{t(button.label)}
				</>}
		</Button>}
		{status && <Box color={status.label === 'Disabled' ? 'warning' : 'hint'} display='flex' alignItems='center'>
			<Icon size='x20' name={status.icon} mie='x4'/>
			{t(status.label)}
		</Box>}
	</Box>;
};

export default memo(AppStatus);


import { Meteor } from 'meteor/meteor';

import { CustomUserStatus } from '../../../models';

Meteor.methods({
	listCustomUserStatus() {
		const currentUserId = Meteor.userId();
		if (!currentUserId) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'listCustomUserStatus' });
		}

		return CustomUserStatus.find({}).fetch();
	},
});


import * as Mailer from '../../mailer';
import { settings } from '../../settings';

Mailer.setSettings(settings);


import React, { useCallback, useState, memo } from 'react';

import { appButtonProps, appStatusSpanProps, handleAPIError, warnStatusChange } from './helpers';

import { Apps } from '../../../../app/apps/client/orchestrator';

import IframeModal from './IframeModal';

import CloudLoginModal from './CloudLoginModal';

keep keep keep replace keep keep keep keep

import { appButtonProps, appStatusSpanProps, handleAPIError, warnStatusChange, handleInstallError } from './helpers';

import { Button, ButtonGroup, Icon, Field, FieldGroup, TextInput, Throbber } from '@rocket.chat/fuselage';
import React, { useCallback, useEffect, useState } from 'react';

import Page from '../../../components/Page';
import { useRoute, useQueryStringParameter } from '../../../contexts/RouterContext';
import { useEndpoint, useUpload } from '../../../contexts/ServerContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useFileInput } from '../../../hooks/useFileInput';
import { useForm } from '../../../hooks/useForm';
import { handleInstallError } from './helpers';
import AppPermissionsReviewModal from './AppPermissionsReviewModal';
import { useSetModal } from '../../../contexts/ModalContext';
import { getPermissionsFromZippedApp } from './lib/getPermissionsFromZippedApp';

const placeholderUrl = 'https://rocket.chat/apps/package.zip';

function AppInstallPage() {
	const t = useTranslation();

	const appsRoute = useRoute('admin-apps');
	const setModal = useSetModal();

	const appId = useQueryStringParameter('id');
	const queryUrl = useQueryStringParameter('url');

	const [installing, setInstalling] = useState(false);

	const endpointAddress = appId ? `/apps/${ appId }` : '/apps';
	const downloadApp = useEndpoint('POST', endpointAddress);
	const uploadApp = useUpload(endpointAddress);

	const { values, handlers } = useForm({
		file: {},
		url: queryUrl,
	});

	const {
		file,
		url,
	} = values;

	const canSave = !!url || !!file.name;

	const {
		handleFile,
		handleUrl,
	} = handlers;

	useEffect(() => {
		queryUrl && handleUrl(queryUrl);
	}, [queryUrl, handleUrl]);

	const [handleUploadButtonClick] = useFileInput(handleFile, 'app');

	const sendFile = async (permissionsGranted, appFile) => {
		const fileData = new FormData();
		fileData.append('app', appFile, appFile.name);
		fileData.append('permissions', JSON.stringify(permissionsGranted));
		const { app } = await uploadApp(fileData);
		appsRoute.push({ context: 'details', id: app.id });
		setModal(null);
	};

	const cancelAction = useCallback(() => {
		setInstalling(false);
		setModal(null);
	}, [setInstalling, setModal]);

	const install = async () => {
		setInstalling(true);

		try {
			let permissions;
			let appFile;
			if (url) {
				const { buff } = await downloadApp({ url, downloadOnly: true });
				const fileData = Uint8Array.from(buff.data);
				permissions = await getPermissionsFromZippedApp(fileData);
				appFile = new File([fileData], 'app.zip', { type: 'application/zip' });
			} else {
				appFile = file;
				permissions = await getPermissionsFromZippedApp(appFile);
			}

			setModal(
				<AppPermissionsReviewModal
					appPermissions={permissions}
					cancel={cancelAction}
					confirm={(permissions) => sendFile(permissions, appFile)}
				/>,
			);
		} catch (error) {
			handleInstallError(error);
		} finally {
			setInstalling(false);
		}
	};

	const handleCancel = () => {
		appsRoute.push();
	};

	return <Page flexDirection='column'>
		<Page.Header title={t('App_Installation')} />
		<Page.ScrollableContent>
			<FieldGroup display='flex' flexDirection='column' alignSelf='center' maxWidth='x600' w='full'>
				<Field>
					<Field.Label>{t('App_Url_to_Install_From')}</Field.Label>
					<Field.Row>
						<TextInput placeholder={placeholderUrl} value={url} onChange={handleUrl} addon={<Icon name='permalink' size='x20'/>}/>
					</Field.Row>
				</Field>
				<Field>
					<Field.Label>{t('App_Url_to_Install_From_File')}</Field.Label>
					<Field.Row>
						<TextInput
							value={file.name || ''}
							addon={<Button small primary onClick={handleUploadButtonClick} mb='neg-x4' mie='neg-x8'>
								<Icon name='upload' size='x12' />{t('Browse_Files')}
							</Button>}
						/>
					</Field.Row>
				</Field>
				<Field>
					<ButtonGroup>
						<Button disabled={!canSave || installing} onClick={install}>
							{!installing && t('Install')}
							{installing && <Throbber inheritColor/>}
						</Button>
						<Button onClick={handleCancel}>{t('Cancel')}</Button>
					</ButtonGroup>
				</Field>
			</FieldGroup>
		</Page.ScrollableContent>
	</Page>;
}

export default AppInstallPage;


<template name="home">

		{{> header sectionName=title}}

			{{{body}}}

</template>


import { getNameAndDomain, isFullyQualified } from '../functions/helpers';
import { getFederationDomain } from '../lib/getFederationDomain';

const denormalizeRoom = (originalResource) => {
	const resource = { ...originalResource };

	if (resource.t === 'd') {
		resource.usernames = resource.usernames.map((u) => {
			const [username, domain] = getNameAndDomain(u);

			return domain === getFederationDomain() ? username : u;
		});
	} else {
		// Denormalize room name
		const [roomName, roomDomain] = getNameAndDomain(resource.name);

		resource.name = roomDomain === getFederationDomain() ? roomName : resource.name;

		// Denormalize room owner name
		const [username, userDomain] = getNameAndDomain(resource.u.username);

		resource.u.username = userDomain === getFederationDomain() ? username : resource.u.username;

		// Denormalize muted users
		if (resource.muted) {
			resource.muted = resource.muted.map((u) => {
				const [username, domain] = getNameAndDomain(u);

				return domain === getFederationDomain() ? username : u;
			});
		}

		// Denormalize unmuted users
		if (resource.unmuted) {
			resource.unmuted = resource.unmuted.map((u) => {
				const [username, domain] = getNameAndDomain(u);

				return domain === getFederationDomain() ? username : u;
			});
		}
	}

	return resource;
};

const normalizeRoom = (originalResource, users) => {
	const resource = { ...originalResource };

	let domains = '';

	if (resource.t === 'd') {
		// Handle user names, adding the Federation domain to local users
		resource.usernames = resource.usernames.map((u) => (!isFullyQualified(u) ? `${ u }@${ getFederationDomain() }` : u));

		// Get the domains of the usernames
		domains = resource.usernames.map((u) => getNameAndDomain(u)[1]);
	} else {
		// Ensure private
		resource.t = 'p';

		// Normalize room name
		resource.name = !isFullyQualified(resource.name) ? `${ resource.name }@${ getFederationDomain() }` : resource.name;

		// Get the users domains
		domains = users.map((u) => u.federation.origin);

		// Normalize the username
		resource.u.username = !isFullyQualified(resource.u.username) ? `${ resource.u.username }@${ getFederationDomain() }` : resource.u.username;

		// Normalize the muted users
		if (resource.muted) {
			resource.muted = resource.muted.map((u) => (!isFullyQualified(u) ? `${ u }@${ getFederationDomain() }` : u));
		}

		// Normalize the unmuted users
		if (resource.unmuted) {
			resource.unmuted = resource.unmuted.map((u) => (!isFullyQualified(u) ? `${ u }@${ getFederationDomain() }` : u));
		}
	}

	domains = [...new Set(domains)];

	// Federation
	resource.federation = resource.federation || {
		origin: getFederationDomain(), // The origin of this resource, where it was created
		domains, // The domains where this room exist (or will exist)
	};

	return resource;
};

export default {
	denormalizeRoom,
	normalizeRoom,
};


//! moment.js locale configuration
//! locale : Catalan [ca]
//! author : Juan G. Hurtado : https://github.com/juanghurtado

;(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined'
       && typeof require === 'function' ? factory(require('../moment')) :
   typeof define === 'function' && define.amd ? define(['../moment'], factory) :
   factory(global.moment)
}(this, function (moment) { 'use strict';

    var ca = moment.defineLocale('ca', {
        months : 'gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre'.split('_'),
        monthsShort : 'gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.'.split('_'),
        monthsParseExact : true,
        weekdays : 'diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte'.split('_'),
        weekdaysShort : 'dg._dl._dt._dc._dj._dv._ds.'.split('_'),
        weekdaysMin : 'Dg_Dl_Dt_Dc_Dj_Dv_Ds'.split('_'),
        weekdaysParseExact : true,
        longDateFormat : {
            LT : 'H:mm',
            LTS : 'H:mm:ss',
            L : 'DD/MM/YYYY',
            LL : 'D MMMM YYYY',
            LLL : 'D MMMM YYYY H:mm',
            LLLL : 'dddd D MMMM YYYY H:mm'
        },
        calendar : {
            sameDay : function () {
                return '[avui a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
            },
            nextDay : function () {
                return '[demà a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
            },
            nextWeek : function () {
                return 'dddd [a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
            },
            lastDay : function () {
                return '[ahir a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
            },
            lastWeek : function () {
                return '[el] dddd [passat a ' + ((this.hours() !== 1) ? 'les' : 'la') + '] LT';
            },
            sameElse : 'L'
        },
        relativeTime : {
            future : 'en %s',
            past : 'fa %s',
            s : 'uns segons',
            m : 'un minut',
            mm : '%d minuts',
            h : 'una hora',
            hh : '%d hores',
            d : 'un dia',
            dd : '%d dies',
            M : 'un mes',
            MM : '%d mesos',
            y : 'un any',
            yy : '%d anys'
        },
        ordinalParse: /\d{1,2}(r|n|t|è|a)/,
        ordinal : function (number, period) {
            var output = (number === 1) ? 'r' :
                (number === 2) ? 'n' :
                (number === 3) ? 'r' :
                (number === 4) ? 't' : 'è';
            if (period === 'w' || period === 'W') {
                output = 'a';
            }
            return number + output;
        },
        week : {
            dow : 1, // Monday is the first day of the week.
            doy : 4  // The week that contains Jan 4th is the first week of the year.
        }
    });

    return ca;

}));


const showAppPermissionsReviewModal = () => {

if (!Array.isArray(app.permissions) || !app.permissions.length) {

return setModal(<AppPermissionsReviewModal appPermissions={app.permissions} cancel={cancelAction} confirm={confirmAction} />);

keep keep keep keep keep replace replace keep keep keep keep

if (!app.permissions || app.permissions.length === 0) {

handleInstallError(new Error('The "permissions" property from the app manifest is invalid'));


<template name="CodeMirror">
	<textarea id="{{editorId}}" name="{{editorName}}" style="display: none">{{code}}</textarea>
</template>


import { createTemplateForComponent } from './reactAdapters';

createTemplateForComponent('MessageActions', () => import('./components/Message/Actions'));
createTemplateForComponent('reactAttachments', () => import('./components/Message/Attachments'));
createTemplateForComponent('ThreadMetric', () => import('./components/Message/Metrics/Thread'));
createTemplateForComponent('DiscussionMetric', () => import('./components/Message/Metrics/Discussion'));
createTemplateForComponent('BroadCastMetric', () => import('./components/Message/Metrics/Broadcast'));


import http from 'http';

import client from 'prom-client';
import connect from 'connect';
import _ from 'underscore';
import gcStats from 'prometheus-gc-stats';
import { Meteor } from 'meteor/meteor';
import { Facts } from 'meteor/facts-base';

import { Info, getOplogInfo } from '../../../utils/server';
import { Migrations } from '../../../migrations';
import { settings } from '../../../settings';
import { Statistics } from '../../../models';
import { metrics } from './metrics';
import { getAppsStatistics } from '../../../statistics/server/lib/getAppsStatistics';

Facts.incrementServerFact = function(pkg, fact, increment) {
	metrics.meteorFacts.inc({ pkg, fact }, increment);
};

const setPrometheusData = async () => {
	metrics.info.set({
		version: Info.version,
		unique_id: settings.get('uniqueID'),
		site_url: settings.get('Site_Url'),
	}, 1);

	const sessions = Array.from(Meteor.server.sessions.values());
	const authenticatedSessions = sessions.filter((s) => s.userId);
	metrics.ddpSessions.set(Meteor.server.sessions.size);
	metrics.ddpAuthenticatedSessions.set(authenticatedSessions.length);
	metrics.ddpConnectedUsers.set(_.unique(authenticatedSessions.map((s) => s.userId)).length);

	const statistics = Statistics.findLast();
	if (!statistics) {
		return;
	}

	metrics.version.set({ version: statistics.version }, 1);
	metrics.migration.set(Migrations._getControl().version);
	metrics.instanceCount.set(statistics.instanceCount);
	metrics.oplogEnabled.set({ enabled: statistics.oplogEnabled }, 1);

	// User statistics
	metrics.totalUsers.set(statistics.totalUsers);
	metrics.activeUsers.set(statistics.activeUsers);
	metrics.nonActiveUsers.set(statistics.nonActiveUsers);
	metrics.onlineUsers.set(statistics.onlineUsers);
	metrics.awayUsers.set(statistics.awayUsers);
	metrics.offlineUsers.set(statistics.offlineUsers);

	// Room statistics
	metrics.totalRooms.set(statistics.totalRooms);
	metrics.totalChannels.set(statistics.totalChannels);
	metrics.totalPrivateGroups.set(statistics.totalPrivateGroups);
	metrics.totalDirect.set(statistics.totalDirect);
	metrics.totalLivechat.set(statistics.totalLivechat);

	// Message statistics
	metrics.totalMessages.set(statistics.totalMessages);
	metrics.totalChannelMessages.set(statistics.totalChannelMessages);
	metrics.totalPrivateGroupMessages.set(statistics.totalPrivateGroupMessages);
	metrics.totalDirectMessages.set(statistics.totalDirectMessages);
	metrics.totalLivechatMessages.set(statistics.totalLivechatMessages);

	// Apps metrics
	const { totalInstalled, totalActive, totalFailed } = getAppsStatistics();

	metrics.totalAppsInstalled.set(totalInstalled || 0);
	metrics.totalAppsEnabled.set(totalActive || 0);
	metrics.totalAppsFailed.set(totalFailed || 0);

	const oplogQueue = getOplogInfo().mongo._oplogHandle?._entryQueue?.length || 0;
	metrics.oplogQueue.set(oplogQueue);

	metrics.pushQueue.set(statistics.pushQueue || 0);
};

const app = connect();

// const compression = require('compression');
// app.use(compression());

app.use('/metrics', (req, res) => {
	res.setHeader('Content-Type', 'text/plain');
	const data = client.register.metrics();

	metrics.metricsRequests.inc();
	metrics.metricsSize.set(data.length);

	res.end(data);
});

app.use('/', (req, res) => {
	const html = `

			<title>Rocket.Chat Prometheus Exporter</title>

Rocket.Chat Prometheus Exporter

Metrics

	`;

	res.write(html);
	res.end();
});

const server = http.createServer(app);

let timer;
let resetTimer;
let defaultMetricsInitiated = false;
let gcStatsInitiated = false;
const was = {
	enabled: false,
	port: 9458,
	resetInterval: 0,
	collectGC: false,
};
const updatePrometheusConfig = async () => {
	const is = {
		port: process.env.PROMETHEUS_PORT || settings.get('Prometheus_Port'),
		enabled: settings.get('Prometheus_Enabled'),
		resetInterval: settings.get('Prometheus_Reset_Interval'),
		collectGC: settings.get('Prometheus_Garbage_Collector'),
	};

	if (Object.values(is).some((s) => s == null)) {
		return;
	}

	if (Object.entries(is).every(([k, v]) => v === was[k])) {
		return;
	}

	if (!is.enabled) {
		if (was.enabled) {
			console.log('Disabling Prometheus');
			server.close();
			Meteor.clearInterval(timer);
		}
		Object.assign(was, is);
		return;
	}

	console.log('Configuring Prometheus', is);

	if (!was.enabled) {
		server.listen({
			port: is.port,
			host: process.env.BIND_IP || '0.0.0.0',
		});

		timer = Meteor.setInterval(setPrometheusData, 5000);
	}

	Meteor.clearInterval(resetTimer);
	if (is.resetInterval) {
		resetTimer = Meteor.setInterval(() => {
			client.register.getMetricsAsArray().forEach((metric) => { metric.hashMap = {}; });
		}, is.resetInterval);
	}

	// Prevent exceptions on calling those methods twice since
	// it's not possible to stop them to be able to restart
	try {
		if (defaultMetricsInitiated === false) {
			defaultMetricsInitiated = true;
			client.collectDefaultMetrics();
		}
		if (is.collectGC && gcStatsInitiated === false) {
			gcStatsInitiated = true;
			gcStats()();
		}
	} catch (error) {
		console.error(error);
	}

	Object.assign(was, is);
};

Meteor.startup(async () => {
	settings.get(/^Prometheus_.+/, updatePrometheusConfig);
});


throw new Error('The "permissions" property from app.json is invalid');

keep keep keep keep keep replace keep keep keep keep


import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { Mongo } from 'meteor/mongo';
import { HTTP } from 'meteor/http';
import _ from 'underscore';

import { initAPN, sendAPN } from './apn';
import { sendGCM } from './gcm';
import { logger, LoggerManager } from './logger';
import { settings } from '../../settings/server';

export const _matchToken = Match.OneOf({ apn: String }, { gcm: String });
export const appTokensCollection = new Mongo.Collection('_raix_push_app_tokens');

appTokensCollection._ensureIndex({ userId: 1 });

export class PushClass {
	options = {}

	isConfigured = false

	configure(options) {
		this.options = Object.assign({
			sendTimeout: 60000, // Timeout period for notification send
		}, options);
		// https://npmjs.org/package/apn

		// After requesting the certificate from Apple, export your private key as
		// a .p12 file anddownload the .cer file from the iOS Provisioning Portal.

		// gateway.push.apple.com, port 2195
		// gateway.sandbox.push.apple.com, port 2195

		// Now, in the directory containing cert.cer and key.p12 execute the
		// following commands to generate your .pem files:
		// $ openssl x509 -in cert.cer -inform DER -outform PEM -out cert.pem
		// $ openssl pkcs12 -in key.p12 -out key.pem -nodes

		// Block multiple calls
		if (this.isConfigured) {
			throw new Error('Configure should not be called more than once!');
		}

		this.isConfigured = true;

		logger.debug('Configure', this.options);

		if (this.options.apn) {
			initAPN({ options: this.options, absoluteUrl: Meteor.absoluteUrl() });
		}
	}

	sendWorker(task, interval) {
		logger.debug(`Send worker started, using interval: ${ interval }`);

		return Meteor.setInterval(() => {
			try {
				task();
			} catch (error) {
				logger.debug(`Error while sending: ${ error.message }`);
			}
		}, interval);
	}

	_replaceToken(currentToken, newToken) {
		appTokensCollection.rawCollection().updateMany({ token: currentToken }, { $set: { token: newToken } });
	}

	_removeToken(token) {
		appTokensCollection.rawCollection().deleteOne({ token });
	}

	_shouldUseGateway() {
		return !!this.options.gateways
			&& settings.get('Register_Server')
			&& settings.get('Cloud_Service_Agree_PrivacyTerms');
	}

	sendNotificationNative(app, notification, countApn, countGcm) {
		logger.debug('send to token', app.token);

		if (app.token.apn) {
			countApn.push(app._id);
			// Send to APN
			if (this.options.apn) {
				notification.topic = app.appName;
				sendAPN({ userToken: app.token.apn, notification, _removeToken: this._removeToken });
			}
		} else if (app.token.gcm) {
			countGcm.push(app._id);

			// Send to GCM
			// We do support multiple here - so we should construct an array
			// and send it bulk - Investigate limit count of id's
			if (this.options.gcm && this.options.gcm.apiKey) {
				sendGCM({ userTokens: app.token.gcm, notification, _replaceToken: this._replaceToken, _removeToken: this._removeToken, options: this.options });
			}
		} else {
			throw new Error('send got a faulty query');
		}
	}

	sendGatewayPush(gateway, service, token, notification, tries = 0) {
		notification.uniqueId = this.options.uniqueId;

		const data = {
			data: {
				token,
				options: notification,
			},
			headers: {},
		};

		if (token && this.options.getAuthorization) {
			data.headers.Authorization = this.options.getAuthorization();
		}

		return HTTP.post(`${ gateway }/push/${ service }/send`, data, (error, response) => {
			if (response?.statusCode === 406) {
				logger.info('removing push token', token);
				appTokensCollection.remove({
					$or: [{
						'token.apn': token,
					}, {
						'token.gcm': token,
					}],
				});
				return;
			}

			if (response?.statusCode === 422) {
				logger.info('gateway rejected push notification. not retrying.', response);
				return;
			}

			if (response?.statusCode === 401) {
				logger.warn('Error sending push to gateway (not authorized)', response);
				return;
			}

			if (!error) {
				return;
			}

			logger.error(`Error sending push to gateway (${ tries } try) ->`, error);

			if (tries <= 4) {
				// [1, 2, 4, 8, 16] minutes (total 31)
				const ms = 60000 * Math.pow(2, tries);

				logger.log('Trying sending push to gateway again in', ms, 'milliseconds');

				return Meteor.setTimeout(() => this.sendGatewayPush(gateway, service, token, notification, tries + 1), ms);
			}
		});
	}

	sendNotificationGateway(app, notification, countApn, countGcm) {
		for (const gateway of this.options.gateways) {
			logger.debug('send to token', app.token);

			if (app.token.apn) {
				countApn.push(app._id);
				notification.topic = app.appName;
				return this.sendGatewayPush(gateway, 'apn', app.token.apn, notification);
			}

			if (app.token.gcm) {
				countGcm.push(app._id);
				return this.sendGatewayPush(gateway, 'gcm', app.token.gcm, notification);
			}
		}
	}

	sendNotification(notification = { badge: 0 }) {
		logger.debug('Sending notification', notification);

		const countApn = [];
		const countGcm = [];

		if (notification.from !== String(notification.from)) {
			throw new Error('Push.send: option "from" not a string');
		}
		if (notification.title !== String(notification.title)) {
			throw new Error('Push.send: option "title" not a string');
		}
		if (notification.text !== String(notification.text)) {
			throw new Error('Push.send: option "text" not a string');
		}

		logger.debug(`send message "${ notification.title }" to userId`, notification.userId);

		const query = {
			userId: notification.userId,
			$or: [
				{ 'token.apn': { $exists: true } },
				{ 'token.gcm': { $exists: true } },
			],
		};

		appTokensCollection.find(query).forEach((app) => {
			logger.debug('send to token', app.token);

			if (this._shouldUseGateway()) {
				return this.sendNotificationGateway(app, notification, countApn, countGcm);
			}

			return this.sendNotificationNative(app, notification, countApn, countGcm);
		});

		if (LoggerManager.logLevel === 2) {
			logger.debug(`Sent message "${ notification.title }" to ${ countApn.length } ios apps ${ countGcm.length } android apps`);

			// Add some verbosity about the send result, making sure the developer
			// understands what just happened.
			if (!countApn.length && !countGcm.length) {
				if (appTokensCollection.find().count() === 0) {
					logger.debug('GUIDE: The "appTokensCollection" is empty - No clients have registered on the server yet...');
				}
			} else if (!countApn.length) {
				if (appTokensCollection.find({ 'token.apn': { $exists: true } }).count() === 0) {
					logger.debug('GUIDE: The "appTokensCollection" - No APN clients have registered on the server yet...');
				}
			} else if (!countGcm.length) {
				if (appTokensCollection.find({ 'token.gcm': { $exists: true } }).count() === 0) {
					logger.debug('GUIDE: The "appTokensCollection" - No GCM clients have registered on the server yet...');
				}
			}
		}

		return {
			apn: countApn,
			gcm: countGcm,
		};
	}

	// This is a general function to validate that the data added to notifications
	// is in the correct format. If not this function will throw errors
	_validateDocument(notification) {
		// Check the general notification
		check(notification, {
			from: String,
			title: String,
			text: String,
			sent: Match.Optional(Boolean),
			sending: Match.Optional(Match.Integer),
			badge: Match.Optional(Match.Integer),
			sound: Match.Optional(String),
			notId: Match.Optional(Match.Integer),
			contentAvailable: Match.Optional(Match.Integer),
			forceStart: Match.Optional(Match.Integer),
			apn: Match.Optional({
				from: Match.Optional(String),
				title: Match.Optional(String),
				text: Match.Optional(String),
				badge: Match.Optional(Match.Integer),
				sound: Match.Optional(String),
				notId: Match.Optional(Match.Integer),
				actions: Match.Optional([Match.Any]),
				category: Match.Optional(String),
			}),
			gcm: Match.Optional({
				from: Match.Optional(String),
				title: Match.Optional(String),
				text: Match.Optional(String),
				image: Match.Optional(String),
				style: Match.Optional(String),
				summaryText: Match.Optional(String),
				picture: Match.Optional(String),
				badge: Match.Optional(Match.Integer),
				sound: Match.Optional(String),
				notId: Match.Optional(Match.Integer),
			}),
			android_channel_id: Match.Optional(String),
			userId: String,
			payload: Match.Optional(Object),
			delayUntil: Match.Optional(Date),
			createdAt: Date,
			createdBy: Match.OneOf(String, null),
		});

		if (!notification.userId) {
			throw new Error('No userId found');
		}
	}

	send(options) {
		// If on the client we set the user id - on the server we need an option
		// set or we default to "<SERVER>" as the creator of the notification
		// If current user not set see if we can set it to the logged in user
		// this will only run on the client if Meteor.userId is available
		const currentUser = options.createdBy || '<SERVER>';

		// Rig the notification object
		const notification = Object.assign({
			createdAt: new Date(),
			createdBy: currentUser,
			sent: false,
			sending: 0,
		}, _.pick(options, 'from', 'title', 'text', 'userId'));

		// Add extra
		Object.assign(notification, _.pick(options, 'payload', 'badge', 'sound', 'notId', 'delayUntil', 'android_channel_id'));

		if (Match.test(options.apn, Object)) {
			notification.apn = _.pick(options.apn, 'from', 'title', 'text', 'badge', 'sound', 'notId', 'category');
		}

		if (Match.test(options.gcm, Object)) {
			notification.gcm = _.pick(options.gcm, 'image', 'style', 'summaryText', 'picture', 'from', 'title', 'text', 'badge', 'sound', 'notId', 'actions', 'android_channel_id');
		}

		if (options.contentAvailable != null) {
			notification.contentAvailable = options.contentAvailable;
		}

		if (options.forceStart != null) {
			notification.forceStart = options.forceStart;
		}

		// Validate the notification
		this._validateDocument(notification);

		try {
			this.sendNotification(notification);
		} catch (error) {
			logger.debug(`Could not send notification id: "${ notification._id }", Error: ${ error.message }`);
			logger.debug(error.stack);
		}
	}
}

export const Push = new PushClass();


import { Template } from 'meteor/templating';

import { settings } from '../../../../settings';

Template.home.helpers({
	title() {
		return settings.get('Layout_Home_Title');
	},
	body() {
		return settings.get('Layout_Home_Body');
	},
});


import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';

import { Migrations } from '../../../app/migrations';
import { Uploads, Messages } from '../../../app/models';

Migrations.add({
	version: 15,
	up() {
		console.log('Starting file migration');
		const oldFilesCollection = new Mongo.Collection('cfs.Files.filerecord');
		const oldGridFSCollection = new Mongo.Collection('cfs_gridfs.files.files');
		const oldChunkCollection = new Mongo.Collection('cfs_gridfs.files.chunks');
		const newFilesCollection = Uploads;
		const newGridFSCollection = new Mongo.Collection('rocketchat_uploads.files');
		const newChunkCollection = new Mongo.Collection('rocketchat_uploads.chunks');

		oldFilesCollection.find({
			'copies.files.key': {
				$exists: true,
			},
		}).forEach((cfsRecord) => {
			const nameParts = cfsRecord.original.name && cfsRecord.original.name.split('.');
			let extension = '';
			let url = `ufs/rocketchat_uploads/${ cfsRecord._id }`;

			console.log('migrating file', url);

			if (nameParts && nameParts.length > 1) {
				extension = nameParts.pop();
				url = `${ url }.${ extension }`;
			}

			const record = {
				_id: cfsRecord._id,
				name: cfsRecord.original.name || '',
				size: cfsRecord.original.size,
				type: cfsRecord.original.type,
				complete: true,
				uploading: false,
				store: 'rocketchat_uploads',
				extension,
				userId: cfsRecord.userId,
				uploadedAt: cfsRecord.updatedAt,
				url: Meteor.absoluteUrl() + url,
			};

			newFilesCollection.insert(record);

			const oldGridFsFile = oldGridFSCollection.findOne({
				_id: new Mongo.Collection.ObjectID(cfsRecord.copies.files.key),
			});

			newGridFSCollection.insert({
				_id: cfsRecord._id,
				filename: cfsRecord._id,
				contentType: oldGridFsFile.contentType,
				length: oldGridFsFile.length,
				chunkSize: oldGridFsFile.chunkSize,
				uploadDate: oldGridFsFile.uploadDate,
				aliases: null,
				metadata: null,
				md5: oldGridFsFile.md5,
			});

			oldChunkCollection.find({
				files_id: new Mongo.Collection.ObjectID(cfsRecord.copies.files.key),
			}).forEach((oldChunk) => {
				newChunkCollection.insert({
					_id: oldChunk._id,
					files_id: cfsRecord._id,
					n: oldChunk.n,
					data: oldChunk.data,
				});
			});

			Messages.find({
				$or: [{
					'urls.url': `https://open.rocket.chat/cfs/files/Files/${ cfsRecord._id }`,
				}, {
					'urls.url': `https://rocket.chat/cfs/files/Files/${ cfsRecord._id }`,
				}],
			}).forEach((message) => {
				for (const urlsItem of message.urls) {
					if (urlsItem.url === `https://open.rocket.chat/cfs/files/Files/${ cfsRecord._id }` || urlsItem.url === `https://rocket.chat/cfs/files/Files/${ cfsRecord._id }`) {
						urlsItem.url = Meteor.absoluteUrl() + url;
						if (urlsItem.parsedUrl && urlsItem.parsedUrl.pathname) {
							urlsItem.parsedUrl.pathname = `/${ url }`;
						}
						message.msg = message.msg.replace(`https://open.rocket.chat/cfs/files/Files/${ cfsRecord._id }`, Meteor.absoluteUrl() + url);
						message.msg = message.msg.replace(`https://rocket.chat/cfs/files/Files/${ cfsRecord._id }`, Meteor.absoluteUrl() + url);
					}
				}

				Messages.update({ _id: message._id }, {
					$set: {
						urls: message.urls,
						msg: message.msg,
					},
				});
			});

			oldFilesCollection.remove({ _id: cfsRecord._id });
			oldGridFSCollection.remove({ _id: oldGridFsFile._id });
			oldChunkCollection.remove({ files_id: new Mongo.Collection.ObjectID(cfsRecord.copies.files.key) });
		});

		return console.log('End of file migration');
	},
});


description: 'Whether the queue will be deleted when the number of consumers drops to zero',

keep keep keep add keep keep keep keep keep

description: 'Whether to assert the exchange exists before sending',

description: 'Whether to assert the queue exists before sending',

import type { IDataObject, IExecuteFunctions, ITriggerFunctions } from 'n8n-workflow';
import { sleep } from 'n8n-workflow';
import * as amqplib from 'amqplib';
import { formatPrivateKey } from '@utils/utilities';

export async function rabbitmqConnect(
	this: IExecuteFunctions | ITriggerFunctions,
	options: IDataObject,
): Promise<amqplib.Channel> {
	const credentials = await this.getCredentials('rabbitmq');

	const credentialKeys = ['hostname', 'port', 'username', 'password', 'vhost'];

	const credentialData: IDataObject = {};
	credentialKeys.forEach((key) => {
		credentialData[key] = credentials[key] === '' ? undefined : credentials[key];
	});

	const optsData: IDataObject = {};
	if (credentials.ssl === true) {
		credentialData.protocol = 'amqps';

		optsData.ca =
			credentials.ca === '' ? undefined : [Buffer.from(formatPrivateKey(credentials.ca as string))];
		if (credentials.passwordless === true) {
			optsData.cert =
				credentials.cert === ''
					? undefined
					: Buffer.from(formatPrivateKey(credentials.cert as string));
			optsData.key =
				credentials.key === ''
					? undefined
					: Buffer.from(formatPrivateKey(credentials.key as string));
			optsData.passphrase = credentials.passphrase === '' ? undefined : credentials.passphrase;
			optsData.credentials = amqplib.credentials.external();
		}
	}

	return await new Promise(async (resolve, reject) => {
		try {
			const connection = await amqplib.connect(credentialData, optsData);

			connection.on('error', (error: Error) => {
				reject(error);
			});

			const channel = (await connection.createChannel().catch(console.warn)) as amqplib.Channel;

			if (
				options.arguments &&
				((options.arguments as IDataObject).argument! as IDataObject[]).length
			) {
				const additionalArguments: IDataObject = {};
				((options.arguments as IDataObject).argument as IDataObject[]).forEach(
					(argument: IDataObject) => {
						additionalArguments[argument.key as string] = argument.value;
					},
				);
				options.arguments = additionalArguments;
			}

			resolve(channel);
		} catch (error) {
			reject(error);
		}
	});
}

export async function rabbitmqConnectQueue(
	this: IExecuteFunctions | ITriggerFunctions,
	queue: string,
	options: IDataObject,
): Promise<amqplib.Channel> {
	const channel = await rabbitmqConnect.call(this, options);

	return await new Promise(async (resolve, reject) => {
		try {
			await channel.assertQueue(queue, options);

			if (options.binding && ((options.binding as IDataObject).bindings! as IDataObject[]).length) {
				((options.binding as IDataObject).bindings as IDataObject[]).forEach(
					async (binding: IDataObject) => {
						await channel.bindQueue(
							queue,
							binding.exchange as string,
							binding.routingKey as string,
						);
					},
				);
			}

			resolve(channel);
		} catch (error) {
			reject(error);
		}
	});
}

export async function rabbitmqConnectExchange(
	this: IExecuteFunctions | ITriggerFunctions,
	exchange: string,
	type: string,
	options: IDataObject,
): Promise<amqplib.Channel> {
	const channel = await rabbitmqConnect.call(this, options);

	return await new Promise(async (resolve, reject) => {
		try {
			await channel.assertExchange(exchange, type, options);
			resolve(channel);
		} catch (error) {
			reject(error);
		}
	});
}

export class MessageTracker {
	messages: number[] = [];

	isClosing = false;

	received(message: amqplib.ConsumeMessage) {
		this.messages.push(message.fields.deliveryTag);
	}

	answered(message: amqplib.ConsumeMessage) {
		if (this.messages.length === 0) {
			return;
		}

		const index = this.messages.findIndex((value) => value !== message.fields.deliveryTag);
		this.messages.splice(index);
	}

	unansweredMessages() {
		return this.messages.length;
	}

	async closeChannel(channel: amqplib.Channel, consumerTag: string) {
		if (this.isClosing) {
			return;
		}
		this.isClosing = true;

		// Do not accept any new messages
		await channel.cancel(consumerTag);

		let count = 0;
		let unansweredMessages = this.unansweredMessages();

		// Give currently executing messages max. 5 minutes to finish before
		// the channel gets closed. If we would not do that, it would not be possible
		// to acknowledge messages anymore for which the executions were already running
		// when for example a new version of the workflow got saved. That would lead to
		// them getting delivered and processed again.
		while (unansweredMessages !== 0 && count++ <= 300) {
			await sleep(1000);
			unansweredMessages = this.unansweredMessages();
		}

		await channel.close();
		await channel.connection.close();
	}
}


import type {
	IRestApiContext,
	IN8nPrompts,
	IN8nValueSurveyData,
	IN8nPromptResponse,
} from '../Interface';
import { makeRestApiRequest, get, post } from '@/utils/apiUtils';
import { N8N_IO_BASE_URL, NPM_COMMUNITY_NODE_SEARCH_API_URL } from '@/constants';
import type { IN8nUISettings } from 'n8n-workflow';

export async function getSettings(context: IRestApiContext): Promise<IN8nUISettings> {
	return await makeRestApiRequest(context, 'GET', '/settings');
}

export async function getPromptsData(instanceId: string, userId: string): Promise<IN8nPrompts> {
	return await get(
		N8N_IO_BASE_URL,
		'/prompts',
		{},
		{ 'n8n-instance-id': instanceId, 'n8n-user-id': userId },
	);
}

export async function submitContactInfo(
	instanceId: string,
	userId: string,
	email: string,
): Promise<IN8nPromptResponse> {
	return await post(
		N8N_IO_BASE_URL,
		'/prompt',
		{ email },
		{ 'n8n-instance-id': instanceId, 'n8n-user-id': userId },
	);
}

export async function submitValueSurvey(
	instanceId: string,
	userId: string,
	params: IN8nValueSurveyData,
): Promise<IN8nPromptResponse> {
	return await post(N8N_IO_BASE_URL, '/value-survey', params, {
		'n8n-instance-id': instanceId,
		'n8n-user-id': userId,
	});
}

export async function getAvailableCommunityPackageCount(): Promise<number> {
	const response = await get(
		NPM_COMMUNITY_NODE_SEARCH_API_URL,
		'search?q=keywords:n8n-community-node-package',
	);

	return response.total || 0;
}


import { CredentialTypes } from '@/CredentialTypes';
import { Container } from 'typedi';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../shared/mocking';

describe('CredentialTypes', () => {
	const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
		loadedCredentials: {
			fakeFirstCredential: {
				type: {
					name: 'fakeFirstCredential',
					displayName: 'Fake First Credential',
					properties: [],
				},
				sourcePath: '',
			},
			fakeSecondCredential: {
				type: {
					name: 'fakeSecondCredential',
					displayName: 'Fake Second Credential',
					properties: [],
				},
				sourcePath: '',
			},
		},
	});

	const credentialTypes = Container.get(CredentialTypes);

	test('Should throw error when calling invalid credential name', () => {
		expect(() => credentialTypes.getByName('fakeThirdCredential')).toThrowError();
	});

	test('Should return correct credential type for valid name', () => {
		const mockedCredentialTypes = mockNodesAndCredentials.loadedCredentials;
		expect(credentialTypes.getByName('fakeFirstCredential')).toStrictEqual(
			mockedCredentialTypes.fakeFirstCredential.type,
		);
	});
});


import type { OptionsWithUri } from 'request';
import type {
	IDataObject,
	IExecuteFunctions,
	ILoadOptionsFunctions,
	IPollFunctions,
	JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { getGoogleAccessToken } from '../../../GenericFunctions';

export async function apiRequest(
	this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
	method: string,
	resource: string,
	body: IDataObject = {},
	qs: IDataObject = {},
	uri?: string,
	headers: IDataObject = {},
	option: IDataObject = {},
) {
	const authenticationMethod = this.getNodeParameter(
		'authentication',
		0,
		'serviceAccount',
	) as string;
	const options: OptionsWithUri = {
		headers: {
			'Content-Type': 'application/json',
		},
		method,
		body,
		qs,
		uri: uri || `https://sheets.googleapis.com${resource}`,
		json: true,
		...option,
	};
	try {
		if (Object.keys(headers).length !== 0) {
			options.headers = Object.assign({}, options.headers, headers);
		}
		if (Object.keys(body).length === 0) {
			delete options.body;
		}

		if (authenticationMethod === 'serviceAccount') {
			const credentials = await this.getCredentials('googleApi');

			const { access_token } = await getGoogleAccessToken.call(this, credentials, 'sheetV2');

			options.headers!.Authorization = `Bearer ${access_token}`;

			return await this.helpers.request(options);
		} else if (authenticationMethod === 'triggerOAuth2') {
			return await this.helpers.requestOAuth2.call(this, 'googleSheetsTriggerOAuth2Api', options);
		} else {
			return await this.helpers.requestOAuth2.call(this, 'googleSheetsOAuth2Api', options);
		}
	} catch (error) {
		if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') {
			error.statusCode = '401';
		}

		if (error.message.includes('PERMISSION_DENIED')) {
			const message = `Missing permissions for Google Sheet, ${error.message}}`;
			const details = error.description ? ` Details of the error: ${error.description}.` : '';
			const description = `Please check that the account you're using has the right permissions. (If you're trying to modify the sheet, you'll need edit access.)${details}`;
			throw new NodeApiError(this.getNode(), error as JsonObject, { message, description });
		}

		throw new NodeApiError(this.getNode(), error as JsonObject);
	}
}

export async function apiRequestAllItems(
	this: IExecuteFunctions | ILoadOptionsFunctions,
	propertyName: string,
	method: string,
	endpoint: string,
	body: IDataObject = {},
	query: IDataObject = {},
	uri?: string,
) {
	const returnData: IDataObject[] = [];

	let responseData;
	query.maxResults = 100;
	const url = uri ? uri : `https://sheets.googleapis.com${method}`;
	do {
		responseData = await apiRequest.call(this, method, endpoint, body, query, url);
		query.pageToken = responseData.nextPageToken;
		returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
	} while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== '');

	return returnData;
}


const channel = await rabbitmqConnect.call(this, options);

return await new Promise(async (resolve, reject) => {

if (options.binding && ((options.binding as IDataObject).bindings! as IDataObject[]).length) {

((options.binding as IDataObject).bindings as IDataObject[]).forEach(