Merge branch 'develop'

* develop: (103 commits)
  Tokenize message on message render to prevent re processing
  using branding image from main app
  Remove unecessary logs
  Do not load all settings to process.env
  Fix preview of images in mobile
  re order settings
  code formatting
  Enforce data in body params
  Get integration name from body
  Set user role in integration update too
  Added new color variables to the theme editor
  embarrassing mistake
  make sample data into array
  fix livechat triggers not triggering
  added livechat branding
  Added infinite scroll to files list
  Allow searching for logged in user in userAutocomplete
  Added "Jump to" and infinite scroll to message search results
  Enable triggers in messages to users
  Rename integration api routes, add apis remove, info and sample
  ...
pull/1898/head 0.11.0
Gabriel Engel 10 years ago
commit 6e51f5b374
  1. 1
      .gitignore
  2. 1
      .meteor/packages
  3. 9
      .meteor/versions
  4. 2
      Dockerfile
  5. 27
      client/methods/saveRoomName.coffee
  6. 26
      client/startup/startup.coffee
  7. 0
      example-build.sh
  8. 13
      i18n/en.i18n.json
  9. 2
      install.sh
  10. 4
      lib/fileUpload.coffee
  11. 7
      packages/rocketchat-authorization/server/startup.coffee
  12. 13
      packages/rocketchat-channel-settings-mail-messages/client/lib/startup.coffee
  13. 24
      packages/rocketchat-channel-settings-mail-messages/client/stylesheets/mail-messages.less
  14. 10
      packages/rocketchat-channel-settings-mail-messages/client/views/channelSettingsMailMessages.coffee
  15. 8
      packages/rocketchat-channel-settings-mail-messages/client/views/channelSettingsMailMessages.html
  16. 123
      packages/rocketchat-channel-settings-mail-messages/client/views/mailMessagesInstructions.coffee
  17. 55
      packages/rocketchat-channel-settings-mail-messages/client/views/mailMessagesInstructions.html
  18. 18
      packages/rocketchat-channel-settings-mail-messages/i18n/en.i18n.json
  19. 51
      packages/rocketchat-channel-settings-mail-messages/package.js
  20. 3
      packages/rocketchat-channel-settings-mail-messages/server/lib/startup.coffee
  21. 55
      packages/rocketchat-channel-settings-mail-messages/server/methods/mailMessages.coffee
  22. 29
      packages/rocketchat-channel-settings/client/lib/ChannelSettings.coffee
  23. 14
      packages/rocketchat-channel-settings/client/startup/tabBar.coffee
  24. 12
      packages/rocketchat-channel-settings/client/startup/trackSettingsChange.coffee
  25. 22
      packages/rocketchat-channel-settings/client/stylesheets/channel-settings.less
  26. 104
      packages/rocketchat-channel-settings/client/views/channelSettings.coffee
  27. 78
      packages/rocketchat-channel-settings/client/views/channelSettings.html
  28. 21
      packages/rocketchat-channel-settings/i18n/en.i18n.json
  29. 7
      packages/rocketchat-channel-settings/package.js
  30. 47
      packages/rocketchat-channel-settings/server/functions/changeRoomType.coffee
  31. 29
      packages/rocketchat-channel-settings/server/functions/saveRoomName.coffee
  32. 5
      packages/rocketchat-channel-settings/server/functions/saveRoomTopic.coffee
  33. 8
      packages/rocketchat-channel-settings/server/functions/saveRoomType.coffee
  34. 33
      packages/rocketchat-channel-settings/server/methods/saveRoomSettings.coffee
  35. 3
      packages/rocketchat-channel-settings/server/models/Messages.coffee
  36. 54
      packages/rocketchat-file/file.server.coffee
  37. 11
      packages/rocketchat-highlight/highlight.coffee
  38. 10
      packages/rocketchat-integrations/client/route.coffee
  39. 50
      packages/rocketchat-integrations/client/views/integrations.html
  40. 7
      packages/rocketchat-integrations/client/views/integrationsIncoming.coffee
  41. 2
      packages/rocketchat-integrations/client/views/integrationsIncoming.html
  42. 4
      packages/rocketchat-integrations/client/views/integrationsNew.html
  43. 167
      packages/rocketchat-integrations/client/views/integrationsOutgoing.coffee
  44. 100
      packages/rocketchat-integrations/client/views/integrationsOutgoing.html
  45. 15
      packages/rocketchat-integrations/package.js
  46. 133
      packages/rocketchat-integrations/server/api/api.coffee
  47. 19
      packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.coffee
  48. 4
      packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.coffee
  49. 18
      packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.coffee
  50. 69
      packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.coffee
  51. 13
      packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.coffee
  52. 81
      packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.coffee
  53. 110
      packages/rocketchat-integrations/server/triggers.coffee
  54. 11
      packages/rocketchat-lib/client/lib/roomExit.coffee
  55. 26
      packages/rocketchat-lib/lib/Message.coffee
  56. 0
      packages/rocketchat-lib/lib/MessageTypes.coffee
  57. 5
      packages/rocketchat-lib/package.js
  58. 2
      packages/rocketchat-lib/server/functions/sendMessage.coffee
  59. 9
      packages/rocketchat-lib/server/functions/settings.coffee
  60. 4
      packages/rocketchat-lib/server/methods/updateUser.coffee
  61. 12
      packages/rocketchat-lib/server/models/Messages.coffee
  62. 21
      packages/rocketchat-lib/server/models/Rooms.coffee
  63. 2
      packages/rocketchat-lib/server/models/Subscriptions.coffee
  64. 18
      packages/rocketchat-lib/server/models/Users.coffee
  65. 9
      packages/rocketchat-lib/server/startup/settings.coffee
  66. 2
      packages/rocketchat-livechat/app/.meteor/packages
  67. 1
      packages/rocketchat-livechat/app/.meteor/versions
  68. 2
      packages/rocketchat-livechat/app/client/lib/chatMessages.coffee
  69. 1
      packages/rocketchat-livechat/app/client/lib/collections.coffee
  70. 2
      packages/rocketchat-livechat/app/client/lib/fromApp/RoomHistoryManager.coffee
  71. 24
      packages/rocketchat-livechat/app/client/lib/triggers.js
  72. 2
      packages/rocketchat-livechat/app/client/routes/router.coffee
  73. 2
      packages/rocketchat-livechat/app/client/stylesheets/_variables.less
  74. 50
      packages/rocketchat-livechat/app/client/stylesheets/main.less
  75. 8
      packages/rocketchat-livechat/app/client/views/livechatWindow.html
  76. 40
      packages/rocketchat-livechat/app/client/views/livechatWindow.js
  77. 6
      packages/rocketchat-livechat/app/client/views/messages.html
  78. 50
      packages/rocketchat-livechat/app/client/views/register.coffee
  79. 11
      packages/rocketchat-livechat/app/client/views/register.html
  80. 68
      packages/rocketchat-livechat/app/client/views/register.js
  81. 3
      packages/rocketchat-livechat/app/i18n/en.i18n.json
  82. 2
      packages/rocketchat-livechat/assets/rocket-livechat.js
  83. 1
      packages/rocketchat-livechat/client/collections/AgentUsers.js
  84. 0
      packages/rocketchat-livechat/client/collections/LivechatDepartment.js
  85. 1
      packages/rocketchat-livechat/client/collections/LivechatDepartmentAgents.js
  86. 0
      packages/rocketchat-livechat/client/collections/LivechatTrigger.js
  87. 12
      packages/rocketchat-livechat/client/route.js
  88. 26
      packages/rocketchat-livechat/client/stylesheets/livechat.less
  89. 69
      packages/rocketchat-livechat/client/views/app/livechatDepartmentForm.html
  90. 89
      packages/rocketchat-livechat/client/views/app/livechatDepartmentForm.js
  91. 5
      packages/rocketchat-livechat/client/views/app/livechatDepartments.js
  92. 2
      packages/rocketchat-livechat/client/views/app/livechatUsers.js
  93. 2
      packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html
  94. 4
      packages/rocketchat-livechat/client/views/sideNav/livechatFlex.js
  95. 1
      packages/rocketchat-livechat/config.js
  96. 7
      packages/rocketchat-livechat/i18n/en.i18n.json
  97. 14
      packages/rocketchat-livechat/package.js
  98. 21
      packages/rocketchat-livechat/server/lib/getNextAgent.js
  99. 5
      packages/rocketchat-livechat/server/methods/registerGuest.js
  100. 4
      packages/rocketchat-livechat/server/methods/saveDepartment.js
  101. Some files were not shown because too many files have changed in this diff Show More

1
.gitignore vendored

@ -66,3 +66,4 @@ tramp
ecosystem.json
pm2.json
settings.json
build.sh

@ -41,6 +41,7 @@ rocketchat:lib
rocketchat:authorization
rocketchat:autolinker
rocketchat:channel-settings
rocketchat:channel-settings-mail-messages
rocketchat:colors
rocketchat:custom-oauth
rocketchat:emojione

@ -7,7 +7,7 @@ accounts-oauth@1.1.8
accounts-password@1.1.4
accounts-twitter@1.0.6
alanning:roles@1.2.14
aldeed:simple-schema@1.5.1
aldeed:simple-schema@1.5.2
arunoda:streams@0.1.17
autoupdate@1.2.4
babel-compiler@5.8.24_1
@ -25,7 +25,7 @@ cfs:http-methods@0.0.30
check@1.1.0
chrismbeckett:toastr@2.1.2_1
coffeescript@1.0.11
cosmos:browserify@0.9.2
cosmos:browserify@0.9.3
dandv:caret-position@2.1.1
ddp@1.2.2
ddp-client@1.2.1
@ -39,7 +39,7 @@ ecmascript@0.1.6
ecmascript-runtime@0.2.6
ejson@1.0.7
email@1.0.8
emojione:emojione@1.5.2
emojione:emojione@2.0.0
facebook@1.2.2
fastclick@1.0.7
francocatena:status@1.5.0
@ -75,7 +75,7 @@ livedata@1.0.15
localstorage@1.0.5
logging@1.0.8
matb33:collection-hooks@0.8.1
mdg:validation-error@0.1.0
mdg:validation-error@0.2.0
meteor@1.1.10
meteor-base@1.0.1
meteor-developer@1.1.5
@ -125,6 +125,7 @@ rocketchat:assets@0.0.1
rocketchat:authorization@0.0.1
rocketchat:autolinker@0.0.1
rocketchat:channel-settings@0.0.1
rocketchat:channel-settings-mail-messages@0.0.1
rocketchat:colors@0.0.1
rocketchat:cors@0.0.1
rocketchat:custom-oauth@1.0.0

@ -16,7 +16,7 @@ RUN gpg --keyserver ha.pool.sks-keyservers.net --recv-keys 0E163286C20D07B9787EB
WORKDIR /app
RUN curl -fSL "https://s3.amazonaws.com/rocketchatbuild/develop.rocket.chat-v.latest.tgz" -o rocket.chat.tgz \
RUN curl -fSL "https://s3.amazonaws.com/rocketchatbuild/rocket.chat-develop.tgz" -o rocket.chat.tgz \
&& tar zxvf ./rocket.chat.tgz \
&& rm ./rocket.chat.tgz \
&& cd /app/bundle/programs/server \

@ -1,27 +0,0 @@
Meteor.methods
saveRoomName: (rid, name) ->
if not Meteor.userId()
throw new Meteor.Error 203, t('User_logged_out')
room = ChatRoom.findOne rid
if room.u._id isnt Meteor.userId() or room.t not in ['c', 'p']
throw new Meteor.Error 403, t('Not allowed')
if RocketChat.settings.get 'UTF8_Names_Slugify'
name = _.slugify name
if name is room.name
return
ChatRoom.update rid,
$set:
name: name
ChatSubscription.update
rid: rid
,
$set:
name: name
return name

@ -10,7 +10,7 @@ Meteor.startup ->
window.lastMessageWindow = {}
window.lastMessageWindowHistory = {}
@defaultUserLanguage = ->
@defaultAppLanguage = ->
lng = window.navigator.userLanguage || window.navigator.language || 'en'
# Fix browsers having all-lowercase language settings eg. pt-br, en-us
re = /([a-z]{2}-)([a-z]{2})/
@ -18,6 +18,9 @@ Meteor.startup ->
lng = lng.replace re, (match, parts...) -> return parts[0] + parts[1].toUpperCase()
return lng
@defaultUserLanguage = ->
return RocketChat.settings.get('Language') || defaultAppLanguage()
loadedLaguages = []
setLanguage = (language) ->
@ -35,17 +38,14 @@ Meteor.startup ->
Function(localeFn)()
moment.locale(language)
Tracker.autorun (c) ->
if Meteor.user()?.language?
c.stop()
if localStorage.getItem('userLanguage') isnt Meteor.user().language
localStorage.setItem("userLanguage", Meteor.user().language)
setLanguage Meteor.user().language
if isRtl localStorage.getItem "userLanguage"
$('html').addClass "rtl"
Meteor.subscribe("userData", () ->
userLanguage = Meteor.user()?.language
userLanguage ?= defaultUserLanguage()
userLanguage = localStorage.getItem("userLanguage")
userLanguage ?= defaultUserLanguage()
if localStorage.getItem('userLanguage') isnt userLanguage
localStorage.setItem('userLanguage', userLanguage)
if isRtl localStorage.getItem 'userLanguage'
$('html').addClass "rtl"
setLanguage userLanguage
setLanguage userLanguage
)

@ -76,6 +76,7 @@
"API_Embed" : "Embed",
"API_EmbedDisabledFor" : "Disable Embed for Users",
"API_EmbedDisabledFor_Description" : "Comma-separated list of usernames",
"Archive" : "Archive",
"are_also_typing" : "are also typing",
"are_typing" : "are typing",
"Are_you_sure" : "Are you sure?",
@ -139,6 +140,8 @@
"Disable_New_Room_Notification" : "Disable New Room Notification",
"Do_you_want_to_change_to_s_question" : "Do you want to change to <strong>%s</strong>?",
"Drop_to_upload_file" : "Drop to upload file",
"Duplicate_archived_channel_name" : "An archived Channel with name '%s' exists",
"Duplicate_archived_private_group_name" : "An archived Private Group with name '%s' exists",
"Duplicate_channel_name" : "A Channel with name '%s' exists",
"Duplicate_private_group_name" : "A Private Group with name '%s' exists",
"E-mail" : "E-mail",
@ -200,6 +203,7 @@
"Invalid_name" : "The name must not be empty",
"Invalid_pass" : "The password must not be empty",
"Invalid_room_name" : "<strong>%s</strong> is not a valid room name,<br/> use only letters, numbers and dashes",
"Invalid_room_type" : "<strong>%s</strong> is not a valid room type.",
"Invalid_Secret_URL" : "Invalid Secret URL",
"Invalid_secret_URL_message" : "The URL provided is invalid.",
"invisible" : "invisible",
@ -216,8 +220,8 @@
"italics" : "italics",
"join" : "Join",
"Join_the_Community" : "Join the Community",
"Jump_to_recent_messages" : "Jump to recent messages",
"Jump_to_message" : "Jump to message",
"Jump_to_recent_messages" : "Jump to recent messages",
"Language" : "Language",
"Language_Version" : "English Version",
"Last_login" : "Last login",
@ -314,6 +318,7 @@
"No_groups_yet" : "You have no private groups yet.",
"No_livechats" : "You have no livechats.",
"No_permission_to_view_room" : "You don't have permission to view this room",
"No_results_found" : "No results found",
"no_tokens_for_this_user" : "There are no tokens for this user",
"No_user_with_username_%s_was_found" : "No user with username <strong>\"%s\"</strong> was found!",
"Not_allowed" : "Not allowed",
@ -382,10 +387,12 @@
"Restart" : "Restart",
"Restart_the_server" : "Restart the server",
"Room" : "Room",
"Room_archived" : "Room archived",
"Room_has_been_deleted" : "Room has been deleted",
"Room_name_changed" : "Room name changed to: <em>__room_name__</em> by <em>__user_by__</em>",
"Room_name_changed_successfully" : "Room name changed successfully",
"Room_not_found" : "Room not found",
"Room_unarchived" : "Room unarchived",
"Room_uploaded_file_list" : "Files List",
"Room_uploaded_file_list_empty" : "No files available.",
"room_user_count" : "%s users",
@ -425,6 +432,7 @@
"Settings_updated" : "Settings updated",
"Should_be_a_URL_of_an_image" : "Should be a URL of an image.",
"Should_exists_a_user_with_this_username" : "The user must already exist.",
"Showing_archived_results" : "<p>Showing <b>%s</b> archived results</p>",
"Showing_online_users" : "Showing <b>__total_online__</b> of __total__ users",
"Showing_results" : "<p>Showing <b>%s</b> results</p>",
"Silence" : "Silence",
@ -476,6 +484,7 @@
"There_is_no_integrations" : "There is no integrations",
"This_is_a_push_test_messsage" : "This is a push test messsage",
"True" : "True",
"Unarchive" : "Unarchive",
"Unmute_user" : "Unmute user",
"Unnamed" : "Unnamed",
"Unread_Rooms" : "Unread Rooms",
@ -547,4 +556,4 @@
"Your_entry_has_been_deleted" : "Your entry has been deleted.",
"Your_Open_Source_solution" : "Your own Open Source chat solution",
"Your_push_was_sent_to_s_devices" : "Your push was sent to %s devices"
}
}

@ -8,7 +8,7 @@ if [ "$1" == "development" ]; then
fi
cd $ROOTPATH
curl -fSL "https://s3.amazonaws.com/rocketchatbuild/demo.rocket.chat-v.latest.tgz" -o rocket.chat.tgz
curl -fSL "https://s3.amazonaws.com/rocketchatbuild/rocket.chat-develop.tgz" -o rocket.chat.tgz
tar zxf rocket.chat.tgz && rm rocket.chat.tgz
cd $ROOTPATH/bundle/programs/server
npm install

@ -73,6 +73,10 @@ if UploadFS?
uid = cookie.get('rc_uid', rawCookies) if rawCookies?
token = cookie.get('rc_token', rawCookies) if rawCookies?
if not uid?
uid = req.query.rc_uid
token = req.query.rc_token
unless uid and token and RocketChat.models.Users.findOneByIdAndLoginToken(uid, token)
res.writeHead 403
return false

@ -27,6 +27,9 @@ Meteor.startup ->
{ _id: 'edit-other-user-info',
roles : ['admin']}
{ _id: 'edit-other-user-password',
roles : ['admin']}
{ _id: 'assign-admin-role',
roles : ['admin']}
@ -100,14 +103,14 @@ Meteor.startup ->
roles : ['admin']}
{ _id: 'manage-integrations',
roles : ['admin']}
roles : ['admin', 'bot']}
]
#alanning:roles
roles = _.pluck(Roles.getAllRoles().fetch(), 'name');
for permission in permissions
RocketChat.models.Permissions.upsert( permission._id, {$setOnInsert : permission })
RocketChat.models.Permissions.upsert( permission._id, {$set: permission })
for role in permission.roles
unless role in roles
Roles.createRole role

@ -0,0 +1,13 @@
Meteor.startup ->
RocketChat.ChannelSettings.addOption
id: 'mail-messages'
template: 'channelSettingsMailMessages'
validation: ->
return RocketChat.authz.hasAllPermission('mail-messages')
RocketChat.callbacks.add 'roomExit', (mainNode) ->
messagesBox = $('.messages-box')
if messagesBox.get(0)?
instance = Blaze.getView(messagesBox.get(0))?.templateInstance()
instance?.resetSelection(false)
, RocketChat.callbacks.priority.MEDIUM, 'room-exit-mail-messages'

@ -0,0 +1,24 @@
.flex-tab {
.mail-message {
form {
margin-top: 20px;
.input-line.double-col {
margin-bottom: 20px;
label {
line-height: 15px;
}
div {
line-height: 15px;
i.octicon {
font-size: 13px;
opacity: 0.4;
vertical-align: top;
}
}
}
}
}
}

@ -0,0 +1,10 @@
Template.channelSettingsMailMessages.events
'click button.mail-messages': (e, t) ->
Session.set 'channelSettingsMailMessages', Session.get('openedRoom')
RocketChat.TabBar.setTemplate('mailMessagesInstructions')
view = Blaze.getView($('.messages-box')[0])
view?.templateInstance?().resetSelection?(true)
Template.channelSettingsMailMessages.onCreated ->
view = Blaze.getView($('.messages-box')[0])
view?.templateInstance?().resetSelection?(false)

@ -0,0 +1,8 @@
<template name="channelSettingsMailMessages">
<li>
<label>{{_ "Mail_Messages"}}</label>
<div>
<button type="button" class="button primary mail-messages">{{_ "Choose_messages"}}</button>
</div>
</li>
</template>

@ -0,0 +1,123 @@
Template.mailMessagesInstructions.helpers
name: ->
return Meteor.user().name
email: ->
return Meteor.user().emails?[0]?.address
roomName: ->
return ChatRoom.findOne(Session.get('openedRoom'))?.name
erroredEmails: ->
return Template.instance()?.erroredEmails.get().join(', ')
autocompleteSettings: ->
return {
limit: 10
# inputDelay: 300
rules: [
{
# @TODO maybe change this 'collection' and/or template
collection: 'CachedChannelList'
subscription: 'userAutocomplete'
field: 'username'
template: Template.userSearch
noMatchTemplate: Template.userSearchEmpty
matchAll: true
filter:
exceptions: Template.instance().selectedUsers.get()
selector: (match) ->
return { username: match }
sort: 'username'
}
]
}
selectedUsers: ->
return Template.instance().selectedUsers.get()
Template.mailMessagesInstructions.events
'click .cancel': (e, t) ->
t.reset()
'click .send': (e, t) ->
t.$('.error').hide()
$btn = t.$('button.send')
oldBtnValue = $btn.html()
$btn.html(TAPi18n.__('Sending'))
selectedMessages = $('.messages-box .message.selected')
error = false
if selectedMessages.length is 0
t.$('.error-select').show()
error = true
if t.$('input[name=to_emails]').val().trim()
rfcMailPatternWithName = /^(?:.*<)?([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)(?:>?)$/
emails = t.$('input[name=to_emails]').val().trim().split(',')
erroredEmails = []
for email in emails
unless rfcMailPatternWithName.test email.trim()
erroredEmails.push email.trim()
t.erroredEmails.set erroredEmails
if erroredEmails.length > 0
t.$('.error-invalid-emails').show()
error = true
else if not t.selectedUsers.get().length
t.$('.error-missing-to').show()
error = true
if error
$btn.html(oldBtnValue)
else
data =
rid: Session.get('openedRoom')
to_users: t.selectedUsers.get()
to_emails: t.$('input[name=to_emails]').val().trim()
subject: t.$('input[name=subject]').val().trim()
messages: selectedMessages.map((i, message) -> return message.id).toArray()
language: localStorage.getItem('userLanguage')
Meteor.call 'mailMessages', data, (err, result) ->
$btn.html(oldBtnValue)
if err?
return toastr.error(err.reason or err.message)
console.log(result)
toastr.success(TAPi18n.__('Your_email_has_been_queued_for_sending'))
t.reset()
'click .select-all': (e, t) ->
t.$('.error-select').hide()
view = Blaze.getView($('.messages-box')[0])
view?.templateInstance?().selectedMessages = _.pluck(ChatMessage.find({rid: Session.get('openedRoom')})?.fetch(), '_id')
$(".messages-box .message").addClass('selected')
'autocompleteselect #to_users': (event, instance, doc) ->
instance.selectedUsers.set instance.selectedUsers.get().concat doc.username
event.currentTarget.value = ''
event.currentTarget.focus()
'click .remove-to-user': (e, instance) ->
self = @
users = Template.instance().selectedUsers.get()
users = _.reject Template.instance().selectedUsers.get(), (_id) ->
return _id is self.valueOf()
Template.instance().selectedUsers.set(users)
$('#to_users').focus()
Template.mailMessagesInstructions.onCreated ->
@autoCompleteCollection = new Mongo.Collection null
@selectedUsers = new ReactiveVar []
@erroredEmails = new ReactiveVar []
@reset = =>
@selectedUsers.set []
RocketChat.TabBar.setTemplate('channelSettings')
view = Blaze.getView($('.messages-box')[0])
view?.templateInstance?().resetSelection?(false)
@autorun =>
if Session.get('channelSettingsMailMessages') isnt Session.get('openedRoom')
this.reset()

@ -0,0 +1,55 @@
<template name="mailMessagesInstructions">
<div class="content">
<div class="list-view mail-message">
<div class="status">
<h2>{{_ "Mail_Messages"}}</h2>
</div>
<p>{{_ "Mail_Messages_Instructions"}}</p>
<form>
<fieldset>
<div class="input-line double-col">
<label>{{_ "From"}}</label>
<div>{{name}}</div>
<div>{{email}}</div>
</div>
<div class="input-line double-col">
<label>{{_ "To_users"}}</label>
<div>
{{> inputAutocomplete settings=autocompleteSettings id="to_users" name="to_users" class="search" autocomplete="off"}}
<ul class="selected-users">
{{#each selectedUsers}}
<li>{{.}} <i class="icon-cancel remove-to-user"></i></li>
{{/each}}
</ul>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Additional_emails"}}</label>
<div>
<input type="text" name="to_emails" value="" />
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Subject"}}</label>
<div>
<input type="text" name="subject" value="{{_ "Mail_Messages_Subject" roomName}}" />
</div>
</div>
</fieldset>
</form>
<div class="error error-missing-to alert alert-danger" style="display: none">
{{_ "Mail_Message_Missing_to"}}
</div>
<div class="error error-invalid-emails alert alert-danger" style="display: none">
{{_ "Mail_Message_Invalid_emails" erroredEmails}}
</div>
<div class="error error-select alert alert-danger" style="display: none">
{{{_ "Mail_Message_No_messages_selected_select_all"}}}
</div>
<p style="margin-top: 30px">
<button type="button" class="button secondary cancel">{{_ "Cancel"}}</button>
<button type="button" class="button primary send">{{_ "Send"}}</button>
</p>
</div>
</div>
</template>

@ -0,0 +1,18 @@
{
"Additional_emails" : "Additional E-mails",
"Body" : "Body",
"Cancel" : "Cancel",
"Choose_messages" : "Choose messages",
"From" : "From",
"Mail_Message_Invalid_emails" : "You have provided one or more invalid e-mails: %s",
"Mail_Message_Missing_to" : "You must select one or more users or provide one or more e-mail addresses, separated by commas.",
"Mail_Message_No_messages_selected_select_all" : "You haven't selected any messages. Would you like to <a href='#' class='select-all'>select all</a> visible messages?",
"Mail_Messages" : "Mail Messages",
"Mail_Messages_Instructions" : "Choose which messages you want to send via e-mail by clicking the messages",
"Mail_Messages_Subject" : "Here's a selected portion of %s messages",
"Send" : "Send",
"Sending" : "Sending...",
"Subject" : "Subject",
"To_users" : "To Users",
"Your_email_has_been_queued_for_sending" : "Your email has been queued for sending"
}

@ -0,0 +1,51 @@
Package.describe({
name: 'rocketchat:channel-settings-mail-messages',
version: '0.0.1',
summary: 'Channel Settings - Mail Messages',
git: ''
});
Package.onUse(function(api) {
api.versionsFrom('1.0');
api.use([
'coffeescript',
'templating',
'reactive-var',
'less@2.5.0',
'rocketchat:lib@0.0.1',
'rocketchat:channel-settings',
'momentjs:moment'
]);
api.addFiles([
'client/lib/startup.coffee',
'client/stylesheets/mail-messages.less',
'client/views/channelSettingsMailMessages.html',
'client/views/channelSettingsMailMessages.coffee',
'client/views/mailMessagesInstructions.html',
'client/views/mailMessagesInstructions.coffee'
], 'client');
api.addFiles([
'server/lib/startup.coffee',
'server/methods/mailMessages.coffee'
], 'server');
// TAPi18n
var _ = Npm.require('underscore');
var fs = Npm.require('fs');
tapi18nFiles = _.compact(_.map(fs.readdirSync('packages/rocketchat-channel-settings-mail-messages/i18n'), function(filename) {
if (fs.statSync('packages/rocketchat-channel-settings-mail-messages/i18n/' + filename).size > 16) {
return 'i18n/' + filename;
}
}));
api.use('tap:i18n@1.6.1');
api.imply('tap:i18n');
api.addFiles(tapi18nFiles);
});
Package.onTest(function(api) {
});

@ -0,0 +1,3 @@
Meteor.startup ->
permission = { _id: 'mail-messages', roles : [ 'admin' ] }
RocketChat.models.Permissions.upsert( permission._id, { $setOnInsert : permission })

@ -0,0 +1,55 @@
Meteor.methods
'mailMessages': (data) ->
if not Meteor.userId()
throw new Meteor.Error('invalid-user', "[methods] mailMessages -> Invalid user")
check(data, Match.ObjectIncluding({ rid: String, to_users: [ String ], to_emails: String, subject: String, messages: [ String ], language: String }))
room = Meteor.call 'canAccessRoom', data.rid, Meteor.userId()
unless room
throw new Meteor.Error('invalid-room', "[methods] mailMessages -> Invalid room")
unless RocketChat.authz.hasPermission(Meteor.userId(), 'mail-messages')
throw new Meteor.Error 'not-authorized'
rfcMailPatternWithName = /^(?:.*<)?([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)(?:>?)$/
emails = _.compact(data.to_emails.trim().split(','))
missing = []
if data.to_users.length > 0
for username in data.to_users
user = RocketChat.models.Users.findOneByUsername(username)
if user?.emails?[0]?.address
emails.push user.emails[0].address
else
missing.push username
console.log emails
for email in emails
unless rfcMailPatternWithName.test email.trim()
throw new Meteor.Error('invalid-email', "[methods] mailMessages -> Invalid e-mail #{email}")
user = Meteor.user()
name = user.name
email = user.emails?[0]?.address
if data.language isnt 'en'
localeFn = Meteor.call 'loadLocale', data.language
if localeFn
Function(localeFn)()
html = ""
RocketChat.models.Messages.findByRoomIdAndMessageIds(data.rid, data.messages, { sort: { ts: 1 } }).forEach (message) ->
dateTime = moment(message.ts).locale(data.language).format('L LT')
html += "<p style='margin-bottom: 5px'><b>#{message.u.username}</b> <span style='color: #aaa; font-size: 12px'>#{dateTime}</span><br />" + RocketChat.Message.parse(message, data.language) + "</p>"
Meteor.defer ->
Email.send
to: emails
from: RocketChat.settings.get('From_Email')
replyTo: email
subject: data.subject
html: html
console.log 'Sending email to ' + emails.join(', ')
return { success: true, missing: missing }

@ -0,0 +1,29 @@
RocketChat.ChannelSettings = new class
options = new ReactiveVar {}
###
# Adds an option in Channel Settings
# @config (object)
# id: option id (required)
# template (string): template name to render (required)
# validation (function): if option should be displayed
###
addOption = (config) ->
unless config?.id
throw new Meteor.Error "ChannelSettings-addOption-error", "Option id was not informed."
Tracker.nonreactive ->
opts = options.get()
opts[config.id] = config
options.set opts
getOptions = ->
allOptions = _.toArray options.get()
allowedOptions = _.compact _.map allOptions, (option) ->
if not option.validation? or option.validation()
return option
return _.sortBy allowedOptions, 'order'
addOption: addOption
getOptions: getOptions

@ -2,11 +2,11 @@ Meteor.startup ->
RocketChat.callbacks.add 'enter-room', (subscription) ->
if RocketChat.authz.hasAtLeastOnePermission('edit-room', subscription?.rid)
RocketChat.TabBar.addButton
id: 'channel-settings'
i18nTitle: 'Channel_Settings'
icon: 'octicon octicon-gear'
template: 'channelSettings'
order: 0
RocketChat.TabBar.addButton
id: 'channel-settings'
i18nTitle: 'Room_Info'
icon: 'octicon octicon-info'
template: 'channelSettings'
order: 0
, RocketChat.callbacks.priority.MEDIUM, 'enter-room-tabbar-channel-settings'

@ -13,3 +13,15 @@ Meteor.startup ->
return msg
RocketChat.callbacks.add 'streamMessage', roomSettingsChangedCallback, RocketChat.callbacks.priority.HIGH
roomNameChangedCallback = (msg) ->
Tracker.nonreactive ->
if msg.t is 'r'
if Session.get('openedRoom') is msg.rid
type = if FlowRouter.current().route.name is 'channel' then 'c' else 'p'
RoomManager.close type + FlowRouter.getParam('name')
FlowRouter.go FlowRouter.current().route.name, name: msg.msg
return msg
RocketChat.callbacks.add 'streamMessage', roomNameChangedCallback, RocketChat.callbacks.priority.HIGH

@ -1,11 +1,25 @@
.flex-tab {
.channel-settings {
margin-top: 60px;
padding: 20px;
ul {
li {
margin-bottom: 20px;
}
}
form {
label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
div span {
font-size: 14px;
i.octicon {
font-size: 12px;
vertical-align: middle;
margin-left: 3px;
}
}
}
@ -13,5 +27,9 @@
margin-top: 30px;
text-align: center;
}
[data-edit] {
cursor: pointer;
}
}
}

@ -1,23 +1,109 @@
Template.channelSettings.helpers
canEdit: ->
return RocketChat.authz.hasAllPermission('edit-room', @rid)
editing: (field) ->
return Template.instance().editing.get() is field
notDirect: ->
return ChatRoom.findOne(@rid)?.t isnt 'd'
roomType: ->
return ChatRoom.findOne(@rid)?.t
channelSettings: ->
return RocketChat.ChannelSettings.getOptions()
roomTypeDescription: ->
roomType = ChatRoom.findOne(@rid)?.t
if roomType is 'c'
return t('Channel')
else if roomType is 'p'
return t('Private_Group')
roomName: ->
return ChatRoom.findOne(@rid)?.name
roomTopic: ->
return ChatRoom.findOne(@rid)?.topic
archived: ->
return ChatRoom.findOne(@rid)?.archived
Template.channelSettings.events
'keydown input[type=text]': (e, t) ->
if e.keyCode is 13
e.preventDefault()
t.saveSetting()
'click [data-edit]': (e, t) ->
e.preventDefault()
t.editing.set($(e.currentTarget).data('edit'))
setTimeout (-> t.$('input.editing').focus().select()), 100
'click .cancel': (e, t) ->
e.preventDefault()
t.editing.set()
'click .save': (e, t) ->
e.preventDefault()
t.saveSetting()
'click .archive': (e, t) ->
e.preventDefault()
Meteor.call 'archiveRoom', t.data.rid, true, (err, results) ->
return toastr.error err.reason if err
toastr.success TAPi18n.__ 'Room_archived'
settings =
roomType: t.$('input[name=roomType]:checked').val()
'click .unarchive': (e, t) ->
e.preventDefault()
Meteor.call 'saveRoomSettings', t.data.rid, settings, (err, results) ->
Meteor.call 'unarchiveRoom', t.data.rid, true, (err, results) ->
return toastr.error err.reason if err
toastr.success TAPi18n.__ 'Settings_updated'
toastr.success TAPi18n.__ 'Room_unarchived'
Template.channelSettings.onCreated ->
@editing = new ReactiveVar
@validateRoomType = =>
type = @$('input[name=roomType]:checked').val()
if type not in ['c', 'p']
toastr.error t('Invalid_room_type', type)
return true
@validateRoomName = =>
rid = Template.currentData()?.rid
room = ChatRoom.findOne rid
if not RocketChat.authz.hasAllPermission('edit-room', @rid) or room.t not in ['c', 'p']
toastr.error t('Not_allowed')
return false
name = $('input[name=roomName]').val()
if not /^[0-9a-z-_]+$/.test name
toastr.error t('Invalid_room_name', name)
return false
return true
@validateRoomTopic = =>
return true
# switch room.t
# when 'c'
# FlowRouter.go 'channel', name: name
# when 'p'
# FlowRouter.go 'group', name: name
@saveSetting = =>
switch @editing.get()
when 'roomName'
if @validateRoomName()
Meteor.call 'saveRoomSettings', @data?.rid, 'roomName', @$('input[name=roomName]').val(), (err, result) ->
if err
if err.error in [ 'duplicate-name', 'name-invalid' ]
return toastr.error TAPi18n.__(err.reason, err.details.channelName)
return toastr.error TAPi18n.__(err.reason)
toastr.success TAPi18n.__ 'Room_name_changed_successfully'
when 'roomTopic'
if @validateRoomTopic()
Meteor.call 'saveRoomSettings', @data?.rid, 'roomTopic', @$('input[name=roomTopic]').val(), (err, result) ->
if err
return toastr.error TAPi18n.__(err.reason)
toastr.success TAPi18n.__ 'Room_topic_changed_successfully'
when 'roomType'
if @validateRoomType()
Meteor.call 'saveRoomSettings', @data?.rid, 'roomType', @$('input[name=roomType]:checked').val(), (err, result) ->
if err
if err.error is 'invalid-room-type'
return toastr.error TAPi18n.__(err.reason, err.details.roomType)
return toastr.error TAPi18n.__(err.reason)
toastr.success TAPi18n.__ 'Room_type_changed_successfully'
@editing.set()

@ -1,25 +1,63 @@
<template name="channelSettings">
<div class="control">
<div class="header">
<h2>{{_ "Room_Settings"}}</h2>
</div>
</div>
<div class="channel-settings scrollable">
<form>
<fieldset>
{{#if notDirect}}
<div class="input-line double-col">
<label>{{_ "Room_Type"}}</label>
<div class="content">
<div class="list-view channel-settings">
<div class="status">
<h2>{{_ "Room_Info"}}</h2>
</div>
<form>
<ul class="list clearfix">
{{#if notDirect}}
<li>
<label>{{_ "Name"}}</label>
<div>
{{#if editing 'roomName'}}
<input type="text" name="roomName" value="{{roomName}}" class="editing" /> <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> <button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{roomName}}{{#if canEdit}} <i class="octicon octicon-pencil" data-edit="roomName"></i>{{/if}}</span>
{{/if}}
</div>
</li>
{{/if}}
<li>
<label>{{_ "Topic"}}</label>
<div>
<label><input type="radio" name="roomType" value="c" checked="{{$eq roomType 'c'}}" /> {{_ "Channel"}}</label>
<label><input type="radio" name="roomType" value="p" checked="{{$eq roomType 'p'}}" /> {{_ "Private_Group"}}</label>
{{#if editing 'roomTopic'}}
<input type="text" name="roomTopic" value="{{roomTopic}}" class="editing" /> <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> <button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{roomTopic}}{{#if canEdit}} <i class="octicon octicon-pencil" data-edit="roomTopic"></i>{{/if}}</span>
{{/if}}
</div>
</div>
{{/if}}
</fieldset>
<div class="submit">
<button class="button save"><i class="icon-send"></i><span>{{_ "Save_changes"}}</span></button>
</div>
</form>
</li>
{{#if notDirect}}
<li>
<label>{{_ "Type"}}</label>
<div>
{{#if editing 'roomType'}}
<label><input type="radio" name="roomType" class="editing" value="c" checked="{{$eq roomType 'c'}}" /> {{_ "Channel"}}</label>
<label><input type="radio" name="roomType" value="p" checked="{{$eq roomType 'p'}}" /> {{_ "Private_Group"}}</label>
<button type="button" class="button secondary cancel">{{_ "Cancel"}}</button>
<button type="button" class="button primary save">{{_ "Save"}}</button>
{{else}}
<span>{{roomTypeDescription}}{{#if canEdit}} <i class="octicon octicon-pencil" data-edit="roomType"></i>{{/if}}</span>
{{/if}}
</div>
</li>
{{/if}}
{{#if notDirect}}
<li>
<label>{{_ "Archive_Unarchive"}}</label>
{{#if archived}}
<button class="button unarchive"><span>{{_ "Unarchive"}}</span></button>
{{else}}
<button class="button archive"><span>{{_ "Archive"}}</span></button>
{{/if}}
</li>
{{/if}}
{{#each channelSettings}}
{{> Template.dynamic template=template data=data}}
{{/each}}
</ul>
</form>
</div>
</div>
</template>

@ -1,8 +1,15 @@
{
"Channel" : "Channel",
"Private_Group" : "Private Group",
"Room_Type" : "Room Type",
"Room_Settings" : "Room Settings",
"room_changed_privacy" : "Room type changed to: <em>__room_type__</em> by <em>__user_by__</em>",
"room_changed_topic" : "Room topic changed to: <em>__room_topic__</em> by <em>__user_by__</em>"
}
"Archive_Unarchive": "Archive / Unarchive",
"Cancel": "Cancel",
"Channel": "Channel",
"Private_Group": "Private Group",
"Name": "Name",
"Save": "Save",
"Topic": "Topic",
"Type": "Type",
"Room_Info": "Room Info",
"room_changed_privacy": "Room type changed to: <em>__room_type__</em> by <em>__user_by__</em>",
"room_changed_topic": "Room topic changed to: <em>__room_topic__</em> by <em>__user_by__</em>",
"Room_topic_changed_successfully": "Room topic changed successfully",
"Room_type_changed_successfully": "Room type changed successfully"
}

@ -10,12 +10,15 @@ Package.onUse(function(api) {
api.use([
'coffeescript',
'reactive-var',
'tracker',
'templating',
'less@2.5.0',
'rocketchat:lib@0.0.1'
]);
api.addFiles([
'client/lib/ChannelSettings.coffee',
'client/startup/messageTypes.coffee',
'client/startup/tabBar.coffee',
'client/startup/trackSettingsChange.coffee',
@ -25,7 +28,9 @@ Package.onUse(function(api) {
], 'client');
api.addFiles([
'server/functions/changeRoomType.coffee',
'server/functions/saveRoomType.coffee',
'server/functions/saveRoomTopic.coffee',
'server/functions/saveRoomName.coffee',
'server/methods/saveRoomSettings.coffee',
'server/models/Messages.coffee'
], 'server');

@ -1,47 +0,0 @@
RocketChat.changeRoomType = (rid, roomType) ->
console.log '[function] RocketChat.changeRoomType'.green, rid, roomType
unless Match.test rid, String
throw new Meteor.Error 'invalid-rid'
if roomType not in ['c', 'p']
throw new Meteor.Error 'invalid-room-type'
return RocketChat.models.Rooms.setTypeById(rid, roomType) and RocketChat.models.Subscriptions.updateTypeByRoomId(rid, roomType)
# username = s.trim username
# if not user or not username
# return false
# if not /^[0-9a-zA-Z-_.]+$/.test username
# return false
# # User already has desired username, return
# if user.username is username
# return user
# # Check username availability
# unless RocketChat.checkUsernameAvailability username
# return false
# previousUsername = user.username
# # Username is available; if coming from old username, update all references
# if previousUsername
# RocketChat.models.Messages.updateAllUsernamesByUserId user._id, username
# RocketChat.models.Messages.findByMention(previousUsername).forEach (msg) ->
# updatedMsg = msg.msg.replace(new RegExp("@#{previousUsername}", "ig"), "@#{username}")
# RocketChat.models.Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername msg._id, previousUsername, username, updatedMsg
# RocketChat.models.Rooms.replaceUsername previousUsername, username
# RocketChat.models.Rooms.replaceUsernameOfUserByUserId user._id, username
# RocketChat.models.Subscriptions.setUserUsernameByUserId user._id, username
# RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName previousUsername, username
# # Set new username
# Meteor.users.update { _id: user._id }, { $set: { username: username } }
# user.username = username
# return user

@ -0,0 +1,29 @@
RocketChat.saveRoomName = (rid, name) ->
if not Meteor.userId()
throw new Meteor.Error('invalid-user', "[methods] sendMessage -> Invalid user")
room = RocketChat.models.Rooms.findOneById rid
if room.t not in ['c', 'p']
throw new Meteor.Error 403, 'Not allowed'
unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-room', rid)
#if room.u._id isnt Meteor.userId() and not hasPermission
throw new Meteor.Error 403, 'Not allowed'
if not /^[0-9a-z-_]+$/.test name
throw new Meteor.Error 'name-invalid', 'Invalid_room_name', { channelName: name }
name = _.slugify name
if name is room.name
return
# avoid duplicate names
if RocketChat.models.Rooms.findOneByName name
throw new Meteor.Error 'duplicate-name', 'Duplicate_channel_name', { channelName: name }
RocketChat.models.Rooms.setNameById rid, name
RocketChat.models.Subscriptions.updateNameByRoomId rid, name
return name

@ -0,0 +1,5 @@
RocketChat.saveRoomTopic = (rid, roomTopic) ->
unless Match.test rid, String
throw new Meteor.Error 'invalid-rid'
return RocketChat.models.Rooms.setTopicById(rid, roomTopic)

@ -0,0 +1,8 @@
RocketChat.saveRoomType = (rid, roomType) ->
unless Match.test rid, String
throw new Meteor.Error 'invalid-rid'
if roomType not in ['c', 'p']
throw new Meteor.Error 'invalid-room-type', 'Invalid_room_type', { roomType: roomType }
return RocketChat.models.Rooms.setTypeById(rid, roomType) and RocketChat.models.Subscriptions.updateTypeByRoomId(rid, roomType)

@ -1,24 +1,31 @@
Meteor.methods
saveRoomSettings: (rid, settings) ->
saveRoomSettings: (rid, setting, value) ->
unless Match.test rid, String
throw new Meteor.Error 'invalid-rid'
throw new Meteor.Error 'invalid-rid', 'Invalid room'
unless Match.test settings, Match.ObjectIncluding { roomType: String }
throw new Meteor.Error 'invalid-settings'
if setting not in ['roomName', 'roomTopic', 'roomType']
throw new Meteor.Error 'invalid-settings', 'Invalid settings provided'
unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-room', rid)
throw new Meteor.Error 503, 'Not authorized'
room = RocketChat.models.Rooms.findOneById rid
if room?
if settings.roomType isnt room.t
RocketChat.changeRoomType(rid, settings.roomType)
if settings.roomType is 'c'
message = TAPi18n.__('Channel')
else
message = TAPi18n.__('Private_Group')
RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_privacy', rid, message, Meteor.user()
switch setting
when 'roomName'
name = RocketChat.saveRoomName rid, value
RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser rid, name, Meteor.user()
when 'roomTopic'
if value isnt room.topic
RocketChat.saveRoomTopic(rid, value)
RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_topic', rid, value, Meteor.user()
when 'roomType'
if value isnt room.t
RocketChat.saveRoomType(rid, value)
if value is 'c'
message = TAPi18n.__('Channel')
else
message = TAPi18n.__('Private_Group')
RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_privacy', rid, message, Meteor.user()
return true

@ -1,2 +1,5 @@
RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser = (type, roomId, message, user, extraData) ->
return @createWithTypeRoomIdMessageAndUser type, roomId, message, user, extraData
RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser = (roomId, roomName, user, extraData) ->
return @createWithTypeRoomIdMessageAndUser 'r', roomId, roomName, user, extraData

@ -20,34 +20,42 @@ RocketChatFile =
RocketChat.settings.updateOptionsById 'Accounts_AvatarResize', {alert: 'The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server'}
exec 'gm version', Meteor.bindEnvironment (error, stdout, stderr) ->
if not error? and stdout.indexOf('GraphicsMagick') > -1
RocketChatFile.enable()
RocketChat.Info.GraphicsMagick =
enabled: true
version: stdout
else
RocketChat.Info.GraphicsMagick =
enabled: false
exec 'convert -version', Meteor.bindEnvironment (error, stdout, stderr) ->
if not error? and stdout.indexOf('ImageMagick') > -1
if RocketChatFile.enabled isnt true
# Enable GM to work with ImageMagick if no GraphicsMagick
RocketChatFile.gm = RocketChatFile.gm.subClass({imageMagick: true})
RocketChatFile.enable()
RocketChat.Info.ImageMagick =
detectGM = ->
exec 'gm version', Meteor.bindEnvironment (error, stdout, stderr) ->
if not error? and stdout.indexOf('GraphicsMagick') > -1
RocketChatFile.enable()
RocketChat.Info.GraphicsMagick =
enabled: true
version: stdout
else
if RocketChatFile.enabled isnt true
RocketChatFile.disable()
RocketChat.Info.ImageMagick =
RocketChat.Info.GraphicsMagick =
enabled: false
exec 'convert -version', Meteor.bindEnvironment (error, stdout, stderr) ->
if not error? and stdout.indexOf('ImageMagick') > -1
if RocketChatFile.enabled isnt true
# Enable GM to work with ImageMagick if no GraphicsMagick
RocketChatFile.gm = RocketChatFile.gm.subClass({imageMagick: true})
RocketChatFile.enable()
RocketChat.Info.ImageMagick =
enabled: true
version: stdout
else
if RocketChatFile.enabled isnt true
RocketChatFile.disable()
RocketChat.Info.ImageMagick =
enabled: false
detectGM()
Meteor.methods
'detectGM': ->
detectGM()
return
RocketChatFile.bufferToStream = (buffer) ->
bufferStream = new stream.PassThrough()

@ -8,6 +8,7 @@ class Highlight
constructor: (message) ->
if s.trim message.html
message.tokens ?= []
# Count occurencies of ```
count = (message.html.match(/```/g) || []).length
@ -40,7 +41,15 @@ class Highlight
result = hljs.highlightAuto code
else
result = hljs.highlight lang, code
msgParts[index] = "<pre><code class='hljs " + result.language + "'><span class='copyonly'>```<br></span>" + result.value + "<span class='copyonly'><br>```</span></code></pre>"
token = "$#{Random.id()}$"
message.tokens.push
highlight: true
token: token
text: "<pre><code class='hljs " + result.language + "'><span class='copyonly'>```<br></span>" + result.value + "<span class='copyonly'><br>```</span></code></pre>"
msgParts[index] = token
else
msgParts[index] = part

@ -24,3 +24,13 @@ FlowRouter.route '/admin/integrations/incoming/:id?',
pageTitle: t('Integration_Incoming_WebHook')
pageTemplate: 'integrationsIncoming'
params: params
FlowRouter.route '/admin/integrations/outgoing/:id?',
name: 'admin-integrations-outgoing'
action: (params) ->
BlazeLayout.render 'main',
center: 'pageSettingsContainer'
pageTitle: t('Integration_Outgoing_WebHook')
pageTemplate: 'integrationsOutgoing'
params: params

@ -7,26 +7,46 @@
<div class="section">
<div class="admin-integrations-new-panel">
{{#each integrations}}
<a href="{{pathFor "admin-integrations-incoming" id=_id}}">
<div class="admin-integrations-new-item">
<i class="icon-login"></i>
<div class="admin-integrations-new-item-body">
<div class="admin-integrations-new-item-title">
Incoming WebHook {{#if name}}- {{name}}{{/if}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Post_to_s_as_s" channel username}}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Created_at_s_by_s" (dateFormated _createdAt) _createdBy.username}}}
{{#if $eq type 'webhook-incoming'}}
<a href="{{pathFor "admin-integrations-incoming" id=_id}}">
<div class="admin-integrations-new-item">
<i class="icon-login"></i>
<div class="admin-integrations-new-item-body">
<div class="admin-integrations-new-item-title">
Incoming WebHook {{#if name}}- {{name}}{{/if}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Post_to_s_as_s" channel username}}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Created_at_s_by_s" (dateFormated _createdAt) _createdBy.username}}}
</div>
</div>
<i class="icon-angle-right"></i>
</div>
<i class="icon-angle-right"></i>
</div>
</a>
</a>
{{/if}}
{{else}}
<h1>{{_ "There_is_no_integrations"}}</h1>
{{/each}}
{{#each integrations}}
{{#if $eq type 'webhook-outgoing'}}
<a href="{{pathFor "admin-integrations-outgoing" id=_id}}">
<div class="admin-integrations-new-item">
<i class="icon-login"></i>
<div class="admin-integrations-new-item-body">
<div class="admin-integrations-new-item-title">
Outgoing WebHook {{#if name}}- {{name}}{{/if}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Created_at_s_by_s" (dateFormated _createdAt) _createdBy.username}}}
</div>
</div>
<i class="icon-angle-right"></i>
</div>
</a>
{{/if}}
{{/each}}
</div>
</div>
</div>

@ -108,7 +108,7 @@ Template.integrationsIncoming.events
closeOnConfirm: false
html: false
, ->
Meteor.call "deleteIntegration", params.id, (err, data) ->
Meteor.call "deleteIncomingIntegration", params.id, (err, data) ->
swal
title: t('Deleted')
text: t('Your_entry_has_been_deleted')
@ -141,16 +141,15 @@ Template.integrationsIncoming.events
params = Template.instance().data.params?()
if params?.id?
Meteor.call "updateIntegration", params.id, integration, (err, data) ->
Meteor.call "updateIncomingIntegration", params.id, integration, (err, data) ->
if err?
return toastr.error TAPi18n.__(err.error)
toastr.success TAPi18n.__("Integration_updated")
else
integration.type = 'webhook-incoming'
integration.username = username
Meteor.call "addIntegration", integration, (err, data) ->
Meteor.call "addIncomingIntegration", integration, (err, data) ->
if err?
return toastr.error TAPi18n.__(err.error)

@ -52,7 +52,7 @@
<div>
<input type="text" name="emoji" value="{{data.emoji}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_can_use_an_emoji_as_avatar"}}</div>
<div class="settings-description">{{_ "Example_s" ":ghost:"}}</div>
<div class="settings-description">{{{_ "Example_s" ":ghost:"}}}</div>
</div>
</div>
{{#if data.token}}

@ -20,7 +20,7 @@
<i class="icon-angle-right"></i>
</div>
</a>
<!-- <a href="{{pathFor "admin-integrations-incoming"}}">
<a href="{{pathFor "admin-integrations-outgoing"}}">
<div class="admin-integrations-new-item">
<i class="icon-logout"></i>
<div class="admin-integrations-new-item-body">
@ -33,7 +33,7 @@
</div>
<i class="icon-angle-right"></i>
</div>
</a> -->
</a>
</div>
</div>
</div>

@ -0,0 +1,167 @@
Template.integrationsOutgoing.onCreated ->
@record = new ReactiveVar
username: 'rocket.cat'
token: Random.id(24)
Template.integrationsOutgoing.helpers
join: (arr, sep) ->
if not arr?.join?
return arr
return arr.join sep
hasPermission: ->
return RocketChat.authz.hasAllPermission 'manage-integrations'
data: ->
params = Template.instance().data.params?()
if params?.id?
data = ChatIntegrations.findOne({_id: params.id})
if data?
if not data.token?
data.token = Random.id(24)
return data
return Template.instance().record.curValue
example: ->
record = Template.instance().record.get()
return {} =
_id: Random.id()
alias: record.alias
emoji: record.emoji
avatar: record.avatar
msg: 'Response text'
bot:
i: Random.id()
groupable: false
attachments: [{
title: "Rocket.Chat"
title_link: "https://rocket.chat"
text: "Rocket.Chat, the best open source chat"
image_url: "https://rocket.chat/images/mockup.png"
color: "#764FA5"
}]
ts: new Date
u:
_id: Random.id()
username: record.username
exampleJson: ->
record = Template.instance().record.get()
data =
username: record.alias
icon_emoji: record.emoji
icon_url: record.avatar
text: 'Response text'
attachments: [{
title: "Rocket.Chat"
title_link: "https://rocket.chat"
text: "Rocket.Chat, the best open source chat"
image_url: "https://rocket.chat/images/mockup.png"
color: "#764FA5"
}]
for key, value of data
delete data[key] if value in [null, ""]
return hljs.highlight('json', JSON.stringify(data, null, 2)).value
Template.integrationsOutgoing.events
"blur input": (e, t) ->
t.record.set
name: $('[name=name]').val().trim()
alias: $('[name=alias]').val().trim()
emoji: $('[name=emoji]').val().trim()
avatar: $('[name=avatar]').val().trim()
channel: $('[name=channel]').val().trim()
username: $('[name=username]').val().trim()
triggerWords: $('[name=triggerWords]').val().trim()
urls: $('[name=urls]').val().trim()
token: $('[name=token]').val().trim()
"click .submit > .delete": ->
params = Template.instance().data.params()
swal
title: t('Are_you_sure')
text: t('You_will_not_be_able_to_recover')
type: 'warning'
showCancelButton: true
confirmButtonColor: '#DD6B55'
confirmButtonText: t('Yes_delete_it')
cancelButtonText: t('Cancel')
closeOnConfirm: false
html: false
, ->
Meteor.call "deleteOutgoingIntegration", params.id, (err, data) ->
swal
title: t('Deleted')
text: t('Your_entry_has_been_deleted')
type: 'success'
timer: 1000
showConfirmButton: false
FlowRouter.go "admin-integrations"
"click .submit > .save": ->
name = $('[name=name]').val().trim()
alias = $('[name=alias]').val().trim()
emoji = $('[name=emoji]').val().trim()
avatar = $('[name=avatar]').val().trim()
channel = $('[name=channel]').val().trim()
username = $('[name=username]').val().trim()
triggerWords = $('[name=triggerWords]').val().trim()
urls = $('[name=urls]').val().trim()
token = $('[name=token]').val().trim()
if username is ''
return toastr.error TAPi18n.__("The_username_is_required")
triggerWords = triggerWords.split(',')
for triggerWord, index in triggerWords
triggerWords[index] = triggerWord.trim()
delete triggerWords[index] if triggerWord.trim() is ''
triggerWords = _.without triggerWords, [undefined]
urls = urls.split('\n')
for url, index in urls
urls[index] = url.trim()
delete urls[index] if url.trim() is ''
urls = _.without urls, [undefined]
if urls.length is 0
return toastr.error TAPi18n.__("You_should_inform_one_url_at_least")
integration =
username: username
channel: channel if channel isnt ''
alias: alias if alias isnt ''
emoji: emoji if emoji isnt ''
avatar: avatar if avatar isnt ''
name: name if name isnt ''
triggerWords: triggerWords if triggerWords isnt ''
urls: urls if urls isnt ''
token: token if token isnt ''
params = Template.instance().data.params?()
if params?.id?
Meteor.call "updateOutgoingIntegration", params.id, integration, (err, data) ->
if err?
return toastr.error TAPi18n.__(err.error)
toastr.success TAPi18n.__("Integration_updated")
else
Meteor.call "addOutgoingIntegration", integration, (err, data) ->
if err?
return toastr.error TAPi18n.__(err.error)
toastr.success TAPi18n.__("Integration_added")
FlowRouter.go "admin-integrations-outgoing", {id: data._id}

@ -0,0 +1,100 @@
<template name="integrationsOutgoing">
<div class="permissions-manager">
{{#if hasPermission}}
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br>
<div class="rocket-form">
<div class="section">
<div class="section-content">
<div class="input-line double-col">
<label>{{_ "Name"}} ({{_ "optional"}})</label>
<div>
<input type="text" name="name" value="{{data.name}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_should_name_it_to_easily_manage_your_integrations"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Channel"}} ({{_ "optional"}})</label>
<div>
<input type="text" name="channel" value="{{data.channel}}" placeholder="{{_ 'User_or_channel_name'}}" />
<div class="settings-description">{{_ "Optional channel to listen on"}}</div>
<div class="settings-description">{{{_ "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s" "@" "#" "@john" "#general"}}}</div>
<div class="settings-description">{{{_ "Leave empty to listen <strong>any public channel</strong>"}}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Trigger_Words"}} ({{_ "optional"}})</label>
<div>
<input type="text" name="triggerWords" value="{{join data.triggerWords ','}}" />
<div class="settings-description">{{_ "When a line starts with one of these words, post to the URL(s) below"}}</div>
<div class="settings-description">{{_ "Separate multiple words with commas"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "URLs"}}</label>
<div>
<textarea name="urls" style="height: 100px;">{{join data.urls "\n"}}</textarea>
<div class="settings-description">{{_ "Enter as many URLs as you like, one per line, please"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Post_as"}}</label>
<div>
<input type="text" name="username" value="{{data.username}}" />
<div class="settings-description">{{_ "Choose_the_username_that_this_integration_will_post_as"}}</div>
<div class="settings-description">{{_ "Should_exists_a_user_with_this_username"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Alias"}} ({{_ "optional"}})</label>
<div>
<input type="text" name="alias" value="{{data.alias}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "Choose_the_alias_that_will_appear_before_the_username_in_messages"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Avatar_URL"}} ({{_ "optional"}})</label>
<div>
<input type="url" name="avatar" value="{{data.avatar}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_can_change_a_different_avatar_too"}}</div>
<div class="settings-description">{{_ "Should_be_a_URL_of_an_image"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Emoji"}} ({{_ "optional"}})</label>
<div>
<input type="text" name="emoji" value="{{data.emoji}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_can_use_an_emoji_as_avatar"}}</div>
<div class="settings-description">{{{_ "Example_s" ":ghost:"}}}</div>
</div>
</div>
<div class="input-line double-col">
<label>Token ({{_ "optional"}})</label>
<div>
<input type="text" name="token" value="{{data.token}}" />
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Responding"}}</label>
<div>
<div class="settings-description">{{{_ "If the handler wishes to post a response back into the Slack channel, the following JSON should be returned as the body of the response:"}}}</div>
<pre><code class="hljs json json-example">{{{exampleJson}}}</code></pre>
<div class="settings-description">{{{_ "Empty bodies or bodies with an empty text property will simply be ignored. Non-200 responses will be retried a reasonable number of times. A response will be posted using the alias and avatar specified above. You can override these informations as in the example above."}}}</div>
</div>
</div>
<div class="input-line message-example">
{{#nrr nrrargs 'message' example}}{{/nrr}}
</div>
</div>
</div>
<div class="submit">
{{#if data.token}}
<button class="button red delete"><i class="icon-trash"></i><span>{{_ "Delete"}}</span></button>
{{/if}}
<button class="button save"><i class="icon-send"></i><span>{{_ "Save_changes"}}</span></button>
</div>
</div>
{{else}}
{{_ "Not_authorized"}}
{{/if}}
</div>
</template>

@ -13,6 +13,7 @@ Package.onUse(function(api) {
api.use('underscore');
api.use('simple:highlight.js');
api.use('rocketchat:lib@0.0.1');
api.use('alanning:roles@1.2.12');
api.use('kadira:flow-router', 'client');
api.use('templating', 'client');
@ -29,6 +30,8 @@ Package.onUse(function(api) {
api.addFiles('client/views/integrationsNew.coffee', 'client');
api.addFiles('client/views/integrationsIncoming.html', 'client');
api.addFiles('client/views/integrationsIncoming.coffee', 'client');
api.addFiles('client/views/integrationsOutgoing.html', 'client');
api.addFiles('client/views/integrationsOutgoing.coffee', 'client');
// stylesheets
api.addAssets('client/stylesheets/integrations.less', 'server');
@ -40,13 +43,19 @@ Package.onUse(function(api) {
api.addFiles('server/publications/integrations.coffee', 'server');
// methods
api.addFiles('server/methods/addIntegration.coffee', 'server');
api.addFiles('server/methods/updateIntegration.coffee', 'server');
api.addFiles('server/methods/deleteIntegration.coffee', 'server');
api.addFiles('server/methods/incoming/addIncomingIntegration.coffee', 'server');
api.addFiles('server/methods/incoming/updateIncomingIntegration.coffee', 'server');
api.addFiles('server/methods/incoming/deleteIncomingIntegration.coffee', 'server');
api.addFiles('server/methods/outgoing/addOutgoingIntegration.coffee', 'server');
api.addFiles('server/methods/outgoing/updateOutgoingIntegration.coffee', 'server');
api.addFiles('server/methods/outgoing/deleteOutgoingIntegration.coffee', 'server');
// api
api.addFiles('server/api/api.coffee', 'server');
api.addFiles('server/triggers.coffee', 'server');
var _ = Npm.require('underscore');
var fs = Npm.require('fs');
tapi18nFiles = _.compact(_.map(fs.readdirSync('packages/rocketchat-integrations/i18n'), function(filename) {

@ -38,8 +38,9 @@ Api.addRoute ':integrationId/:userId/:token', authRequired: true,
error: 'invalid-channel'
rid = room._id
Meteor.runAsUser user._id, ->
Meteor.call 'joinRoom', room._id
if room.t is 'c'
Meteor.runAsUser user._id, ->
Meteor.call 'joinRoom', room._id
when '@'
roomUser = RocketChat.models.Users.findOne
@ -60,7 +61,7 @@ Api.addRoute ':integrationId/:userId/:token', authRequired: true,
if not room
Meteor.runAsUser user._id, ->
Meteor.call 'createDirectMessage', roomUser._id
Meteor.call 'createDirectMessage', roomUser.username
room = RocketChat.models.Rooms.findOne(rid)
else
@ -100,3 +101,129 @@ Api.addRoute ':integrationId/:userId/:token', authRequired: true,
statusCode: 200
body:
success: true
Api.addRoute 'add/:integrationId/:userId/:token', authRequired: true,
post: ->
console.log 'Add integration'
console.log @bodyParams
if @bodyParams?.payload?
@bodyParams = JSON.parse @bodyParams.payload
integration = RocketChat.models.Integrations.findOne(@urlParams.integrationId)
user = RocketChat.models.Users.findOne(@userId)
if not integration?
return {} =
statusCode: 400
body:
success: false
error: 'Invalid integraiton id'
Meteor.runAsUser user._id, =>
switch @bodyParams['event']
when 'newMessageOnChannel'
@bodyParams.data ?= {}
if @bodyParams.data.channel_name? and @bodyParams.data.channel_name.indexOf('#') is -1
@bodyParams.data.channel_name = '#' + @bodyParams.data.channel_name
Meteor.call 'addOutgoingIntegration',
username: 'rocket.cat'
urls: [@bodyParams.target_url]
name: @bodyParams.name
channel: @bodyParams.data.channel_name
triggerWords: @bodyParams.data.trigger_words
when 'newMessageToUser'
if @bodyParams.data.username.indexOf('@') is -1
@bodyParams.data.username = '@' + @bodyParams.data.username
Meteor.call 'addOutgoingIntegration',
username: 'rocket.cat'
urls: [@bodyParams.target_url]
name: @bodyParams.name
channel: @bodyParams.data.username
triggerWords: @bodyParams.data.trigger_words
return {} =
statusCode: 200
body:
success: true
Api.addRoute 'remove/:integrationId/:userId/:token', authRequired: true,
post: ->
console.log 'Remove integration'
console.log @bodyParams
if @bodyParams?.payload?
@bodyParams = JSON.parse @bodyParams.payload
integration = RocketChat.models.Integrations.findOne(@urlParams.integrationId)
user = RocketChat.models.Users.findOne(@userId)
if not integration?
return {} =
statusCode: 400
body:
success: false
error: 'Invalid integraiton id'
integrationToRemove = RocketChat.models.Integrations.findOne urls: @bodyParams.target_url
Meteor.runAsUser user._id, =>
Meteor.call 'deleteOutgoingIntegration', integrationToRemove._id
return {} =
statusCode: 200
body:
success: true
Api.addRoute 'sample/:integrationId/:userId/:token', authRequired: true,
get: ->
console.log 'Sample Integration'
return {} =
statusCode: 200
body: [
token: Random.id(24)
channel_id: Random.id()
channel_name: 'general'
timestamp: new Date
user_id: Random.id()
user_name: 'rocket.cat'
text: 'Sample text 1'
trigger_word: 'Sample'
,
token: Random.id(24)
channel_id: Random.id()
channel_name: 'general'
timestamp: new Date
user_id: Random.id()
user_name: 'rocket.cat'
text: 'Sample text 2'
trigger_word: 'Sample'
,
token: Random.id(24)
channel_id: Random.id()
channel_name: 'general'
timestamp: new Date
user_id: Random.id()
user_name: 'rocket.cat'
text: 'Sample text 3'
trigger_word: 'Sample'
]
Api.addRoute 'info/:integrationId/:userId/:token', authRequired: true,
get: ->
console.log 'Info integration'
return {} =
statusCode: 200
body:
success: true

@ -1,22 +1,22 @@
Meteor.methods
addIntegration: (integration) ->
addIncomingIntegration: (integration) ->
if not RocketChat.authz.hasPermission @userId, 'manage-integrations'
throw new Meteor.Error 'not_authorized'
if not _.isString(integration.channel)
throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel must be string'
throw new Meteor.Error 'invalid_channel', '[methods] addIncomingIntegration -> channel must be string'
if integration.channel.trim() is ''
throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel can\'t be empty'
throw new Meteor.Error 'invalid_channel', '[methods] addIncomingIntegration -> channel can\'t be empty'
if integration.channel[0] not in ['@', '#']
throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel should start with # or @'
throw new Meteor.Error 'invalid_channel', '[methods] addIncomingIntegration -> channel should start with # or @'
if not _.isString(integration.username)
throw new Meteor.Error 'invalid_username', '[methods] addIntegration -> username must be string'
throw new Meteor.Error 'invalid_username', '[methods] addIncomingIntegration -> username must be string'
if integration.username.trim() is ''
throw new Meteor.Error 'invalid_username', '[methods] addIntegration -> username can\'t be empty'
throw new Meteor.Error 'invalid_username', '[methods] addIncomingIntegration -> username can\'t be empty'
record = undefined
channelType = integration.channel[0]
@ -37,12 +37,12 @@ Meteor.methods
]
if record is undefined
throw new Meteor.Error 'channel_does_not_exists', "[methods] addIntegration -> The channel does not exists"
throw new Meteor.Error 'channel_does_not_exists', "[methods] addIncomingIntegration -> The channel does not exists"
user = RocketChat.models.Users.findOne({username: integration.username})
if not user?
throw new Meteor.Error 'user_does_not_exists', "[methods] addIntegration -> The username does not exists"
throw new Meteor.Error 'user_does_not_exists', "[methods] addIncomingIntegration -> The username does not exists"
stampedToken = Accounts._generateStampedLoginToken()
hashStampedToken = Accounts._hashStampedToken(stampedToken)
@ -53,6 +53,7 @@ Meteor.methods
hashedToken: hashStampedToken.hashedToken
integration: true
integration.type = 'webhook-incoming'
integration.token = hashStampedToken.hashedToken
integration.userId = user._id
integration._createdAt = new Date
@ -60,6 +61,8 @@ Meteor.methods
RocketChat.models.Users.update {_id: user._id}, updateObj
Roles.addUsersToRoles user._id, 'bot', 'bot'
integration._id = RocketChat.models.Integrations.insert integration
return integration

@ -1,12 +1,12 @@
Meteor.methods
deleteIntegration: (integrationId) ->
deleteIncomingIntegration: (integrationId) ->
if not RocketChat.authz.hasPermission @userId, 'manage-integrations'
throw new Meteor.Error 'not_authorized'
integration = RocketChat.models.Integrations.findOne(integrationId)
if not integration?
throw new Meteor.Error 'invalid_integration', '[methods] addIntegration -> integration not found'
throw new Meteor.Error 'invalid_integration', '[methods] deleteIncomingIntegration -> integration not found'
updateObj =
$pull:

@ -1,19 +1,20 @@
Meteor.methods
updateIntegration: (integrationId, integration) ->
updateIncomingIntegration: (integrationId, integration) ->
if not RocketChat.authz.hasPermission @userId, 'manage-integrations'
throw new Meteor.Error 'not_authorized'
if not _.isString(integration.channel)
throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel must be string'
throw new Meteor.Error 'invalid_channel', '[methods] updateIncomingIntegration -> channel must be string'
if integration.channel.trim() is ''
throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel can\'t be empty'
throw new Meteor.Error 'invalid_channel', '[methods] updateIncomingIntegration -> channel can\'t be empty'
if integration.channel[0] not in ['@', '#']
throw new Meteor.Error 'invalid_channel', '[methods] addIntegration -> channel should start with # or @'
throw new Meteor.Error 'invalid_channel', '[methods] updateIncomingIntegration -> channel should start with # or @'
if not RocketChat.models.Integrations.findOne(integrationId)?
throw new Meteor.Error 'invalid_integration', '[methods] addIntegration -> integration not found'
currentIntegration = RocketChat.models.Integrations.findOne(integrationId)
if not currentIntegration?
throw new Meteor.Error 'invalid_integration', '[methods] updateIncomingIntegration -> integration not found'
record = undefined
channelType = integration.channel[0]
@ -34,7 +35,10 @@ Meteor.methods
]
if record is undefined
throw new Meteor.Error 'channel_does_not_exists', "[methods] addIntegration -> The channel does not exists"
throw new Meteor.Error 'channel_does_not_exists', "[methods] updateIncomingIntegration -> The channel does not exists"
user = RocketChat.models.Users.findOne({username: currentIntegration.username})
Roles.addUsersToRoles user._id, 'bot', 'bot'
RocketChat.models.Integrations.update integrationId,
$set:

@ -0,0 +1,69 @@
Meteor.methods
addOutgoingIntegration: (integration) ->
if integration.channel?.trim? and integration.channel.trim() is ''
delete integration.channel
if not RocketChat.authz.hasPermission(@userId, 'manage-integrations') and not RocketChat.authz.hasPermission(@userId, 'manage-integrations', 'bot')
throw new Meteor.Error 'not_authorized'
if integration.username.trim() is ''
throw new Meteor.Error 'invalid_username', '[methods] addOutgoingIntegration -> username can\'t be empty'
if not Match.test integration.urls, [String]
throw new Meteor.Error 'invalid_urls', '[methods] addOutgoingIntegration -> urls must be an array'
for url, index in integration.urls
delete integration.urls[index] if url.trim() is ''
integration.urls = _.without integration.urls, [undefined]
if integration.urls.length is 0
throw new Meteor.Error 'invalid_urls', '[methods] addOutgoingIntegration -> urls is required'
if integration.channel? and integration.channel[0] not in ['@', '#']
throw new Meteor.Error 'invalid_channel', '[methods] addOutgoingIntegration -> channel should start with # or @'
if integration.triggerWords?
if not Match.test integration.triggerWords, [String]
throw new Meteor.Error 'invalid_triggerWords', '[methods] addOutgoingIntegration -> triggerWords must be an array'
for triggerWord, index in integration.triggerWords
delete integration.triggerWords[index] if triggerWord.trim() is ''
integration.triggerWords = _.without integration.triggerWords, [undefined]
if integration.channel?
record = undefined
channelType = integration.channel[0]
channel = integration.channel.substr(1)
switch channelType
when '#'
record = RocketChat.models.Rooms.findOne
$or: [
{_id: channel}
{name: channel}
]
when '@'
record = RocketChat.models.Users.findOne
$or: [
{_id: channel}
{username: channel}
]
if record is undefined
throw new Meteor.Error 'channel_does_not_exists', "[methods] addOutgoingIntegration -> The channel does not exists"
user = RocketChat.models.Users.findOne({username: integration.username})
if not user?
throw new Meteor.Error 'user_does_not_exists', "[methods] addOutgoingIntegration -> The username does not exists"
integration.type = 'webhook-outgoing'
integration.userId = user._id
integration._createdAt = new Date
integration._createdBy = RocketChat.models.Users.findOne @userId, {fields: {username: 1}}
integration._id = RocketChat.models.Integrations.insert integration
return integration

@ -0,0 +1,13 @@
Meteor.methods
deleteOutgoingIntegration: (integrationId) ->
if not RocketChat.authz.hasPermission(@userId, 'manage-integrations') and not RocketChat.authz.hasPermission(@userId, 'manage-integrations', 'bot')
throw new Meteor.Error 'not_authorized'
integration = RocketChat.models.Integrations.findOne(integrationId)
if not integration?
throw new Meteor.Error 'invalid_integration', '[methods] deleteOutgoingIntegration -> integration not found'
RocketChat.models.Integrations.remove _id: integrationId
return true

@ -0,0 +1,81 @@
Meteor.methods
updateOutgoingIntegration: (integrationId, integration) ->
if not RocketChat.authz.hasPermission @userId, 'manage-integrations'
throw new Meteor.Error 'not_authorized'
if integration.username.trim() is ''
throw new Meteor.Error 'invalid_username', '[methods] updateOutgoingIntegration -> username can\'t be empty'
if not Match.test integration.urls, [String]
throw new Meteor.Error 'invalid_urls', '[methods] updateOutgoingIntegration -> urls must be an array'
for url, index in integration.urls
delete integration.urls[index] if url.trim() is ''
integration.urls = _.without integration.urls, [undefined]
if integration.urls.length is 0
throw new Meteor.Error 'invalid_urls', '[methods] updateOutgoingIntegration -> urls is required'
if integration.channel?.trim() isnt '' and integration.channel[0] not in ['@', '#']
throw new Meteor.Error 'invalid_channel', '[methods] updateOutgoingIntegration -> channel should start with # or @'
if not integration.token? or integration.token?.trim() is ''
throw new Meteor.Error 'invalid_token', '[methods] updateOutgoingIntegration -> token is required'
if integration.triggerWords?
if not Match.test integration.triggerWords, [String]
throw new Meteor.Error 'invalid_triggerWords', '[methods] updateOutgoingIntegration -> triggerWords must be an array'
for triggerWord, index in integration.triggerWords
delete integration.triggerWords[index] if triggerWord.trim() is ''
integration.triggerWords = _.without integration.triggerWords, [undefined]
if not RocketChat.models.Integrations.findOne(integrationId)?
throw new Meteor.Error 'invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'
if integration.channel?.trim() isnt ''
record = undefined
channelType = integration.channel[0]
channel = integration.channel.substr(1)
switch channelType
when '#'
record = RocketChat.models.Rooms.findOne
$or: [
{_id: channel}
{name: channel}
]
when '@'
record = RocketChat.models.Users.findOne
$or: [
{_id: channel}
{username: channel}
]
if record is undefined
throw new Meteor.Error 'channel_does_not_exists', "[methods] updateOutgoingIntegration -> The channel does not exists"
user = RocketChat.models.Users.findOne({username: integration.username})
if not user?
throw new Meteor.Error 'user_does_not_exists', "[methods] updateOutgoingIntegration -> The username does not exists"
RocketChat.models.Integrations.update integrationId,
$set:
name: integration.name
avatar: integration.avatar
emoji: integration.emoji
alias: integration.alias
channel: integration.channel
username: integration.username
userId: user._id
urls: integration.urls
token: integration.token
triggerWords: integration.triggerWords
_updatedAt: new Date
_updatedBy: RocketChat.models.Users.findOne @userId, {fields: {username: 1}}
return RocketChat.models.Integrations.findOne(integrationId)

@ -0,0 +1,110 @@
triggers = {}
RocketChat.models.Integrations.find({type: 'webhook-outgoing'}).observe
added: (record) ->
channel = record.channel or '__any'
triggers[channel] ?= {}
triggers[channel][record._id] = record
changed: (record) ->
channel = record.channel or '__any'
triggers[channel] ?= {}
triggers[channel][record._id] = record
removed: (record) ->
channel = record.channel or '__any'
delete triggers[channel][record._id]
ExecuteTriggerUrl = (url, trigger, message, room, tries=0) ->
word = undefined
if trigger.triggerWords?.length > 0
for triggerWord in trigger.triggerWords
if message.msg.indexOf(triggerWord) is 0
word = triggerWord
break
# Stop if there are triggerWords but none match
if not word?
return
data =
token: trigger.token
channel_id: room._id
channel_name: room.name
timestamp: message.ts
user_id: message.u._id
user_name: message.u.username
text: message.msg
if word?
data.trigger_word = word
opts =
data: data
npmRequestOptions:
rejectUnauthorized: !RocketChat.settings.get 'Allow_Invalid_SelfSigned_Certs'
strictSSL: !RocketChat.settings.get 'Allow_Invalid_SelfSigned_Certs'
headers:
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36'
HTTP.call 'POST', url, opts, (error, result) ->
if not result? or result.statusCode isnt 200
if result.statusCode is 410
RocketChat.models.Integrations.remove _id: trigger._id
return
if tries <= 6
# Try again in 0.1s, 1s, 10s, 1m40s, 16m40s, 2h46m40s and 27h46m40s
Meteor.setTimeout ->
ExecuteTriggerUrl url, trigger, message, room, tries+1
, Math.pow(10, tries+2)
return
# TODO process return and insert message if necessary
ExecuteTrigger = (trigger, message, room) ->
for url in trigger.urls
ExecuteTriggerUrl url, trigger, message, room
ExecuteTriggers = (message, room) ->
if not room?
return
triggersToExecute = []
switch room.t
when 'd'
id = room._id.replace(message.u._id, '')
username = _.without room.usernames, message.u.username
username = username[0]
if triggers['@'+id]?
triggersToExecute.push trigger for key, trigger of triggers['@'+id]
if id isnt username and triggers['@'+username]?
triggersToExecute.push trigger for key, trigger of triggers['@'+username]
when 'c'
if triggers.__any?
triggersToExecute.push trigger for key, trigger of triggers.__any
else
if triggers['#'+room._id]?
triggersToExecute.push trigger for key, trigger of triggers['#'+room._id]
if room._id isnt room.name and triggers['#'+room.name]?
triggersToExecute.push trigger for key, trigger of triggers['#'+room.name]
for triggerToExecute in triggersToExecute
ExecuteTrigger triggerToExecute, message, room
return message
RocketChat.callbacks.add 'afterSaveMessage', ExecuteTriggers, RocketChat.callbacks.priority.LOW

@ -1,4 +1,6 @@
@roomExit = ->
RocketChat.callbacks.run 'roomExit'
BlazeLayout.render 'main', {center: 'none'}
if currentTracker?
@ -10,8 +12,9 @@
if child?
if child.classList.contains('room-container')
wrapper = child.querySelector('.messages-box > .wrapper')
if wrapper.scrollTop >= wrapper.scrollHeight - wrapper.clientHeight
child.oldScrollTop = 10e10
else
child.oldScrollTop = wrapper.scrollTop
if wrapper
if wrapper.scrollTop >= wrapper.scrollHeight - wrapper.clientHeight
child.oldScrollTop = 10e10
else
child.oldScrollTop = wrapper.scrollTop
mainNode.removeChild child

@ -0,0 +1,26 @@
RocketChat.Message =
parse: (msg, language) ->
messageType = RocketChat.MessageTypes.getType(msg)
if messageType?.render?
return messageType.render(msg)
else if messageType?.template?
# render template
else if messageType?.message?
if not language and localStorage?.getItem('userLanguage')
language = localStorage.getItem('userLanguage')
if messageType.data?(msg)?
return TAPi18n.__(messageType.message, messageType.data(msg), language)
else
return TAPi18n.__(messageType.message, {}, language)
else
if msg.u?.username is RocketChat.settings.get('Chatops_Username')
msg.html = msg.msg
return msg.html
msg.html = msg.msg
if _.trim(msg.html) isnt ''
msg.html = _.escapeHTML msg.html
# message = RocketChat.callbacks.run 'renderMessage', msg
msg.html = msg.html.replace /\n/gm, '<br/>'
return msg.html

@ -26,13 +26,15 @@ Package.onUse(function(api) {
api.use('kadira:flow-router', 'client');
// DEBUGGER
api.addFiles('server/lib/debug.js');
api.addFiles('server/lib/debug.js', 'server');
// COMMON LIB
api.addFiles('lib/core.coffee');
api.addFiles('lib/settings.coffee');
api.addFiles('lib/callbacks.coffee');
api.addFiles('lib/slashCommand.coffee');
api.addFiles('lib/Message.coffee');
api.addFiles('lib/MessageTypes.coffee');
// SERVER LIB
api.addFiles('server/lib/RateLimiter.coffee', 'server');
@ -92,7 +94,6 @@ Package.onUse(function(api) {
api.addFiles('client/Notifications.coffee', 'client');
api.addFiles('client/TabBar.coffee', 'client');
api.addFiles('client/MessageAction.coffee', 'client');
api.addFiles('client/MessageTypes.coffee', 'client');
// VERSION
api.addFiles('rocketchat.info');

@ -30,7 +30,7 @@ RocketChat.sendMessage = (user, message, room, options) ->
###
Meteor.defer ->
RocketChat.callbacks.run 'afterSaveMessage', message
RocketChat.callbacks.run 'afterSaveMessage', message, room
###
Update all the room activity tracker fields

@ -147,14 +147,17 @@ RocketChat.settings.init = ->
RocketChat.models.Settings.find().observe
added: (record) ->
Meteor.settings[record._id] = record.value
process.env[record._id] = record.value
if record.env is true
process.env[record._id] = record.value
RocketChat.settings.load record._id, record.value, initialLoad
changed: (record) ->
Meteor.settings[record._id] = record.value
process.env[record._id] = record.value
if record.env is true
process.env[record._id] = record.value
RocketChat.settings.load record._id, record.value, initialLoad
removed: (record) ->
delete Meteor.settings[record._id]
delete process.env[record._id]
if record.env is true
delete process.env[record._id]
RocketChat.settings.load record._id, undefined, initialLoad
initialLoad = false

@ -23,4 +23,8 @@ Meteor.methods
Meteor.runAsUser userData._id, ->
Meteor.call 'setUsername', userData.username
canEditUserPassword = RocketChat.authz.hasPermission( user._id, 'edit-other-user-password')
if canEditUserPassword and userData.password.trim()
Accounts.setPassword userData._id, userData.password.trim()
return true

@ -116,6 +116,14 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base
return @find(query, options)?.fetch?()?[0]?.ts
findByRoomIdAndMessageIds: (rid, messageIds, options) ->
query =
rid: rid
_id:
$in: messageIds
return @find query, options
cloneAndSaveAsHistoryById: (_id) ->
me = RocketChat.models.Users.findOneById Meteor.userId()
record = @findOneById _id
@ -134,7 +142,6 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base
return @insert record
# UPDATE
setHiddenById: (_id, hidden=true) ->
query =
@ -276,9 +283,6 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base
message = user.username
return @createWithTypeRoomIdMessageAndUser 'au', roomId, message, user, extraData
createRoomRenamedWithRoomIdRoomNameAndUser: (roomId, roomName, user, extraData) ->
return @createWithTypeRoomIdMessageAndUser 'r', roomId, roomName, user, extraData
createCommandWithRoomIdAndUser: (command, roomId, user, extraData) ->
return @createWithTypeRoomIdMessageAndUser 'command', roomId, command, user, extraData

@ -147,6 +147,17 @@ RocketChat.models.Rooms = new class extends RocketChat.models._Base
return @find query, options
findByTypeAndArchivationState: (type, archivationstate, options) ->
query =
t: type
if archivationstate
query.archived = true
else
query.archived = { $ne: true }
return @find query, options
findByVisitorToken: (visitorToken, options) ->
query =
"v.token": visitorToken
@ -320,6 +331,16 @@ RocketChat.models.Rooms = new class extends RocketChat.models._Base
return @update query, update
setTopicById: (_id, topic) ->
query =
_id: _id
update =
$set:
topic: topic
return @update query, update
muteUsernameByRoomId: (_id, username) ->
query =
_id: _id

@ -54,7 +54,7 @@ RocketChat.models.Subscriptions = new class extends RocketChat.models._Base
update =
$set:
alert: false
open: false
open: true
archived: false
return @update query, update

@ -71,6 +71,24 @@ RocketChat.models.Users = new class extends RocketChat.models._Base
return @find query, options
findActiveByUsernameRegexWithExceptions: (username, exceptions = [], options = {}) ->
console.log 'findActiveByUsernameRegexWithExceptions', username, exceptions
if not _.isArray exceptions
exceptions = [ exceptions ]
usernameRegex = new RegExp username, "i"
query =
$and: [
{ active: true }
{ username: { $nin: exceptions } }
{ username: usernameRegex }
]
# username: { $regex: usernameRegex, $nin: exceptions }
# username: { $nin: exceptions }
console.log 'findActiveByUsernameRegexWithExceptions query', JSON.stringify query, null, ' '
return @find query, options
findByActiveUsersNameOrUsername: (nameOrUsername, options) ->
query =
username:

@ -72,6 +72,7 @@ RocketChat.settings.addGroup 'FileUpload', ->
RocketChat.settings.addGroup 'General', ->
@add 'Site_Url', __meteor_runtime_config__?.ROOT_URL, { type: 'string', i18nDescription: 'Site_Url_Description', public: true }
@add 'Site_Name', 'Rocket.Chat', { type: 'string', public: true }
@add 'Language', '', { type: 'language', public: true }
@add 'Allow_Invalid_SelfSigned_Certs', false, { type: 'boolean' }
@add 'Disable_Favorite_Rooms', false, { type: 'boolean' }
@add 'CDN_PREFIX', '', { type: 'string' }
@ -90,10 +91,10 @@ RocketChat.settings.addGroup 'API', ->
RocketChat.settings.addGroup 'SMTP', ->
@add 'SMTP_Host', '', { type: 'string' }
@add 'SMTP_Port', '', { type: 'string' }
@add 'SMTP_Username', '', { type: 'string' }
@add 'SMTP_Password', '', { type: 'string' }
@add 'SMTP_Host', '', { type: 'string', env: true }
@add 'SMTP_Port', '', { type: 'string', env: true }
@add 'SMTP_Username', '', { type: 'string', env: true }
@add 'SMTP_Password', '', { type: 'string', env: true }
@add 'From_Email', '', { type: 'string', placeholder: 'email@domain' }
@section 'Invitation', ->

@ -36,3 +36,5 @@ accounts-password
standard-minifiers
tap:i18n
kevohagan:sweetalert
ecmascript
es5-shim

@ -27,6 +27,7 @@ ecmascript@0.1.6
ecmascript-runtime@0.2.6
ejson@1.0.7
email@1.0.8
es5-shim@4.1.14
geojson-utils@1.0.4
html-tools@1.0.5
htmljs@1.0.5

@ -93,7 +93,7 @@ class @ChatMessages
showError error.reason
if not Meteor.userId()
Meteor.call 'registerGuest', visitor.getToken(), (error, result) ->
Meteor.call 'livechat:registerGuest', { token: visitor.getToken() }, (error, result) ->
if error?
return showError error.reason

@ -2,3 +2,4 @@
@ChatRoom = new Meteor.Collection 'rocketchat_room'
@Settings = new Meteor.Collection 'rocketchat_settings'
@Trigger = new Meteor.Collection 'rocketchat_livechat_trigger'
@Department = new Meteor.Collection 'rocketchat_livechat_department'

@ -30,6 +30,8 @@
ts = new Date
Meteor.call 'loadHistory', rid, ts, limit, undefined, (err, result) ->
return if err?
for item in result?.messages or []
if item.t isnt 'command'
ChatMessage.upsert {_id: item._id}, item

@ -1,9 +1,20 @@
this.Triggers = (function() {
var triggers = [];
var initiated = false;
var requests = [];
var init = function() {
initiated = true;
Tracker.autorun(function() {
triggers = Trigger.find().fetch();
if (requests.length > 0 && triggers.length > 0) {
requests.forEach(function(request) {
processRequest(request);
});
requests = [];
}
});
};
@ -14,13 +25,17 @@ this.Triggers = (function() {
}
actions.forEach(function(action) {
if (action.name === 'send-message') {
var room = Random.id();
visitor.setRoom(room);
var roomId = visitor.getRoom();
if (!roomId) {
roomId = Random.id();
visitor.setRoom(roomId);
}
Session.set('triggered', true);
ChatMessage.insert({
msg: action.params.msg,
rid: room,
rid: roomId,
u: {
username: action.params.name
}
@ -32,6 +47,9 @@ this.Triggers = (function() {
};
var processRequest = function(request) {
if (!initiated) {
return requests.push(request);
}
triggers.forEach(function(trigger) {
trigger.conditions.forEach(function(condition) {
switch (condition.name) {

@ -9,4 +9,4 @@ FlowRouter.route '/livechat',
]
action: ->
BlazeLayout.render 'main', {center: 'room'}
BlazeLayout.render 'main', {center: 'livechatWindow'}

@ -1,5 +1,5 @@
@header-min-height: 30px;
@footer-min-height: 42px;
@footer-min-height: 55px;
@rooms-box-width: 260px;
@flex-tab-width: 400px;

@ -1,4 +1,3 @@
@import url(//fonts.googleapis.com/css?family=Roboto:300,400,500,600,700&subset=latin,cyrillic-ext,greek-ext,greek,vietnamese,latin-ext,cyrillic);
@import "_variables.less";
@import "utils/_lesshat.import.less";
@import "utils/_reset.import.less";
@ -13,11 +12,9 @@ html, body {
height: 100%;
}
body {
padding: 0;
margin: 0;
font-size: 10pt;
font-family: "Roboto", "HelveticaNeue", sans-serif;
// font-size: 14px;
font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif, "Meiryo UI";
font-size: 0.8rem;
color: @primary-font-color;
height: 100%;
width: 100%;
@ -36,6 +33,11 @@ textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
padding: 5px;
margin: 5px;
border: 1px solid #E7E7E7;
border-radius: 5px;
outline: none;
}
input:focus {
@ -54,6 +56,7 @@ input:focus {
word-spacing: 0;
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.125);
border: none;
border-radius: 0;
line-height: 16px;
position: relative;
cursor: pointer;background-color: #FFF;
@ -360,16 +363,14 @@ input:focus {
border-right: 1px solid #E7E7E7;
.input-wrapper {
padding: 6px;
padding: 6px 6px 0 6px;
textarea {
display: block;
padding: 6px 8px;
padding-right: 38px;
overflow-y: auto;
resize: none;
border: 1px solid #E7E7E7;
// margin: 10px;
border-radius: 5px;
margin: 0;
max-height: 200px;
width: 100%;
font-size: 12px;
@ -378,7 +379,6 @@ input:focus {
line-height: normal;
background-color: #fff;
position: relative;
outline: none;
}
}
}
@ -399,11 +399,10 @@ input:focus {
border-right: 1px solid #E7E7E7;
padding: 5px;
input, button {
input, button, select {
display: block;
padding: 5px;
margin: 5px;
}
.error {
display: none;
// width: 100%;
@ -484,6 +483,31 @@ input:focus {
}
}
.powered-by {
text-align: right;
font-size: 0.65rem;
height: 20px;
line-height: 20px;
color: #666;
padding: 0 1em;
opacity: 0.5;
.transition(opacity .15s ease-out);
&:hover {
opacity: 1;
}
a {
text-decoration: none;
margin-left: 1px;
img {
height: 14px;
vertical-align: middle;
}
}
}
@media all and(max-height: 200px) {
.livechat-room {
.title {

@ -1,4 +1,4 @@
<template name="room">
<template name="livechatWindow">
{{#if livechatStartedEnabled}}
<div class="livechat-room">
<div class="title" style="background-color:{{color}}">
@ -12,10 +12,10 @@
</div>
{{#if livechatEnabled}}
{{#if showMessages}}
{{> messages}}
{{else}}
{{#if showRegisterForm}}
{{> register}}
{{else}}
{{> messages}}
{{/if}}
{{else}}
<div class="offline">{{_ "We_are_offline_Sorry_for_the_inconvenience"}}</div>

@ -1,60 +1,62 @@
Template.room.helpers({
title: function() {
Template.livechatWindow.helpers({
title() {
var ref;
if (!Template.instance().subscriptionsReady()) {
return '';
}
return ((ref = Settings.findOne('Livechat_title')) != null ? ref.value : void 0) || 'Rocket.Chat';
},
color: function() {
color() {
var ref;
if (!Template.instance().subscriptionsReady()) {
return 'transparent';
}
return ((ref = Settings.findOne('Livechat_title_color')) != null ? ref.value : void 0) || '#C1272D';
},
popoutActive: function() {
popoutActive() {
return FlowRouter.getQueryParam('mode') === 'popout';
},
showMessages: function() {
return Session.get('triggered') || Meteor.userId();
showRegisterForm() {
if (Session.get('triggered') || Meteor.userId()) {
return false;
}
var form = Settings.findOne('Livechat_registration_form');
return form.value;
},
livechatStartedEnabled: function() {
livechatStartedEnabled() {
return Template.instance().startedEnabled.get() !== null;
},
livechatEnabled: function() {
livechatEnabled() {
return Template.instance().startedEnabled.get();
}
});
Template.room.events({
'click .title': function() {
Template.livechatWindow.events({
'click .title'() {
parentCall('toggleWindow');
},
'click .popout': function(event) {
'click .popout'(event) {
event.stopPropagation();
parentCall('openPopout');
}
});
Template.room.onCreated(function() {
self = this;
self.startedEnabled = new ReactiveVar(null);
Template.livechatWindow.onCreated(function() {
this.startedEnabled = new ReactiveVar(null);
self.subscribe('settings', ['Livechat_title', 'Livechat_title_color', 'Livechat_enabled']);
this.subscribe('settings', ['Livechat_title', 'Livechat_title_color', 'Livechat_enabled', 'Livechat_registration_form']);
var initialCheck = true;
self.autorun(function() {
if (self.subscriptionsReady()) {
this.autorun(() => {
if (this.subscriptionsReady()) {
var enabled = Settings.findOne('Livechat_enabled');
if (enabled !== undefined) {
if (!enabled.value && initialCheck) {
parentCall('removeWidget');
}
initialCheck = false;
self.startedEnabled.set(enabled.value);
this.startedEnabled.set(enabled.value);
}
}
});

@ -19,5 +19,11 @@
<div class="input-wrapper">
<textarea class="input-message" placeholder="Type your message"></textarea>
</div>
<p class="powered-by">
Powered by
<a href="https://rocket.chat" target="_blank">
<img class="logo" src="/images/logo/logo-dark.svg?v=1">
</a>
</p>
</div>
</template>

@ -1,50 +0,0 @@
Template.register.helpers
error: ->
return Template.instance().error.get()
title: ->
return '' unless Template.instance().subscriptionsReady()
return Settings.findOne('Livechat_title')?.value or 'Rocket.Chat'
color: ->
return 'transparent' unless Template.instance().subscriptionsReady()
return Settings.findOne('Livechat_title_color')?.value or '#C1272D'
welcomeMessage: ->
return ""
Template.register.events
'submit #livechat-registration': (e, instance) ->
e.preventDefault()
$name = instance.$('input[name=name]')
$email = instance.$('input[name=email]')
unless $name.val().trim() and $email.val().trim()
return instance.showError TAPi18n.__('Please_fill_name_and_email')
else
Meteor.call 'registerGuest', visitor.getToken(), $name.val(), $email.val(), (error, result) ->
if error?
return instance.showError error.reason
Meteor.loginWithToken result.token, (error) ->
if error
return instance.showError error.reason
'click .error': (e, instance) ->
instance.hideError()
Template.register.onCreated ->
@subscribe 'settings', ['Livechat_title', 'Livechat_title_color']
@error = new ReactiveVar
@showError = (msg) =>
$('.error').addClass('show')
@error.set msg
return
@hideError = =>
$('.error').removeClass('show')
@error.set()
return

@ -9,7 +9,16 @@
</label>
<input type="text" name="name" id="guestName" placeholder="{{_ "Name"}}">
<input type="email" name="email" id="guestEmail" placeholder="{{_ "E-mail"}}">
<button type="submit" id="btnEntrar" class="-btn"> {{_ "Start_Chat"}} </button>
{{#if hasDepartments}}
<select name="department">
<option value="">{{_ "Select_a_department"}}</option>
{{#each departments}}
<option value="{{_id}}">{{name}}</option>
{{/each}}
</select>
{{/if}}
<button type="submit" id="btnEntrar" class="button"> {{_ "Start_Chat"}} </button>
</form>
</template>

@ -0,0 +1,68 @@
Template.register.helpers({
error() {
return Template.instance().error.get();
},
welcomeMessage() {
return "";
},
hasDepartments() {
return Department.find().count() > 1;
},
departments() {
return Department.find();
}
});
Template.register.events({
'submit #livechat-registration' (e, instance) {
var $email, $name;
e.preventDefault();
$name = instance.$('input[name=name]');
$email = instance.$('input[name=email]');
if (!($name.val().trim() && $email.val().trim())) {
return instance.showError(TAPi18n.__('Please_fill_name_and_email'));
} else {
var departmentId = instance.$('select[name=department]').val();
if (!departmentId) {
var department = Department.findOne();
if (department) {
departmentId = department._id;
}
}
var guest = {
token: visitor.getToken(),
name: $name.val(),
email: $email.val(),
department: departmentId
};
Meteor.call('livechat:registerGuest', guest, function(error, result) {
if (error != null) {
return instance.showError(error.reason);
}
Meteor.loginWithToken(result.token, function(error) {
if (error) {
return instance.showError(error.reason);
}
});
});
}
},
'click .error' (e, instance) {
return instance.hideError();
}
});
Template.register.onCreated(function() {
this.subscribe('livechat:availableDepartments');
this.error = new ReactiveVar;
this.showError = (msg) => {
$('.error').addClass('show');
this.error.set(msg);
};
this.hideError = () => {
$('.error').removeClass('show');
this.error.set();
};
});

@ -8,10 +8,11 @@
"Installation" : "Installation",
"Please_answer_survey" : "Please take a moment to answer a quick survey about this chat",
"Please_fill_name_and_email" : "Please fill name and e-mail",
"Select_a_department" : "Select a department",
"Skip" : "Skip",
"Start_Chat" : "Start Chat",
"Survey" : "Survey",
"Survey_instructions" : "Rate each question according to your satisfaction, 1 meaning you are completely unsatisfied and 5 meaning you are completely satisfied.",
"Thank_you_for_your_feedback" : "Thank you for your feedback",
"We_are_offline_Sorry_for_the_inconvenience" : "We are offline. Sorry for the inconvenience."
}
}

@ -119,7 +119,7 @@
w.addEventListener('message', function(msg) {
if (typeof msg.data === 'object' && msg.data.src !== undefined && msg.data.src === 'rocketchat') {
if (api[msg.data.fn] !== undefined && typeof api[msg.data.fn] === 'function') {
var args = [].concat(msg.data.args || [])
var args = [].concat(msg.data.args || []);
api[msg.data.fn].apply(null, args);
}
}

@ -0,0 +1 @@
this.AgentUsers = new Mongo.Collection('agentUsers');

@ -0,0 +1 @@
this.LivechatDepartmentAgents = new Mongo.Collection('rocketchat_livechat_department_agents');

@ -1,15 +1,3 @@
FlowRouter.route('/live/:name', {
name: 'live',
action: function(params, queryParams) {
console.log('action route livechat');
Session.set('showUserInfo');
openRoom('l', params.name);
},
triggersExit: [roomExit]
});
livechatManagerRoutes = FlowRouter.group({
prefix: '/livechat-manager',
name: 'livechat-manager'

@ -411,3 +411,29 @@
}
}
}
.department-agents {
list-style-type: none;
li {
display: inline-block;
background-color: #DDD;
border-radius: 10px;
padding: 2px 8px 2px 2px;
margin: 1px 0;
cursor: pointer;
.icon-plus-circled {
opacity: 0.5;
font-size: 0.8rem;
}
}
}
.agent-info {
input[type='text'] {
width: auto;
line-height: 24px;
height: 24px;
}
}

@ -24,34 +24,51 @@
</div>
<hr />
<h2>{{_ "Agents"}}</h2>
<div class="input-line double-col">
<input type="text" name="agent" placeholder="{{_ "Enter_a_username"}}">
<button name="addAgent" type="button" class="button add-agent">{{_ "Add_agent"}}</button>
</div>
<div class="list">
<table>
<thead>
<tr>
<th width="25%">{{_ "Username"}}</th>
<th>{{_ "Delete"}}</th>
</tr>
</thead>
<tbody>
{{#if agents.length}}
{{#each agents}}
<tr class="agent-info" data-id="{{_id}}">
<td>{{username}}</td>
<td><a href="#remove" class="remove-agent"><i class="icon-trash"></i></a></td>
</tr>
{{/each}}
{{else}}
<fieldset>
<legend>{{_ "Available_agents"}}</legend>
<ul class="department-agents available-agents">
{{#each availableAgents}}
<li><i class="icon-plus-circled"></i>{{username}}</li>
{{/each}}
</ul>
</fieldset>
<fieldset>
<legend>{{_ "Selected_agents"}}</legend>
<div class="list">
<table>
<thead>
<tr>
<td colspan="2">{{_ "There_are_no_agents_added_to_this_department_yet"}}</td>
<th width="25%">{{_ "Username"}}</th>
<th>{{_ "Count"}}</th>
<th>{{_ "Order"}}</th>
<th>&nbsp;</th>
</tr>
{{/if}}
</tbody>
</table>
</div>
</thead>
<tbody>
{{#if selectedAgents}}
{{#each selectedAgents}}
<tr class="agent-info">
<td>{{username}}</td>
<td><input type="text" class="count-{{agentId}}" name="count" value="{{count}}" size="3"></td>
<td><input type="text" class="order-{{agentId}}" name="order" value="{{order}}" size="3"></td>
<td><a href="#remove" class="remove-agent"><i class="icon-trash"></i></a></td>
</tr>
{{/each}}
{{else}}
<tr>
<td colspan="4">{{_ "There_are_no_agents_added_to_this_department_yet"}}</td>
</tr>
{{/if}}
</tbody>
</table>
</div>
</fieldset>
</fieldset>
<div class="submit">
<button type="button" class="button secondary back"><i class="icon-left-big"></i><span>{{_ "Back"}}</span></button>

@ -1,10 +1,16 @@
Template.livechatDepartmentForm.helpers({
department() {
// return Template.instance().department && !_.isEmpty(Template.instance().department.get()) ? Template.instance().department.get() : { enabled: true };
return Template.instance().department.get();
},
agents() {
return Template.instance().department && !_.isEmpty(Template.instance().department.get()) ? Template.instance().department.get().agents : []
},
selectedAgents() {
return _.sortBy(Template.instance().selectedAgents.get(), 'username');
},
availableAgents() {
var selected = _.pluck(Template.instance().selectedAgents.get(), 'username');
return AgentUsers.find({ username: { $nin: selected }}, { sort: { username: 1 } });
}
});
@ -29,16 +35,22 @@ Template.livechatDepartmentForm.events({
var oldBtnValue = $btn.html();
$btn.html(t('Saving'));
agents = instance.department && !_.isEmpty(instance.department.get()) ? instance.department.get().agents : [];
departmentData = {
var departmentData = {
enabled: enabled === "1" ? true : false,
name: name.trim(),
description: description.trim(),
agents: agents
}
description: description.trim()
};
var departmentAgents = [];
instance.selectedAgents.get().forEach((agent) => {
agent.count = instance.$('.count-' + agent.agentId).val();
agent.order = instance.$('.order-' + agent.agentId).val();
departmentAgents.push(agent);
});
Meteor.call('livechat:saveDepartment', _id, departmentData, function(error, result) {
Meteor.call('livechat:saveDepartment', _id, departmentData, departmentAgents, function(error, result) {
$btn.html(oldBtnValue);
if (error) {
return toastr.error(t(error.reason || error.error));
@ -54,59 +66,44 @@ Template.livechatDepartmentForm.events({
FlowRouter.go('livechat-departments');
},
'click button.add-agent' (e, instance) {
'click .remove-agent' (e, instance) {
e.preventDefault();
var $btn = $(e.currentTarget);
var $agent = instance.$('input[name=agent]')
if ($agent.val().trim() === '') {
return toastr.error(t('Please_fill_a_username'));
}
var oldBtnValue = $btn.html();
$btn.html(t('Saving'));
Meteor.call('livechat:searchAgent', $agent.val(), function(error, user) {
$btn.html(oldBtnValue);
if (error) {
return toastr.error(t(error.reason || error.error));
}
department = instance.department.get() || {};
if (department.agents === undefined || !_.isArray(department.agents)) {
department.agents = [];
}
if (!_.findWhere(department.agents, { _id: user._id })) {
department.agents.push(user);
}
instance.department.set(department);
$agent.val('');
});
var selectedAgents = instance.selectedAgents.get();
selectedAgents = _.reject(selectedAgents, (agent) => { return agent._id === this._id });
instance.selectedAgents.set(selectedAgents);
},
'click a.remove-agent' (e, instance) {
e.preventDefault();
department = instance.department.get();
department.agents = _.reject(department.agents, (agent) => { return agent._id === this._id });
instance.department.set(department);
},
'keydown input[name=agent]' (e, instance) {
if (e.keyCode === 13) {
e.preventDefault();
$("button.add-agent").click();
}
'click .available-agents li' (e, instance) {
var selectedAgents = instance.selectedAgents.get();
var agent = _.clone(this);
agent.agentId = this._id;
delete agent._id;
selectedAgents.push(agent);
instance.selectedAgents.set(selectedAgents);
}
});
Template.livechatDepartmentForm.onCreated(function() {
this.department = new ReactiveVar({ enabled: true });
this.selectedAgents = new ReactiveVar([]);
this.subscribe('livechat:agents');
this.autorun(() => {
var sub = this.subscribe('livechat:departments', FlowRouter.getParam('_id'));
if (sub.ready()) {
department = LivechatDepartment.findOne({ _id: FlowRouter.getParam('_id') });
if (department) {
this.department.set(department);
this.subscribe('livechat:departmentAgents', department._id, () => {
var newSelectedAgents = [];
LivechatDepartmentAgents.find({ departmentId: department._id }).forEach((agent) => {
newSelectedAgents.push(agent);
});
this.selectedAgents.set(newSelectedAgents);
});
}
}
});

@ -1,11 +1,6 @@
Template.livechatDepartments.helpers({
"departments": () => {
return LivechatDepartment.find();
},
"numAgents"() {
if (Array.isArray(this.agents)) {
return this.agents.length;
}
}
});

@ -1,8 +1,6 @@
var AgentUsers;
var ManagerUsers;
Meteor.startup(function() {
AgentUsers = new Mongo.Collection('agentUsers');
ManagerUsers = new Mongo.Collection('managerUsers');
});

@ -10,7 +10,7 @@
<li>
<!-- <a href="{{pathFor 'livechat-dashboard'}}" class="{{active 'livechat-dashboard'}}">{{_ "Dashboard"}}</a> -->
<a href="{{pathFor 'livechat-users'}}" class="{{active 'livechat-users'}}">{{_ "User_management"}}</a>
<a href="{{pathFor 'livechat-departments'}}" class="{{active 'livechat-departments'}}">{{_ "Departments"}}</a>
<a href="{{pathFor 'livechat-departments'}}" class="{{active 'livechat-departments' 'livechat-department-edit'}}">{{_ "Departments"}}</a>
<a href="{{pathFor 'livechat-triggers'}}" class="{{active 'livechat-triggers'}}">{{_ "Triggers"}}</a>
<a href="{{pathFor 'livechat-installation'}}" class="{{active 'livechat-installation'}}">{{_ "Installation"}}</a>
<a href="{{pathFor 'livechat-appearance'}}" class="{{active 'livechat-appearance'}}">{{_ "Appearance"}}</a>

@ -1,7 +1,7 @@
Template.livechatFlex.helpers({
active (route) {
active (...routes) {
FlowRouter.watchPathChange();
if (FlowRouter.current().route.name === route) {
if (routes.indexOf(FlowRouter.current().route.name) !== -1) {
return 'active';
}
}

@ -3,4 +3,5 @@ Meteor.startup(function() {
RocketChat.settings.add('Livechat_title' , 'Rocket.Chat', { type: 'string', group: 'Livechat', public: true });
RocketChat.settings.add('Livechat_title_color' , '#C1272D', { type: 'string', group: 'Livechat', public: true });
RocketChat.settings.add('Livechat_enabled' , true, { type: 'boolean', group: 'Livechat', public: true });
RocketChat.settings.add('Livechat_registration_form' , true, { type: 'boolean', group: 'Livechat', public: true, i18nLabel: 'Show_preregistration_form' });
});

@ -4,9 +4,11 @@
"Add_manager" : "Add manager",
"Agent_added" : "Agent added",
"Agent_removed" : "Agent removed",
"Available_agents" : "Available agents",
"Back" : "Back",
"Closed" : "Closed",
"Copy_to_clipboard" : "Copy to clipboard",
"Count" : "Count",
"Dashboard" : "Dashboard",
"Department_not_found" : "Department not found",
"Department_removed" : "Department removed",
@ -32,11 +34,14 @@
"New_Department" : "New Department",
"Num_Agents" : "# Agents",
"Opened" : "Opened",
"Order" : "Order",
"Please_fill_a_name" : "Please fill a name",
"Please_fill_a_username" : "Please fill a username",
"Please_select_enabled_yes_or_no" : "Please select an option for Enabled",
"Saved" : "Saved",
"Selected_agents" : "Selected agents",
"Send_a_message" : "Send a message",
"Show_preregistration_form" : "Show pre-registration form",
"Theme" : "Theme",
"There_are_no_agents_added_to_this_department_yet" : "There are no agents added to this department yet.",
"Time_in_seconds" : "Time in seconds",
@ -48,4 +53,4 @@
"Username_not_found" : "Username not found",
"Visitor_page_URL" : "Visitor page URL",
"Visitor_time_on_site" : "Visitor time on site"
}
}

@ -38,6 +38,12 @@ Package.onUse(function(api) {
api.addFiles('client/stylesheets/livechat.less', 'client');
// collections
api.addFiles('client/collections/AgentUsers.js', 'client');
api.addFiles('client/collections/LivechatDepartment.js', 'client');
api.addFiles('client/collections/LivechatDepartmentAgents.js', 'client');
api.addFiles('client/collections/LivechatTrigger.js', 'client');
// client views
api.addFiles('client/views/app/livechatAppearance.html', 'client');
api.addFiles('client/views/app/livechatAppearance.js', 'client');
@ -80,13 +86,15 @@ Package.onUse(function(api) {
api.addFiles('server/models/Users.js', 'server');
api.addFiles('server/models/Rooms.js', 'server');
api.addFiles('server/models/LivechatDepartment.js', 'server');
api.addFiles('server/models/LivechatDepartmentAgents.js', 'server');
api.addFiles('server/models/LivechatTrigger.js', 'server');
// collections
api.addFiles('client/lib/LivechatDepartment.js', 'client');
api.addFiles('client/lib/LivechatTrigger.js', 'client');
// server lib
api.addFiles('server/lib/getNextAgent.js', 'server');
// publications
api.addFiles('server/publications/availableDepartments.js', 'server');
api.addFiles('server/publications/departmentAgents.js', 'server');
api.addFiles('server/publications/livechatAgents.js', 'server');
api.addFiles('server/publications/livechatManagers.js', 'server');
api.addFiles('server/publications/livechatDepartments.js', 'server');

@ -1,27 +1,8 @@
this.getNextAgent = function(department) {
var agentFilter = {};
// find agents from that department
if (department) {
var agents = RocketChat.models.LivechatDepartment.getNextAgent(department);
if (!agents) {
return;
}
// sort = {
// count: 1,
// order: 1,
// 'user.name': 1
// }
// update = {
// $inc: {
// count: 1
// }
// }
// queueUser = findAndModify query, sort, update
return RocketChat.models.LivechatDepartmentAgents.getNextAgentForDepartment(department);
} else {
return RocketChat.models.Users.getNextAgent();
}

@ -1,5 +1,5 @@
Meteor.methods({
registerGuest: function(token, name, email) {
'livechat:registerGuest': function({ token, name, email, department } = {}) {
var pass, qt, user, userData, userExists, userId, inc = 0;
check(token, String);
@ -35,7 +35,8 @@ Meteor.methods({
}
userData = {
username: user,
globalRoles: 'livechat-guest'
globalRoles: 'livechat-guest',
department: department
};
userId = Accounts.insertUserDoc({}, userData);

@ -1,5 +1,5 @@
Meteor.methods({
'livechat:saveDepartment' (_id, departmentData) {
'livechat:saveDepartment' (_id, departmentData, departmentAgents) {
if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'view-livechat-manager')) {
throw new Meteor.Error("not-authorized");
}
@ -17,6 +17,6 @@ Meteor.methods({
}
}
return RocketChat.models.LivechatDepartment.createOrUpdateDepartment(_id, departmentData.enabled, departmentData.name, departmentData.description, departmentData.agents);
return RocketChat.models.LivechatDepartment.createOrUpdateDepartment(_id, departmentData.enabled, departmentData.name, departmentData.description, departmentAgents);
}
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save