diff --git a/.meteor/packages b/.meteor/packages index 3caa7be9e165499ccb1e792cba05949087fd1dc3..0585cdb7b02f6ecbfce67bec4bbf03db819f2a12 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -93,6 +93,7 @@ rocketchat:slashcommands-me rocketchat:slashcommands-mute rocketchat:slashcommands-topic rocketchat:slashcommands-unarchive +rocketchat:smarsh-connector rocketchat:spotify rocketchat:statistics rocketchat:streamer diff --git a/.meteor/versions b/.meteor/versions index 9d194ace4cb3b203b730f73b472ee65c83b815f5..f0319f51d354a18feeaac19325db3ba65226ac80 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -189,6 +189,7 @@ rocketchat:slashcommands-mute@0.0.1 rocketchat:slashcommands-open@0.0.1 rocketchat:slashcommands-topic@0.0.1 rocketchat:slashcommands-unarchive@0.0.1 +rocketchat:smarsh-connector@0.0.1 rocketchat:sms@0.0.1 rocketchat:spotify@0.0.1 rocketchat:statistics@0.0.1 diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp index e05904382916053459dd623e3804907aca92bb1c..95b054c07280b96b2a121aa3b09fb53e45de0162 100644 --- a/.sandstorm/sandstorm-pkgdef.capnp +++ b/.sandstorm/sandstorm-pkgdef.capnp @@ -21,7 +21,7 @@ const pkgdef :Spk.PackageDefinition = ( appVersion = 38, # Increment this for every release. - appMarketingVersion = (defaultText = "0.37.1"), + appMarketingVersion = (defaultText = "0.38.0"), # Human-readable representation of appVersion. Should match the way you # identify versions of your app in documentation and marketing. diff --git a/HISTORY.md b/HISTORY.md index d9b10f8ed44803d616989ed72fd319eb2cd98f68..bd1e025731f007fb3829fe561c37bde09e45d42a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,40 @@ ## NEXT +## 0.38.0, 2016-Aug-30 + +- Action links improvements +- Add global event unread-changed-by-subscription +- Add role to disable/enable channel preview (#4127) +- Add room setting to require code to join Room (#4126) +- Add the timer for disconnecting, one minute after going in the background it'll disconnect +- Add Ubuntu 16.04-under 30 seconds snap deployment using SNAPS +- Added File Uploaded text on attachments to i18n +- Added option to populate Rocket Chat with LDAP users (import them) (#4054) +- Changes rtl check in ChatMessages class (#4049) +- Check message timestamp before notifying users +- Do not check for last admin while updating a user +- Don't send offline emails to users who aren't active +- Fix mispelling for seriliazedDescriptor +- Fix multiple notifications (closes #3517) (#4074) +- Fix offering Sandstorm grains without a title +- Fix the verbs in Sandstorm activity events +- Fix user update check for last admin +- Fixed buttons margins and upload file list +- Formatting and adding some missing permissions to standard roles +- Handle locations when disabled +- Improve lazy loading of custom fields and translations +- Improve stream broadcast connection (#4119) +- Improvements/login and registration (#4073) +- Less borders (#4101) +- Make sure Sandstorm.notify is always called for DMs +- Open room correctly after creation and new messages +- Set gitlabs scope to 'api', the only support scope. +- Set message.ts if empty on sendMessage method +- Update moment locales +- Update to Autolinker.js 0.28.0 +- Update to depend only on the gMaps API key, add i18n strings for geolocaiotn sharing +- Updated loginform a11y and UX - labels instead of placeholders (#4075) + ## 0.37.1, 2016-Aug-17 - Allow deletion of records with same id on settings diff --git a/README.md b/README.md index 7b4fb699d3092567a9d2c082565751140ac42c6f..fb04b826603faa2709baa38415f277bcc0f503c3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ * [Sandstorm.io](#sandstormio) * [DPlatform](#dplatform) * [IndieHosters](#indiehosters) + * [Ubuntu 16.04](#ubuntu-1604) * [Cloudron.io](#cloudronio) * [Nitrous.io](#nitrousio) * [Heroku](#heroku) @@ -28,7 +29,6 @@ * [Raspberry Pi 2](#raspberry-pi-2) * [Koozali SME](#koozali-sme) * [Ubuntu VPS](#ubuntu-vps) - * [Ubuntu Software Center](#ubuntu-software-center) * [About Rocket.Chat](#about-rocketchat) * [On the News](#on-the-news) * [Features](#features) @@ -90,6 +90,15 @@ Get your Rocket.Chat instance hosted in a "as a Service" style. You register and [](https://indiehosters.net/shop/product/rocket-chat-21) +## Ubuntu 16.04 +[](https://uappexplorer.com/app/rocketchat-server.rocketchat) + +Deploy from shell: + +`snap install rocketchat-server` + +In under 30 seconds, your Rocket.Chat server will be up and running at `http://host-ip:3000` + ## Cloudron.io Install Rocket.Chat on [Cloudron](https://cloudron.io) Smartserver: @@ -172,11 +181,6 @@ Add Rocket.Chat to this world famous time tested small enterprise server today: ## Ubuntu VPS Follow these [deployment instructions](https://rocket.chat/docs/installation/manual-installation/ubuntu/) -## Ubuntu Software Center -Easy one click install right from your Ubuntu Desktop (coming soon) - -[]() - # About Rocket.Chat Rocket.Chat is a Web Chat Server, developed in JavaScript, using the [Meteor](https://www.meteor.com/install) fullstack framework. @@ -246,7 +250,7 @@ It is a great solution for communities and companies wanting to privately host t - Audio calls - Multi-users Audio Conference - Screensharing -- XMPP bridge ([try it](https://demo.rocket.chat/channel/xmppbridge)) +- XMPP bridge ([try it](https://demo.rocket.chat/channel/general)) - REST APIs - Remote Locations Video Monitoring - Native real-time APIs for Microsoft C#, Visual Basic, F# and other .NET supported languages ([Get it!](https://www.nuget.org/packages/Rocket.Chat.Net/0.0.12-pre)) diff --git a/client/startup/emailVerification.js b/client/startup/emailVerification.js index cae42e4b19b3dc3c636ad935261fc41b7139ce5e..5be8d18838030a603ed5a5cc60510f57ce02c4e2 100644 --- a/client/startup/emailVerification.js +++ b/client/startup/emailVerification.js @@ -1,6 +1,7 @@ Meteor.startup(function() { Tracker.autorun(function() { - if (Meteor.user() && Meteor.user().emails && Meteor.user().emails[0] && Meteor.user().emails[0].verified !== true && RocketChat.settings.get('Accounts_EmailVerification') === true && !Session.get('Accounts_EmailVerification_Warning')) { + var user = Meteor.user(); + if (user && user.emails && user.emails[0] && user.emails[0].verified !== true && RocketChat.settings.get('Accounts_EmailVerification') === true && !Session.get('Accounts_EmailVerification_Warning')) { toastr.warning(TAPi18n.__('You_have_not_verified_your_email')); Session.set('Accounts_EmailVerification_Warning', true); } diff --git a/client/startup/userSetUtcOffset.js b/client/startup/userSetUtcOffset.js new file mode 100644 index 0000000000000000000000000000000000000000..27444599d9135f46b5bcbfedee986d7bdbe016db --- /dev/null +++ b/client/startup/userSetUtcOffset.js @@ -0,0 +1,12 @@ +Meteor.startup(function() { + Tracker.autorun(function() { + var user, utcOffset; + user = Meteor.user(); + if (user && user.statusConnection === 'online') { + utcOffset = moment().utcOffset() / 60; + if (user.utcOffset !== utcOffset) { + Meteor.call('userSetUtcOffset', utcOffset); + } + } + }); +}); diff --git a/client/startup/userTimezone.coffee b/client/startup/userTimezone.coffee deleted file mode 100644 index 0d35691abb6f8904ec455d21de5d769d11baf2df..0000000000000000000000000000000000000000 --- a/client/startup/userTimezone.coffee +++ /dev/null @@ -1,6 +0,0 @@ -Tracker.autorun -> - user = Meteor.user() - if user?.statusConnection is 'online' - utcOffset = moment().utcOffset() / 60 - if user.utcOffset isnt utcOffset - Meteor.call 'updateUserUtcOffset', utcOffset \ No newline at end of file diff --git a/packages/meteor-accounts-saml/saml_server.js b/packages/meteor-accounts-saml/saml_server.js index 4c6816a16af3c83bbfe06a398e06196fef9e5d13..e5d6359200d1f9abb6b77cb628449b595cc2c610 100644 --- a/packages/meteor-accounts-saml/saml_server.js +++ b/packages/meteor-accounts-saml/saml_server.js @@ -110,6 +110,8 @@ Accounts.registerLoginHandler(function(loginRequest) { if (username) { newUser.username = username; } + } else if (loginResult.profile.username) { + newUser.username = loginResult.profile.username; } var userId = Accounts.insertUserDoc({}, newUser); diff --git a/packages/meteor-autocomplete/autocomplete-client.coffee b/packages/meteor-autocomplete/autocomplete-client.coffee new file mode 100755 index 0000000000000000000000000000000000000000..b58875785139042817b8edf03b41f032651c88c2 --- /dev/null +++ b/packages/meteor-autocomplete/autocomplete-client.coffee @@ -0,0 +1,367 @@ +AutoCompleteRecords = new Mongo.Collection("autocompleteRecords") + +isServerSearch = (rule) -> _.isString(rule.collection) + +validateRule = (rule) -> + if rule.subscription? and not Match.test(rule.collection, String) + throw new Error("Collection name must be specified as string for server-side search") + + # XXX back-compat message, to be removed + if rule.callback? + console.warn("autocomplete no longer supports callbacks; use event listeners instead.") + +isWholeField = (rule) -> + # either '' or null both count as whole field. + return !rule.token + +getRegExp = (rule) -> + unless isWholeField(rule) + # Expressions for the range from the last word break to the current cursor position + new RegExp('(^|\\b|\\s)' + rule.token + '([\\w.]*)$') + else + # Whole-field behavior - word characters or spaces + new RegExp('(^)(.*)$') + +getFindParams = (rule, filter, limit) -> + # This is a different 'filter' - the selector from the settings + # We need to extend so that we don't copy over rule.filter + selector = _.extend({}, rule.filter || {}) + options = { limit: limit } + + # Match anything, no sort, limit X + return [ selector, options ] unless filter + + if rule.sort and rule.field + sortspec = {} + # Only sort if there is a filter, for faster performance on a match of anything + sortspec[rule.field] = 1 + options.sort = sortspec + + if _.isFunction(rule.selector) + # Custom selector + _.extend(selector, rule.selector(filter)) + else + selector[rule.field] = { + $regex: if rule.matchAll then filter else "^" + filter + # default is case insensitive search - empty string is not the same as undefined! + $options: if (typeof rule.options is 'undefined') then 'i' else rule.options + } + + return [ selector, options ] + +getField = (obj, str) -> + obj = obj[key] for key in str.split(".") + return obj + +class @AutoComplete + + @KEYS: [ + 40, # DOWN + 38, # UP + 13, # ENTER + 27, # ESCAPE + 9 # TAB + ] + + constructor: (settings) -> + @limit = settings.limit || 5 + @position = settings.position || "bottom" + + @rules = settings.rules + validateRule(rule) for rule in @rules + + @expressions = (getRegExp(rule) for rule in @rules) + + @matched = -1 + @loaded = true + + # Reactive dependencies for current matching rule and filter + @ruleDep = new Deps.Dependency + @filterDep = new Deps.Dependency + @loadingDep = new Deps.Dependency + + # autosubscribe to the record set published by the server based on the filter + # This will tear down server subscriptions when they are no longer being used. + @sub = null + @comp = Deps.autorun => + # Stop any existing sub immediately, don't wait + @sub?.stop() + + return unless (rule = @matchedRule()) and (filter = @getFilter()) isnt null + + # subscribe only for server-side collections + unless isServerSearch(rule) + @setLoaded(true) # Immediately loaded + return + + [ selector, options ] = getFindParams(rule, filter, @limit) + + # console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field + @setLoaded(false) + subName = rule.subscription || "autocomplete-recordset" + @sub = Meteor.subscribe(subName, + selector, options, rule.collection, => @setLoaded(true)) + + teardown: -> + # Stop the reactive computation we started for this autocomplete instance + @comp.stop() + + # reactive getters and setters for @filter and the currently matched rule + matchedRule: -> + @ruleDep.depend() + if @matched >= 0 then @rules[@matched] else null + + setMatchedRule: (i) -> + @matched = i + @ruleDep.changed() + + getFilter: -> + @filterDep.depend() + return @filter + + setFilter: (x) -> + @filter = x + @filterDep.changed() + return @filter + + isLoaded: -> + @loadingDep.depend() + return @loaded + + setLoaded: (val) -> + return if val is @loaded # Don't cause redraws unnecessarily + @loaded = val + @loadingDep.changed() + + onKeyUp: -> + return unless @$element # Don't try to do this while loading + startpos = @element.selectionStart + val = @getText().substring(0, startpos) + + ### + Matching on multiple expressions. + We always go from a matched state to an unmatched one + before going to a different matched one. + ### + i = 0 + breakLoop = false + while i < @expressions.length + matches = val.match(@expressions[i]) + + # matching -> not matching + if not matches and @matched is i + @setMatchedRule(-1) + breakLoop = true + + # not matching -> matching + if matches and @matched is -1 + @setMatchedRule(i) + breakLoop = true + + # Did filter change? + if matches and @filter isnt matches[2] + @setFilter(matches[2]) + breakLoop = true + + break if breakLoop + i++ + + onKeyDown: (e) -> + return if @matched is -1 or (@constructor.KEYS.indexOf(e.keyCode) < 0) + + switch e.keyCode + when 9, 13 # TAB, ENTER + if @select() # Don't jump fields or submit if select successful + e.preventDefault() + e.stopPropagation() + # preventDefault needed below to avoid moving cursor when selecting + when 40 # DOWN + e.preventDefault() + @next() + when 38 # UP + e.preventDefault() + @prev() + when 27 # ESCAPE + @$element.blur() + @hideList() + + return + + onFocus: -> + # We need to run onKeyUp after the focus resolves, + # or the caret position (selectionStart) will not be correct + Meteor.defer => @onKeyUp() + + onBlur: -> + # We need to delay this so click events work + # TODO this is a bit of a hack; see if we can't be smarter + Meteor.setTimeout => + @hideList() + , 500 + + onItemClick: (doc, e) => @processSelection(doc, @rules[@matched]) + + onItemHover: (doc, e) -> + @tmplInst.$(".-autocomplete-item").removeClass("selected") + $(e.target).closest(".-autocomplete-item").addClass("selected") + + filteredList: -> + # @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered + filter = @getFilter() # Reactively depend on the filter + return null if @matched is -1 + + rule = @rules[@matched] + # Don't display list unless we have a token or a filter (or both) + # Single field: nothing displayed until something is typed + return null unless rule.token or filter + + [ selector, options ] = getFindParams(rule, filter, @limit) + + Meteor.defer => @ensureSelection() + + # if server collection, the server has already done the filtering work + return AutoCompleteRecords.find({}, options) if isServerSearch(rule) + + # Otherwise, search on client + return rule.collection.find(selector, options) + + isShowing: -> + rule = @matchedRule() + # Same rules as above + showing = rule? and (rule.token or @getFilter()) + + # Do this after the render + if showing + Meteor.defer => + @positionContainer() + @ensureSelection() + + return showing + + # Replace text with currently selected item + select: -> + node = @tmplInst.find(".-autocomplete-item.selected") + return false unless node? + doc = Blaze.getData(node) + return false unless doc # Don't select if nothing matched + + @processSelection(doc, @rules[@matched]) + return true + + processSelection: (doc, rule) -> + replacement = getField(doc, rule.field) + + unless isWholeField(rule) + @replace(replacement, rule) + @hideList() + + else + # Empty string or doesn't exist? + # Single-field replacement: replace whole field + @setText(replacement) + + # Field retains focus, but list is hidden unless another key is pressed + # Must be deferred or onKeyUp will trigger and match again + # TODO this is a hack; see above + @onBlur() + + @$element.trigger("autocompleteselect", doc) + return + + # Replace the appropriate region + replace: (replacement) -> + startpos = @element.selectionStart + fullStuff = @getText() + val = fullStuff.substring(0, startpos) + val = val.replace(@expressions[@matched], "$1" + @rules[@matched].token + replacement) + posfix = fullStuff.substring(startpos, fullStuff.length) + separator = (if posfix.match(/^\s/) then "" else " ") + finalFight = val + separator + posfix + @setText finalFight + + newPosition = val.length + 1 + @element.setSelectionRange(newPosition, newPosition) + return + + hideList: -> + @setMatchedRule(-1) + @setFilter(null) + + getText: -> + return @$element.val() || @$element.text() + + setText: (text) -> + if @$element.is("input,textarea") + @$element.val(text) + else + @$element.html(text) + + ### + Rendering functions + ### + positionContainer: -> + # First render; Pick the first item and set css whenever list gets shown + position = @$element.position() + + rule = @matchedRule() + + offset = getCaretCoordinates(@element, @element.selectionStart) + + # In whole-field positioning, we don't move the container and make it the + # full width of the field. + if rule? and isWholeField(rule) + pos = + left: position.left + width: @$element.outerWidth() # position.offsetWidth + else # Normal positioning, at token word + pos = + left: position.left + offset.left + + # Position menu from top (above) or from bottom of caret (below, default) + if @position is "top" + pos.bottom = @$element.offsetParent().height() - position.top - offset.top + else + pos.top = position.top + offset.top + parseInt(@$element.css('font-size')) + + @tmplInst.$(".-autocomplete-container").css(pos) + + ensureSelection : -> + # Re-render; make sure selected item is something in the list or none if list empty + selectedItem = @tmplInst.$(".-autocomplete-item.selected") + + unless selectedItem.length + # Select anything + @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") + + # Select next item in list + next: -> + currentItem = @tmplInst.$(".-autocomplete-item.selected") + return unless currentItem.length # Don't try to iterate an empty list + currentItem.removeClass("selected") + + next = currentItem.next() + if next.length + next.addClass("selected") + else # End of list or lost selection; Go back to first item + @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") + + # Select previous item in list + prev: -> + currentItem = @tmplInst.$(".-autocomplete-item.selected") + return unless currentItem.length # Don't try to iterate an empty list + currentItem.removeClass("selected") + + prev = currentItem.prev() + if prev.length + prev.addClass("selected") + else # Beginning of list or lost selection; Go to end of list + @tmplInst.$(".-autocomplete-item:last-child").addClass("selected") + + # This doesn't need to be reactive because list already changes reactively + # and will cause all of the items to re-render anyway + currentTemplate: -> @rules[@matched].template + +AutocompleteTest = + records: AutoCompleteRecords + getRegExp: getRegExp + getFindParams: getFindParams diff --git a/packages/meteor-autocomplete/autocomplete-server.coffee b/packages/meteor-autocomplete/autocomplete-server.coffee new file mode 100755 index 0000000000000000000000000000000000000000..192d5479bb20a72935a8730bb583391fee7100a6 --- /dev/null +++ b/packages/meteor-autocomplete/autocomplete-server.coffee @@ -0,0 +1,27 @@ +class Autocomplete + @publishCursor: (cursor, sub) -> + # This also attaches an onStop callback to sub, so we don't need to worry about that. + # https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js + Mongo.Collection._publishCursor(cursor, sub, "autocompleteRecords") + +Meteor.publish 'autocomplete-recordset', (selector, options, collName) -> + collection = global[collName] + unless collection + throw new Error(collName + ' is not defined on the global namespace of the server.') + + # This is a semi-documented Meteor feature: + # https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js + unless collection._isInsecure() + Meteor._debug(collName + ' is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.') + return [] # We need this for the subscription to be marked ready + + # guard against client-side DOS: hard limit to 50 + options.limit = Math.min(50, Math.abs(options.limit)) if options.limit + + # Push this into our own collection on the client so they don't interfere with other publications of the named collection. + # This also stops the observer automatically when the subscription is stopped. + Autocomplete.publishCursor( collection.find(selector, options), this) + + # Mark the subscription ready after the initial addition of documents. + this.ready() + diff --git a/packages/meteor-autocomplete/autocomplete.css b/packages/meteor-autocomplete/autocomplete.css new file mode 100755 index 0000000000000000000000000000000000000000..c4af66c2ff879b18f509445c8859b1fadd3eedea --- /dev/null +++ b/packages/meteor-autocomplete/autocomplete.css @@ -0,0 +1,27 @@ +.-autocomplete-container { + position: absolute; + background: white; + border: 1px solid #DDD; + border-radius: 3px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + min-width: 180px; + z-index: 1000; +} + +.-autocomplete-list { + list-style: none; + margin: 0; + padding: 0; +} + +.-autocomplete-item { + display: block; + padding: 5px 10px; + border-bottom: 1px solid #DDD; +} + +.-autocomplete-item.selected { + color: white; + background: #4183C4; + text-decoration: none; +} diff --git a/packages/meteor-autocomplete/inputs.html b/packages/meteor-autocomplete/inputs.html new file mode 100755 index 0000000000000000000000000000000000000000..289caa58ff7e8e50c7288dab4ae926f5e78f2aca --- /dev/null +++ b/packages/meteor-autocomplete/inputs.html @@ -0,0 +1,39 @@ +<template name="inputAutocomplete"> + <input type="text" {{attributes}}> + {{> autocompleteContainer}} +</template> + +<template name="textareaAutocomplete"> + <textarea {{attributes}}>{{> UI.contentBlock}}</textarea> + {{> autocompleteContainer}} +</template> + +<template name="_autocompleteContainer"> + {{#if isShowing}} + <div class='-autocomplete-container'> + {{#if isLoaded}} + {{#unless empty}} + <ul class='-autocomplete-list'> + {{#each filteredList}} + <li class="-autocomplete-item"> + {{#with ../currentTemplate }} + {{#with ..}} {{! original 'data' context to itemTemplate}} + {{> ..}} {{! return value from itemTemplate }} + {{/with}} + {{/with}} + </li> + {{/each}} + </ul> + {{else}} + {{> noMatchTemplate }} + {{/unless}} + {{else}} + {{> loading}} + {{/if}} + </div> + {{/if}} +</template> + +<template name="_noMatch"> + (<i>no matches</i>) +</template> diff --git a/packages/meteor-autocomplete/package.js b/packages/meteor-autocomplete/package.js new file mode 100755 index 0000000000000000000000000000000000000000..8e459c8df749fbbb3c634cc2f9398d8d621ed53e --- /dev/null +++ b/packages/meteor-autocomplete/package.js @@ -0,0 +1,44 @@ +Package.describe({ + name: 'mizzao:autocomplete', + summary: 'Client/server autocompletion designed for Meteor\'s collections and reactivity', + version: '0.5.1', + git: 'https://github.com/mizzao/meteor-autocomplete.git' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.0'); + + api.use(['blaze', 'templating', 'jquery'], 'client'); + api.use(['coffeescript', 'underscore']); // both + api.use(['mongo', 'ddp']); + + api.use('dandv:caret-position@2.1.0-3', 'client'); + + // Our files + api.addFiles([ + 'autocomplete.css', + 'inputs.html', + 'autocomplete-client.coffee', + 'templates.coffee' + ], 'client'); + + api.addFiles([ + 'autocomplete-server.coffee' + ], 'server'); + + api.export('Autocomplete', 'server'); + api.export('AutocompleteTest', {testOnly: true}); +}); + +Package.onTest(function(api) { + api.use('mizzao:autocomplete'); + + api.use('coffeescript'); + api.use('mongo'); + api.use('tinytest'); + + api.addFiles('tests/rule_tests.coffee', 'client'); + api.addFiles('tests/regex_tests.coffee', 'client'); + api.addFiles('tests/param_tests.coffee', 'client'); + api.addFiles('tests/security_tests.coffee'); +}); diff --git a/packages/meteor-autocomplete/templates.coffee b/packages/meteor-autocomplete/templates.coffee new file mode 100755 index 0000000000000000000000000000000000000000..cd56d8ba739846aa43fde4f8deca69d1eaf5dd63 --- /dev/null +++ b/packages/meteor-autocomplete/templates.coffee @@ -0,0 +1,50 @@ +# Events on template instances, sent to the autocomplete class +acEvents = + "keydown": (e, t) -> t.ac.onKeyDown(e) + "keyup": (e, t) -> t.ac.onKeyUp(e) + "focus": (e, t) -> t.ac.onFocus(e) + "blur": (e, t) -> t.ac.onBlur(e) + +Template.inputAutocomplete.events(acEvents) +Template.textareaAutocomplete.events(acEvents) + +attributes = -> _.omit(@, 'settings') # Render all but the settings parameter + +autocompleteHelpers = { + attributes, + autocompleteContainer: new Template('AutocompleteContainer', -> + ac = new AutoComplete( Blaze.getData().settings ) + # Set the autocomplete object on the parent template instance + this.parentView.templateInstance().ac = ac + + # Set nodes on render in the autocomplete class + this.onViewReady -> + ac.element = this.parentView.firstNode() + ac.$element = $(ac.element) + + return Blaze.With(ac, -> Template._autocompleteContainer) + ) +} + +Template.inputAutocomplete.helpers(autocompleteHelpers) +Template.textareaAutocomplete.helpers(autocompleteHelpers) + +Template._autocompleteContainer.rendered = -> + @data.tmplInst = this + +Template._autocompleteContainer.destroyed = -> + # Meteor._debug "autocomplete destroyed" + @data.teardown() + +### + List rendering helpers +### + +Template._autocompleteContainer.events + # t.data is the AutoComplete instance; `this` is the data item + "click .-autocomplete-item": (e, t) -> t.data.onItemClick(this, e) + "mouseenter .-autocomplete-item": (e, t) -> t.data.onItemHover(this, e) + +Template._autocompleteContainer.helpers + empty: -> @filteredList().count() is 0 + noMatchTemplate: -> @matchedRule().noMatchTemplate || Template._noMatch diff --git a/packages/rocketchat-authorization/client/lib/models/Subscriptions.js b/packages/rocketchat-authorization/client/lib/models/Subscriptions.js index ac5aab09a6dbcd11dc8d11a9583e37d6c42104d2..3d948a2722337ceea9904d93544548b2555bc787 100644 --- a/packages/rocketchat-authorization/client/lib/models/Subscriptions.js +++ b/packages/rocketchat-authorization/client/lib/models/Subscriptions.js @@ -3,6 +3,10 @@ if (_.isUndefined(RocketChat.models.Subscriptions)) { } RocketChat.models.Subscriptions.isUserInRole = function(userId, roleName, roomId) { + if (roomId == null) { + return false; + } + var query = { rid: roomId, roles: roleName diff --git a/packages/rocketchat-authorization/server/functions/addUserRoles.coffee b/packages/rocketchat-authorization/server/functions/addUserRoles.coffee index c715153ce326174249ddba7290f2051551558997..647c25d68269ad8695e18755c716f0dbd883d241 100644 --- a/packages/rocketchat-authorization/server/functions/addUserRoles.coffee +++ b/packages/rocketchat-authorization/server/functions/addUserRoles.coffee @@ -4,7 +4,7 @@ RocketChat.authz.addUserRoles = (userId, roleNames, scope) -> user = RocketChat.models.Users.findOneById(userId) if not user - throw new Meteor.Error 'invalid-user' + throw new Meteor.Error 'error-invalid-user', 'Invalid user', { function: 'RocketChat.authz.addUserRoles' } roleNames = [].concat roleNames diff --git a/packages/rocketchat-authorization/server/models/Base.js b/packages/rocketchat-authorization/server/models/Base.js index a2463bc07055b84bafa5fd7b28921667081f14c0..cc9d4dd67ce0225233d479ff52eca2df1581e153 100644 --- a/packages/rocketchat-authorization/server/models/Base.js +++ b/packages/rocketchat-authorization/server/models/Base.js @@ -1,5 +1,5 @@ RocketChat.models._Base.prototype.roleBaseQuery = function(/*userId, scope*/) { - return {}; + return; }; RocketChat.models._Base.prototype.findRolesByUserId = function(userId/*, options*/) { @@ -9,6 +9,11 @@ RocketChat.models._Base.prototype.findRolesByUserId = function(userId/*, options RocketChat.models._Base.prototype.isUserInRole = function(userId, roleName, scope) { var query = this.roleBaseQuery(userId, scope); + + if (query == null) { + return false; + } + query.roles = roleName; return !_.isUndefined(this.findOne(query)); }; diff --git a/packages/rocketchat-authorization/server/models/Subscriptions.js b/packages/rocketchat-authorization/server/models/Subscriptions.js index e55fdd75373eb16ec2a6597855813c64ec751ffa..06a0f62e2221cf80c8e4ea57e694ac843713f72a 100644 --- a/packages/rocketchat-authorization/server/models/Subscriptions.js +++ b/packages/rocketchat-authorization/server/models/Subscriptions.js @@ -1,4 +1,8 @@ RocketChat.models.Subscriptions.roleBaseQuery = function(userId, scope) { + if (scope == null) { + return; + } + var query = { 'u._id': userId }; if (!_.isUndefined(scope)) { query.rid = scope; diff --git a/packages/rocketchat-authorization/server/startup.coffee b/packages/rocketchat-authorization/server/startup.coffee index 907d0c3ea616096e5dd284ef7a5901248c173c14..d4feba4af6632d5f397c847f70c5743c4f8c1fa1 100644 --- a/packages/rocketchat-authorization/server/startup.coffee +++ b/packages/rocketchat-authorization/server/startup.coffee @@ -46,6 +46,7 @@ Meteor.startup -> { _id: 'view-full-other-user-info', roles : ['admin'] } { _id: 'view-history', roles : ['admin', 'user'] } { _id: 'view-joined-room', roles : ['guest', 'bot'] } + { _id: 'view-join-code', roles : ['admin'] } { _id: 'view-logs', roles : ['admin'] } { _id: 'view-other-user-channels', roles : ['admin'] } { _id: 'view-p-room', roles : ['admin', 'user'] } @@ -53,6 +54,7 @@ Meteor.startup -> { _id: 'view-room-administration', roles : ['admin'] } { _id: 'view-statistics', roles : ['admin'] } { _id: 'view-user-administration', roles : ['admin'] } + { _id: 'preview-c-room', roles : ['admin', 'user'] } ] for permission in permissions diff --git a/packages/rocketchat-autolinker/lib/Autolinker.min.js b/packages/rocketchat-autolinker/lib/Autolinker.min.js index 45023691fb1d32d2242c21d9eb73000965be9f35..c518e9185a251d1664e8231e76c4e620bc80105f 100644 --- a/packages/rocketchat-autolinker/lib/Autolinker.min.js +++ b/packages/rocketchat-autolinker/lib/Autolinker.min.js @@ -1,10 +1,10 @@ /*! * Autolinker.js - * 0.26.0 + * 0.28.0 * * Copyright(c) 2016 Gregory Jacobs <greg@greg-jacobs.com> * MIT License * * https://github.com/gregjacobs/Autolinker.js */ -!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Autolinker=e()}(this,function(){var t=function(e){e=e||{},this.version=t.version,this.urls=this.normalizeUrlsCfg(e.urls),this.email="boolean"==typeof e.email?e.email:!0,this.twitter="boolean"==typeof e.twitter?e.twitter:!0,this.phone="boolean"==typeof e.phone?e.phone:!0,this.hashtag=e.hashtag||!1,this.newWindow="boolean"==typeof e.newWindow?e.newWindow:!0,this.stripPrefix="boolean"==typeof e.stripPrefix?e.stripPrefix:!0;var r=this.hashtag;if(r!==!1&&"twitter"!==r&&"facebook"!==r&&"instagram"!==r)throw new Error("invalid `hashtag` cfg - see docs");this.truncate=this.normalizeTruncateCfg(e.truncate),this.className=e.className||"",this.replaceFn=e.replaceFn||null,this.htmlParser=null,this.matchers=null,this.tagBuilder=null};return t.link=function(e,r){var a=new t(r);return a.link(e)},t.version="0.26.0",t.prototype={constructor:t,normalizeUrlsCfg:function(t){return null==t&&(t=!0),"boolean"==typeof t?{schemeMatches:t,wwwMatches:t,tldMatches:t}:{schemeMatches:"boolean"==typeof t.schemeMatches?t.schemeMatches:!0,wwwMatches:"boolean"==typeof t.wwwMatches?t.wwwMatches:!0,tldMatches:"boolean"==typeof t.tldMatches?t.tldMatches:!0}},normalizeTruncateCfg:function(e){return"number"==typeof e?{length:e,location:"end"}:t.Util.defaults(e||{},{length:Number.POSITIVE_INFINITY,location:"end"})},parse:function(t){for(var e=this.getHtmlParser(),r=e.parse(t),a=0,n=[],i=0,s=r.length;s>i;i++){var o=r[i],c=o.getType();if("element"===c&&"a"===o.getTagName())o.isClosing()?a=Math.max(a-1,0):a++;else if("text"===c&&0===a){var h=this.parseText(o.getText(),o.getOffset());n.push.apply(n,h)}}return n=this.compactMatches(n),n=this.removeUnwantedMatches(n)},compactMatches:function(t){t.sort(function(t,e){return t.getOffset()-e.getOffset()});for(var e=0;e<t.length-1;e++)for(var r=t[e],a=r.getOffset()+r.getMatchedText().length;e+1<t.length&&t[e+1].getOffset()<=a;)t.splice(e+1,1);return t},removeUnwantedMatches:function(e){var r=t.Util.remove;return this.hashtag||r(e,function(t){return"hashtag"===t.getType()}),this.email||r(e,function(t){return"email"===t.getType()}),this.phone||r(e,function(t){return"phone"===t.getType()}),this.twitter||r(e,function(t){return"twitter"===t.getType()}),this.urls.schemeMatches||r(e,function(t){return"url"===t.getType()&&"scheme"===t.getUrlMatchType()}),this.urls.wwwMatches||r(e,function(t){return"url"===t.getType()&&"www"===t.getUrlMatchType()}),this.urls.tldMatches||r(e,function(t){return"url"===t.getType()&&"tld"===t.getUrlMatchType()}),e},parseText:function(t,e){e=e||0;for(var r=this.getMatchers(),a=[],n=0,i=r.length;i>n;n++){for(var s=r[n].parseMatches(t),o=0,c=s.length;c>o;o++)s[o].setOffset(e+s[o].getOffset());a.push.apply(a,s)}return a},link:function(t){if(!t)return"";for(var e=this.parse(t),r=[],a=0,n=0,i=e.length;i>n;n++){var s=e[n];r.push(t.substring(a,s.getOffset())),r.push(this.createMatchReturnVal(s)),a=s.getOffset()+s.getMatchedText().length}return r.push(t.substring(a)),r.join("")},createMatchReturnVal:function(e){var r;if(this.replaceFn&&(r=this.replaceFn.call(this,this,e)),"string"==typeof r)return r;if(r===!1)return e.getMatchedText();if(r instanceof t.HtmlTag)return r.toAnchorString();var a=e.buildTag();return a.toAnchorString()},getHtmlParser:function(){var e=this.htmlParser;return e||(e=this.htmlParser=new t.htmlParser.HtmlParser),e},getMatchers:function(){if(this.matchers)return this.matchers;var e=t.matcher,r=this.getTagBuilder(),a=[new e.Hashtag({tagBuilder:r,serviceName:this.hashtag}),new e.Email({tagBuilder:r}),new e.Phone({tagBuilder:r}),new e.Twitter({tagBuilder:r}),new e.Url({tagBuilder:r,stripPrefix:this.stripPrefix})];return this.matchers=a},getTagBuilder:function(){var e=this.tagBuilder;return e||(e=this.tagBuilder=new t.AnchorTagBuilder({newWindow:this.newWindow,truncate:this.truncate,className:this.className})),e}},t.match={},t.matcher={},t.htmlParser={},t.truncate={},t.Util={abstractMethod:function(){throw"abstract"},trimRegex:/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,assign:function(t,e){for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);return t},defaults:function(t,e){for(var r in e)e.hasOwnProperty(r)&&void 0===t[r]&&(t[r]=e[r]);return t},extend:function(e,r){var a=e.prototype,n=function(){};n.prototype=a;var i;i=r.hasOwnProperty("constructor")?r.constructor:function(){a.constructor.apply(this,arguments)};var s=i.prototype=new n;return s.constructor=i,s.superclass=a,delete r.constructor,t.Util.assign(s,r),i},ellipsis:function(t,e,r){return t.length>e&&(r=null==r?"..":r,t=t.substring(0,e-r.length)+r),t},indexOf:function(t,e){if(Array.prototype.indexOf)return t.indexOf(e);for(var r=0,a=t.length;a>r;r++)if(t[r]===e)return r;return-1},remove:function(t,e){for(var r=t.length-1;r>=0;r--)e(t[r])===!0&&t.splice(r,1)},splitAndCapture:function(t,e){for(var r,a=[],n=0;r=e.exec(t);)a.push(t.substring(n,r.index)),a.push(r[0]),n=r.index+r[0].length;return a.push(t.substring(n)),a},trim:function(t){return t.replace(this.trimRegex,"")}},t.HtmlTag=t.Util.extend(Object,{whitespaceRegex:/\s+/,constructor:function(e){t.Util.assign(this,e),this.innerHtml=this.innerHtml||this.innerHTML},setTagName:function(t){return this.tagName=t,this},getTagName:function(){return this.tagName||""},setAttr:function(t,e){var r=this.getAttrs();return r[t]=e,this},getAttr:function(t){return this.getAttrs()[t]},setAttrs:function(e){var r=this.getAttrs();return t.Util.assign(r,e),this},getAttrs:function(){return this.attrs||(this.attrs={})},setClass:function(t){return this.setAttr("class",t)},addClass:function(e){for(var r,a=this.getClass(),n=this.whitespaceRegex,i=t.Util.indexOf,s=a?a.split(n):[],o=e.split(n);r=o.shift();)-1===i(s,r)&&s.push(r);return this.getAttrs()["class"]=s.join(" "),this},removeClass:function(e){for(var r,a=this.getClass(),n=this.whitespaceRegex,i=t.Util.indexOf,s=a?a.split(n):[],o=e.split(n);s.length&&(r=o.shift());){var c=i(s,r);-1!==c&&s.splice(c,1)}return this.getAttrs()["class"]=s.join(" "),this},getClass:function(){return this.getAttrs()["class"]||""},hasClass:function(t){return-1!==(" "+this.getClass()+" ").indexOf(" "+t+" ")},setInnerHtml:function(t){return this.innerHtml=t,this},getInnerHtml:function(){return this.innerHtml||""},toAnchorString:function(){var t=this.getTagName(),e=this.buildAttrsStr();return e=e?" "+e:"",["<",t,e,">",this.getInnerHtml(),"</",t,">"].join("")},buildAttrsStr:function(){if(!this.attrs)return"";var t=this.getAttrs(),e=[];for(var r in t)t.hasOwnProperty(r)&&e.push(r+'="'+t[r]+'"');return e.join(" ")}}),t.RegexLib=function(){var t="A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-ˈ-Ë‘Ë -ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ÒÒŠ-Ô¯Ô±-Õ–Õ™Õ¡-Ö‡×-תװ-×²Ø -يٮٯٱ-Û“Û•Û¥Û¦Û®Û¯Ûº-Û¼Û¿ÜÜ’-ܯÝ-ޥޱߊ-ßªß´ßµßºà €-à •à šà ¤à ¨à¡€-ࡘࢠ-ࢴऄ-हऽà¥à¥˜-ॡॱ-ঀঅ-ঌà¦à¦à¦“-নপ-রলশ-হঽৎড়à§à§Ÿ-ৡৰৱਅ-ਊà¨à¨à¨“-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-àªàª-ઑઓ-નપ-રલળવ-હઽà«à« ૡૹଅ-ଌà¬à¬à¬“-ନପ-ରଲଳଵ-ହଽàœààŸ-à¡à±à®ƒà®…-ஊஎ-à®à®’-கஙசஜஞடணதந-பம-ஹà¯à°…-ఌఎ-à°à°’-నప-హఽౘ-ౚౠౡಅ-ಌಎ-à²à²’-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-à´à´’-ഺഽൎൟ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-à·†à¸-ะาำเ-ๆàºàº‚ຄງຈຊàºàº”-ທນ-ຟມ-ຣລວສຫàº-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿá-á•áš-áá¡á¥á¦á®-á°áµ-á‚á‚Žá‚ -ჅჇáƒáƒ-ჺჼ-ቈቊ-á‰á‰-ቖቘቚ-á‰á‰ -ኈኊ-áŠáŠ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-áŒáŒ’-ጕጘ-ášáŽ€-áŽáŽ -áµá¸-á½á-ᙬᙯ-ᙿáš-áššáš -ᛪᛱ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱá€-á‘á -á¬á®-á°áž€-ឳៗៜá -ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤞá¥-á¥á¥°-ᥴᦀ-ᦫᦰ-ᧉᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳá…-á‹á®ƒ-ᮠᮮᮯᮺ-ᯥᰀ-á°£á±-á±á±š-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-á¼á¼ -ὅὈ-á½á½-ὗὙὛá½á½Ÿ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-á¿Œá¿-á¿“á¿–-Ίῠ-Ῥῲ-ῴῶ-ῼâ±â¿â‚-ₜℂℇℊ-â„“â„•â„™-â„ℤΩℨK-â„ℯ-ℹℼ-â„¿â……-ⅉⅎↃↄⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧâ´â´°-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-â·Žâ·-â·–â·˜-ⷞⸯ々〆〱-〵〻〼ã-ã‚–ã‚-ã‚Ÿã‚¡-ヺー-ヿㄅ-ã„ㄱ-ㆎㆠ-ㆺㇰ-ㇿã€-䶵一-鿕ꀀ-ê’Œê“-ꓽꔀ-ꘌê˜-ꘟꘪꘫꙀ-ꙮꙿ-êšêš -ꛥꜗ-ꜟꜢ-ꞈꞋ-êžêž°-ꞷꟷ-ê ê ƒ-ê …ê ‡-ê Šê Œ-ê ¢ê¡€-ꡳꢂ-ꢳꣲ-ꣷꣻꣽꤊ-ꤥꤰ-ê¥†ê¥ -ꥼꦄ-ꦲê§ê§ -ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ê©‚ê©„-ê©‹ê© -ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ê«ê« -ꫪꫲ-ê«´ê¬-ꬆꬉ-ꬎꬑ-ê¬–ê¬ -ꬦꬨ-ꬮꬰ-êšêœ-ê¥ê°-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-ï©ï©°-龎ff-stﬓ-ﬗï¬ï¬Ÿ-ﬨשׁ-זּטּ-לּמּï€ïïƒï„ï†-ﮱﯓ-ï´½ïµ-ï¶ï¶’-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Zï½-zヲ-하-ᅦᅧ-ï¿ï¿’-ï¿—ï¿š-ï¿œ",e="0-9Ù -Ù©Û°-۹߀-߉०-९০-৯੦-੯૦-૯à¦-à¯à¯¦-௯౦-౯೦-೯൦-൯෦-à·¯à¹-๙à»-໙༠-༩á€-á‰á‚-႙០-៩á -á ™á¥†-á¥á§-᧙᪀-᪉áª-᪙á-á™á®°-᮹᱀-᱉á±-á±™ê˜ -꘩ê£-꣙꤀-꤉ê§-꧙꧰-꧹ê©-꩙꯰-꯹ï¼-ï¼™",r=t+e,a=new RegExp("["+r+".\\-]*["+r+"\\-]"),n=/(?:travelersinsurance|sandvikcoromant|kerryproperties|cancerresearch|weatherchannel|kerrylogistics|spreadbetting|international|wolterskluwer|lifeinsurance|construction|pamperedchef|scholarships|versicherung|bridgestone|creditunion|kerryhotels|investments|productions|blackfriday|enterprises|lamborghini|photography|motorcycles|williamhill|playstation|contractors|barclaycard|accountants|redumbrella|engineering|management|telefonica|protection|consulting|tatamotors|creditcard|vlaanderen|schaeffler|associates|properties|foundation|republican|bnpparibas|boehringer|eurovision|extraspace|industries|immobilien|university|technology|volkswagen|healthcare|restaurant|cuisinella|vistaprint|apartments|accountant|travelers|homedepot|institute|vacations|furniture|fresenius|insurance|christmas|bloomberg|solutions|barcelona|firestone|financial|kuokgroup|fairwinds|community|passagens|goldpoint|equipment|lifestyle|yodobashi|aquarelle|marketing|analytics|education|amsterdam|statefarm|melbourne|allfinanz|directory|microsoft|stockholm|montblanc|accenture|lancaster|landrover|everbank|istanbul|graphics|grainger|ipiranga|softbank|attorney|pharmacy|saarland|catering|airforce|yokohama|mortgage|frontier|mutuelle|stcgroup|memorial|pictures|football|symantec|cipriani|ventures|telecity|cityeats|verisign|flsmidth|boutique|cleaning|firmdale|clinique|clothing|redstone|infiniti|deloitte|feedback|services|broadway|plumbing|commbank|training|barclays|exchange|computer|brussels|software|delivery|barefoot|builders|business|bargains|engineer|holdings|download|security|helsinki|lighting|movistar|discount|hdfcbank|supplies|marriott|property|diamonds|capetown|partners|democrat|jpmorgan|bradesco|budapest|rexroth|zuerich|shriram|academy|science|support|youtube|singles|surgery|alibaba|statoil|dentist|schwarz|android|cruises|cricket|digital|markets|starhub|systems|courses|coupons|netbank|country|domains|corsica|network|neustar|realtor|lincoln|limited|schmidt|yamaxun|cooking|contact|auction|spiegel|liaison|leclerc|latrobe|lasalle|abogado|compare|lanxess|exposed|express|company|cologne|college|avianca|lacaixa|fashion|recipes|ferrero|komatsu|storage|wanggou|clubmed|sandvik|fishing|fitness|bauhaus|kitchen|flights|florist|flowers|watches|weather|temasek|samsung|bentley|forsale|channel|theater|frogans|theatre|okinawa|website|tickets|jewelry|gallery|tiffany|iselect|shiksha|brother|organic|wedding|genting|toshiba|origins|philips|hyundai|hotmail|hoteles|hosting|rentals|windows|cartier|bugatti|holiday|careers|whoswho|hitachi|panerai|caravan|reviews|guitars|capital|trading|hamburg|hangout|finance|stream|family|abbott|health|review|travel|report|hermes|hiphop|gratis|career|toyota|hockey|dating|repair|google|social|soccer|reisen|global|otsuka|giving|unicom|casino|photos|center|broker|rocher|orange|bostik|garden|insure|ryukyu|bharti|safety|physio|sakura|oracle|online|jaguar|gallup|piaget|tienda|futbol|pictet|joburg|webcam|berlin|office|juegos|kaufen|chanel|chrome|xihuan|church|tennis|circle|kinder|flickr|bayern|claims|clinic|viajes|nowruz|xperia|norton|yachts|studio|coffee|camera|sanofi|nissan|author|expert|events|comsec|lawyer|tattoo|viking|estate|villas|condos|realty|yandex|energy|emerck|virgin|vision|durban|living|school|coupon|london|taobao|natura|taipei|nagoya|luxury|walter|aramco|sydney|madrid|credit|maison|makeup|schule|market|anquan|direct|design|swatch|suzuki|alsace|vuelos|dental|alipay|voyage|shouji|voting|airtel|mutual|degree|supply|agency|museum|mobily|dealer|monash|select|mormon|active|moscow|racing|datsun|quebec|nissay|rodeo|email|gifts|works|photo|chloe|edeka|cheap|earth|vista|tushu|koeln|glass|shoes|globo|tunes|gmail|nokia|space|kyoto|black|ricoh|seven|lamer|sener|epson|cisco|praxi|trust|citic|crown|shell|lease|green|legal|lexus|ninja|tatar|gripe|nikon|group|video|wales|autos|gucci|party|nexus|guide|linde|adult|parts|amica|lixil|boats|azure|loans|locus|cymru|lotte|lotto|stada|click|poker|quest|dabur|lupin|nadex|paris|faith|dance|canon|place|gives|trade|skype|rocks|mango|cloud|boots|smile|final|swiss|homes|honda|media|horse|cards|deals|watch|bosch|house|pizza|miami|osaka|tours|total|xerox|coach|sucks|style|delta|toray|iinet|tools|money|codes|beats|tokyo|salon|archi|movie|baidu|study|actor|yahoo|store|apple|world|forex|today|bible|tmall|tirol|irish|tires|forum|reise|vegas|vodka|sharp|omega|weber|jetzt|audio|promo|build|bingo|chase|gallo|drive|dubai|rehab|press|solar|sale|beer|bbva|bank|band|auto|sapo|sarl|saxo|audi|asia|arte|arpa|army|yoga|ally|zara|scor|scot|sexy|seat|zero|seek|aero|adac|zone|aarp|maif|meet|meme|menu|surf|mini|mobi|mtpc|porn|desi|star|ltda|name|talk|navy|love|loan|live|link|news|limo|like|spot|life|nico|lidl|lgbt|land|taxi|team|tech|kred|kpmg|sony|song|kiwi|kddi|jprs|jobs|sohu|java|itau|tips|info|immo|icbc|hsbc|town|host|page|toys|here|help|pars|haus|guru|guge|tube|goog|golf|gold|sncf|gmbh|gift|ggee|gent|gbiz|game|vana|pics|fund|ford|ping|pink|fish|film|fast|farm|play|fans|fail|plus|skin|pohl|fage|moda|post|erni|dvag|prod|doha|prof|docs|viva|diet|luxe|site|dell|sina|dclk|show|qpon|date|vote|cyou|voto|read|coop|cool|wang|club|city|chat|cern|cash|reit|rent|casa|cars|care|camp|rest|call|cafe|weir|wien|rich|wiki|buzz|wine|book|bond|room|work|rsvp|shia|ruhr|blue|bing|shaw|bike|safe|xbox|best|pwc|mtn|lds|aig|boo|fyi|nra|nrw|ntt|car|gal|obi|zip|aeg|vin|how|one|ong|onl|dad|ooo|bet|esq|org|htc|bar|uol|ibm|ovh|gdn|ice|icu|uno|gea|ifm|bot|top|wtf|lol|day|pet|eus|wtc|ubs|tvs|aco|ing|ltd|ink|tab|abb|afl|cat|int|pid|pin|bid|cba|gle|com|cbn|ads|man|wed|ceb|gmo|sky|ist|gmx|tui|mba|fan|ski|iwc|app|pro|med|ceo|jcb|jcp|goo|dev|men|aaa|meo|pub|jlc|bom|jll|gop|jmp|mil|got|gov|win|jot|mma|joy|trv|red|cfa|cfd|bio|moe|moi|mom|ren|biz|aws|xin|bbc|dnp|buy|kfh|mov|thd|xyz|fit|kia|rio|rip|kim|dog|vet|nyc|bcg|mtr|bcn|bms|bmw|run|bzh|rwe|tel|stc|axa|kpn|fly|krd|cab|bnl|foo|crs|eat|tci|sap|srl|nec|sas|net|cal|sbs|sfr|sca|scb|csc|edu|new|xxx|hiv|fox|wme|ngo|nhk|vip|sex|frl|lat|yun|law|you|tax|soy|sew|om|ac|hu|se|sc|sg|sh|sb|sa|rw|ru|rs|ro|re|qa|py|si|pw|pt|ps|sj|sk|pr|pn|pm|pl|sl|sm|pk|sn|ph|so|pg|pf|pe|pa|zw|nz|nu|nr|np|no|nl|ni|ng|nf|sr|ne|st|nc|na|mz|my|mx|mw|mv|mu|mt|ms|mr|mq|mp|mo|su|mn|mm|ml|mk|mh|mg|me|sv|md|mc|sx|sy|ma|ly|lv|sz|lu|lt|ls|lr|lk|li|lc|lb|la|tc|kz|td|ky|kw|kr|kp|kn|km|ki|kh|tf|tg|th|kg|ke|jp|jo|jm|je|it|is|ir|tj|tk|tl|tm|iq|tn|to|io|in|im|il|ie|ad|sd|ht|hr|hn|hm|tr|hk|gy|gw|gu|gt|gs|gr|gq|tt|gp|gn|gm|gl|tv|gi|tw|tz|ua|gh|ug|uk|gg|gf|ge|gd|us|uy|uz|va|gb|ga|vc|ve|fr|fo|fm|fk|fj|vg|vi|fi|eu|et|es|er|eg|ee|ec|dz|do|dm|dk|vn|dj|de|cz|cy|cx|cw|vu|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|wf|bz|by|bw|bv|bt|bs|br|bo|bn|bm|bj|bi|ws|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ye|ar|aq|ao|am|al|yt|ai|za|ag|af|ae|zm|id)\b/;return{alphaNumericCharsStr:r,domainNameRegex:a,tldRegex:n}}(),t.AnchorTagBuilder=t.Util.extend(Object,{constructor:function(e){t.Util.assign(this,e)},build:function(e){return new t.HtmlTag({tagName:"a",attrs:this.createAttrs(e.getType(),e.getAnchorHref()),innerHtml:this.processAnchorText(e.getAnchorText())})},createAttrs:function(t,e){var r={href:e},a=this.createCssClass(t);return a&&(r["class"]=a),this.newWindow&&(r.target="_blank",r.rel="noopener noreferrer"),r},createCssClass:function(t){var e=this.className;return e?e+" "+e+"-"+t:""},processAnchorText:function(t){return t=this.doTruncate(t)},doTruncate:function(e){var r=this.truncate;if(!r||!r.length)return e;var a=r.length,n=r.location;return"smart"===n?t.truncate.TruncateSmart(e,a,".."):"middle"===n?t.truncate.TruncateMiddle(e,a,".."):t.truncate.TruncateEnd(e,a,"..")}}),t.htmlParser.HtmlParser=t.Util.extend(Object,{htmlRegex:function(){var t=/!--([\s\S]+?)--/,e=/[0-9a-zA-Z][0-9a-zA-Z:]*/,r=/[^\s\0"'>\/=\x01-\x1F\x7F]+/,a=/(?:"[^"]*?"|'[^']*?'|[^'"=<>`\s]+)/,n=r.source+"(?:\\s*=\\s*"+a.source+")?";return new RegExp(["(?:","<(!DOCTYPE)","(?:","\\s+","(?:",n,"|",a.source+")",")*",">",")","|","(?:","<(/)?","(?:",t.source,"|","(?:","("+e.source+")","(?:","\\s*",n,")*","\\s*/?",")",")",">",")"].join(""),"gi")}(),htmlCharacterEntitiesRegex:/( | |<|<|>|>|"|"|')/gi,parse:function(t){for(var e,r,a=this.htmlRegex,n=0,i=[];null!==(e=a.exec(t));){var s=e[0],o=e[3],c=e[1]||e[4],h=!!e[2],l=e.index,u=t.substring(n,l);u&&(r=this.parseTextAndEntityNodes(n,u),i.push.apply(i,r)),o?i.push(this.createCommentNode(l,s,o)):i.push(this.createElementNode(l,s,c,h)),n=l+s.length}if(n<t.length){var g=t.substring(n);g&&(r=this.parseTextAndEntityNodes(n,g),i.push.apply(i,r))}return i},parseTextAndEntityNodes:function(e,r){for(var a=[],n=t.Util.splitAndCapture(r,this.htmlCharacterEntitiesRegex),i=0,s=n.length;s>i;i+=2){var o=n[i],c=n[i+1];o&&(a.push(this.createTextNode(e,o)),e+=o.length),c&&(a.push(this.createEntityNode(e,c)),e+=c.length)}return a},createCommentNode:function(e,r,a){return new t.htmlParser.CommentNode({offset:e,text:r,comment:t.Util.trim(a)})},createElementNode:function(e,r,a,n){return new t.htmlParser.ElementNode({offset:e,text:r,tagName:a.toLowerCase(),closing:n})},createEntityNode:function(e,r){return new t.htmlParser.EntityNode({offset:e,text:r})},createTextNode:function(e,r){return new t.htmlParser.TextNode({offset:e,text:r})}}),t.htmlParser.HtmlNode=t.Util.extend(Object,{offset:void 0,text:void 0,constructor:function(e){t.Util.assign(this,e)},getType:t.Util.abstractMethod,getOffset:function(){return this.offset},getText:function(){return this.text}}),t.htmlParser.CommentNode=t.Util.extend(t.htmlParser.HtmlNode,{comment:"",getType:function(){return"comment"},getComment:function(){return this.comment}}),t.htmlParser.ElementNode=t.Util.extend(t.htmlParser.HtmlNode,{tagName:"",closing:!1,getType:function(){return"element"},getTagName:function(){return this.tagName},isClosing:function(){return this.closing}}),t.htmlParser.EntityNode=t.Util.extend(t.htmlParser.HtmlNode,{getType:function(){return"entity"}}),t.htmlParser.TextNode=t.Util.extend(t.htmlParser.HtmlNode,{getType:function(){return"text"}}),t.match.Match=t.Util.extend(Object,{constructor:function(t){this.tagBuilder=t.tagBuilder,this.matchedText=t.matchedText,this.offset=t.offset},getType:t.Util.abstractMethod,getMatchedText:function(){return this.matchedText},setOffset:function(t){this.offset=t},getOffset:function(){return this.offset},getAnchorHref:t.Util.abstractMethod,getAnchorText:t.Util.abstractMethod,buildTag:function(){return this.tagBuilder.build(this)}}),t.match.Email=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.email=e.email},getType:function(){return"email"},getEmail:function(){return this.email},getAnchorHref:function(){return"mailto:"+this.email},getAnchorText:function(){return this.email}}),t.match.Hashtag=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.serviceName=e.serviceName,this.hashtag=e.hashtag},getType:function(){return"hashtag"},getServiceName:function(){return this.serviceName},getHashtag:function(){return this.hashtag},getAnchorHref:function(){var t=this.serviceName,e=this.hashtag;switch(t){case"twitter":return"https://twitter.com/hashtag/"+e;case"facebook":return"https://www.facebook.com/hashtag/"+e;case"instagram":return"https://instagram.com/explore/tags/"+e;default:throw new Error("Unknown service name to point hashtag to: ",t)}},getAnchorText:function(){return"#"+this.hashtag}}),t.match.Phone=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.number=e.number,this.plusSign=e.plusSign},getType:function(){return"phone"},getNumber:function(){return this.number},getAnchorHref:function(){return"tel:"+(this.plusSign?"+":"")+this.number},getAnchorText:function(){return this.matchedText}}),t.match.Twitter=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.twitterHandle=e.twitterHandle},getType:function(){return"twitter"},getTwitterHandle:function(){return this.twitterHandle},getAnchorHref:function(){return"https://twitter.com/"+this.twitterHandle},getAnchorText:function(){return"@"+this.twitterHandle}}),t.match.Url=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.urlMatchType=e.urlMatchType,this.url=e.url,this.protocolUrlMatch=e.protocolUrlMatch,this.protocolRelativeMatch=e.protocolRelativeMatch,this.stripPrefix=e.stripPrefix},urlPrefixRegex:/^(https?:\/\/)?(www\.)?/i,protocolRelativeRegex:/^\/\//,protocolPrepended:!1,getType:function(){return"url"},getUrlMatchType:function(){return this.urlMatchType},getUrl:function(){var t=this.url;return this.protocolRelativeMatch||this.protocolUrlMatch||this.protocolPrepended||(t=this.url="http://"+t,this.protocolPrepended=!0),t},getAnchorHref:function(){var t=this.getUrl();return t.replace(/&/g,"&")},getAnchorText:function(){var t=this.getMatchedText();return this.protocolRelativeMatch&&(t=this.stripProtocolRelativePrefix(t)),this.stripPrefix&&(t=this.stripUrlPrefix(t)),t=this.removeTrailingSlash(t)},stripUrlPrefix:function(t){return t.replace(this.urlPrefixRegex,"")},stripProtocolRelativePrefix:function(t){return t.replace(this.protocolRelativeRegex,"")},removeTrailingSlash:function(t){return"/"===t.charAt(t.length-1)&&(t=t.slice(0,-1)),t}}),t.matcher.Matcher=t.Util.extend(Object,{constructor:function(t){this.tagBuilder=t.tagBuilder},parseMatches:t.Util.abstractMethod}),t.matcher.Email=t.Util.extend(t.matcher.Matcher,{matcherRegex:function(){var e=t.RegexLib.alphaNumericCharsStr,r=new RegExp("["+e+"\\-;:&=+$.,]+@"),a=t.RegexLib.domainNameRegex,n=t.RegexLib.tldRegex;return new RegExp([r.source,a.source,"\\.",n.source].join(""),"gi")}(),parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.tagBuilder,i=[];null!==(r=a.exec(e));){var s=r[0];i.push(new t.match.Email({tagBuilder:n,matchedText:s,offset:r.index,email:s}))}return i}}),t.matcher.Hashtag=t.Util.extend(t.matcher.Matcher,{matcherRegex:new RegExp("#[_"+t.RegexLib.alphaNumericCharsStr+"]{1,139}","g"),nonWordCharRegex:new RegExp("[^"+t.RegexLib.alphaNumericCharsStr+"]"),constructor:function(e){t.matcher.Matcher.prototype.constructor.call(this,e),this.serviceName=e.serviceName},parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.nonWordCharRegex,i=this.serviceName,s=this.tagBuilder,o=[];null!==(r=a.exec(e));){var c=r.index,h=e.charAt(c-1);if(0===c||n.test(h)){var l=r[0],u=r[0].slice(1);o.push(new t.match.Hashtag({tagBuilder:s,matchedText:l,offset:c,serviceName:i,hashtag:u}))}}return o}}),t.matcher.Phone=t.Util.extend(t.matcher.Matcher,{matcherRegex:/(?:(\+)?\d{1,3}[-\040.])?\(?\d{3}\)?[-\040.]?\d{3}[-\040.]\d{4}/g,parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.tagBuilder,i=[];null!==(r=a.exec(e));){var s=r[0],o=s.replace(/\D/g,""),c=!!r[1];i.push(new t.match.Phone({tagBuilder:n,matchedText:s,offset:r.index,number:o,plusSign:c}))}return i}}),t.matcher.Twitter=t.Util.extend(t.matcher.Matcher,{matcherRegex:new RegExp("@[_"+t.RegexLib.alphaNumericCharsStr+"]{1,20}","g"),nonWordCharRegex:new RegExp("[^"+t.RegexLib.alphaNumericCharsStr+"]"),parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.nonWordCharRegex,i=this.tagBuilder,s=[];null!==(r=a.exec(e));){var o=r.index,c=e.charAt(o-1);if(0===o||n.test(c)){var h=r[0],l=r[0].slice(1);s.push(new t.match.Twitter({tagBuilder:i,matchedText:h,offset:o,twitterHandle:l}))}}return s}}),t.matcher.Url=t.Util.extend(t.matcher.Matcher,{matcherRegex:function(){var e=/(?:[A-Za-z][-.+A-Za-z0-9]*:(?![A-Za-z][-.+A-Za-z0-9]*:\/\/)(?!\d+\/?)(?:\/\/)?)/,r=/(?:www\.)/,a=t.RegexLib.domainNameRegex,n=t.RegexLib.tldRegex,i=t.RegexLib.alphaNumericCharsStr,s=new RegExp("["+i+"\\-+&@#/%=~_()|'$*\\[\\]?!:,.;]*["+i+"\\-+&@#/%=~_()|'$*\\[\\]]");return new RegExp(["(?:","(",e.source,a.source,")","|","(","(//)?",r.source,a.source,")","|","(","(//)?",a.source+"\\.",n.source,")",")","(?:"+s.source+")?"].join(""),"gi")}(),wordCharRegExp:/\w/,openParensRe:/\(/g,closeParensRe:/\)/g,constructor:function(e){t.matcher.Matcher.prototype.constructor.call(this,e),this.stripPrefix=e.stripPrefix},parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.stripPrefix,i=this.tagBuilder,s=[];null!==(r=a.exec(e));){var o=r[0],c=r[1],h=r[2],l=r[3],u=r[5],g=r.index,m=l||u,f=e.charAt(g-1);if(t.matcher.UrlMatchValidator.isValid(o,c)&&!(g>0&&"@"===f||g>0&&m&&this.wordCharRegExp.test(f))){if(this.matchHasUnbalancedClosingParen(o))o=o.substr(0,o.length-1);else{var p=this.matchHasInvalidCharAfterTld(o,c);p>-1&&(o=o.substr(0,p))}var d=c?"scheme":h?"www":"tld",b=!!c;s.push(new t.match.Url({tagBuilder:i,matchedText:o,offset:g,urlMatchType:d,url:o,protocolUrlMatch:b,protocolRelativeMatch:!!m,stripPrefix:n}))}}return s},matchHasUnbalancedClosingParen:function(t){var e=t.charAt(t.length-1);if(")"===e){var r=t.match(this.openParensRe),a=t.match(this.closeParensRe),n=r&&r.length||0,i=a&&a.length||0;if(i>n)return!0}return!1},matchHasInvalidCharAfterTld:function(t,e){if(!t)return-1;var r=0;e&&(r=t.indexOf(":"),t=t.slice(r));var a=/^((.?\/\/)?[A-Za-z0-9\u00C0-\u017F\.\-]*[A-Za-z0-9\u00C0-\u017F\-]\.[A-Za-z]+)/,n=a.exec(t);return null===n?-1:(r+=n[1].length,t=t.slice(n[1].length),/^[^.A-Za-z:\/?#]/.test(t)?r:-1)}}),t.matcher.UrlMatchValidator={hasFullProtocolRegex:/^[A-Za-z][-.+A-Za-z0-9]*:\/\//,uriSchemeRegex:/^[A-Za-z][-.+A-Za-z0-9]*:/,hasWordCharAfterProtocolRegex:/:[^\s]*?[A-Za-z\u00C0-\u017F]/,isValid:function(t,e){return!(e&&!this.isValidUriScheme(e)||this.urlMatchDoesNotHaveProtocolOrDot(t,e)||this.urlMatchDoesNotHaveAtLeastOneWordChar(t,e))},isValidUriScheme:function(t){var e=t.match(this.uriSchemeRegex)[0].toLowerCase();return"javascript:"!==e&&"vbscript:"!==e},urlMatchDoesNotHaveProtocolOrDot:function(t,e){return!(!t||e&&this.hasFullProtocolRegex.test(e)||-1!==t.indexOf("."))},urlMatchDoesNotHaveAtLeastOneWordChar:function(t,e){return t&&e?!this.hasWordCharAfterProtocolRegex.test(t):!1}},t.truncate.TruncateEnd=function(e,r,a){return t.Util.ellipsis(e,r,a)},t.truncate.TruncateMiddle=function(t,e,r){if(t.length<=e)return t;var a=e-r.length,n="";return a>0&&(n=t.substr(-1*Math.floor(a/2))),(t.substr(0,Math.ceil(a/2))+r+n).substr(0,e)},t.truncate.TruncateSmart=function(t,e,r){var a=function(t){var e={},r=t,a=r.match(/^([a-z]+):\/\//i);return a&&(e.scheme=a[1],r=r.substr(a[0].length)),a=r.match(/^(.*?)(?=(\?|#|\/|$))/i),a&&(e.host=a[1],r=r.substr(a[0].length)),a=r.match(/^\/(.*?)(?=(\?|#|$))/i),a&&(e.path=a[1],r=r.substr(a[0].length)),a=r.match(/^\?(.*?)(?=(#|$))/i),a&&(e.query=a[1],r=r.substr(a[0].length)),a=r.match(/^#(.*?)$/i),a&&(e.fragment=a[1]),e},n=function(t){var e="";return t.scheme&&t.host&&(e+=t.scheme+"://"),t.host&&(e+=t.host),t.path&&(e+="/"+t.path),t.query&&(e+="?"+t.query),t.fragment&&(e+="#"+t.fragment),e},i=function(t,e){var a=e/2,n=Math.ceil(a),i=-1*Math.floor(a),s="";return 0>i&&(s=t.substr(i)),t.substr(0,n)+r+s};if(t.length<=e)return t;var s=e-r.length,o=a(t);if(o.query){var c=o.query.match(/^(.*?)(?=(\?|\#))(.*?)$/i);c&&(o.query=o.query.substr(0,c[1].length),t=n(o))}if(t.length<=e)return t;if(o.host&&(o.host=o.host.replace(/^www\./,""),t=n(o)),t.length<=e)return t;var h="";if(o.host&&(h+=o.host),h.length>=s)return o.host.length==e?(o.host.substr(0,e-r.length)+r).substr(0,e):i(h,s).substr(0,e);var l="";if(o.path&&(l+="/"+o.path),o.query&&(l+="?"+o.query),l){if((h+l).length>=s){if((h+l).length==e)return(h+l).substr(0,e);var u=s-h.length;return(h+i(l,u)).substr(0,e)}h+=l}if(o.fragment){var g="#"+o.fragment;if((h+g).length>=s){if((h+g).length==e)return(h+g).substr(0,e);var m=s-h.length;return(h+i(g,m)).substr(0,e)}h+=g}if(o.scheme&&o.host){var f=o.scheme+"://";if((h+f).length<s)return(f+h).substr(0,e)}if(h.length<=e)return h;var p="";return s>0&&(p=h.substr(-1*Math.floor(s/2))),(h.substr(0,Math.ceil(s/2))+r+p).substr(0,e)},t}); \ No newline at end of file +!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Autolinker=e()}(this,function(){var t=function(e){e=e||{},this.version=t.version,this.urls=this.normalizeUrlsCfg(e.urls),this.email="boolean"!=typeof e.email||e.email,this.twitter="boolean"!=typeof e.twitter||e.twitter,this.phone="boolean"!=typeof e.phone||e.phone,this.hashtag=e.hashtag||!1,this.newWindow="boolean"!=typeof e.newWindow||e.newWindow,this.stripPrefix="boolean"!=typeof e.stripPrefix||e.stripPrefix;var r=this.hashtag;if(r!==!1&&"twitter"!==r&&"facebook"!==r&&"instagram"!==r)throw new Error("invalid `hashtag` cfg - see docs");this.truncate=this.normalizeTruncateCfg(e.truncate),this.className=e.className||"",this.replaceFn=e.replaceFn||null,this.htmlParser=null,this.matchers=null,this.tagBuilder=null};return t.link=function(e,r){var a=new t(r);return a.link(e)},t.version="0.28.0",t.prototype={constructor:t,normalizeUrlsCfg:function(t){return null==t&&(t=!0),"boolean"==typeof t?{schemeMatches:t,wwwMatches:t,tldMatches:t}:{schemeMatches:"boolean"!=typeof t.schemeMatches||t.schemeMatches,wwwMatches:"boolean"!=typeof t.wwwMatches||t.wwwMatches,tldMatches:"boolean"!=typeof t.tldMatches||t.tldMatches}},normalizeTruncateCfg:function(e){return"number"==typeof e?{length:e,location:"end"}:t.Util.defaults(e||{},{length:Number.POSITIVE_INFINITY,location:"end"})},parse:function(t){for(var e=this.getHtmlParser(),r=e.parse(t),a=0,n=[],i=0,s=r.length;i<s;i++){var o=r[i],c=o.getType();if("element"===c&&"a"===o.getTagName())o.isClosing()?a=Math.max(a-1,0):a++;else if("text"===c&&0===a){var h=this.parseText(o.getText(),o.getOffset());n.push.apply(n,h)}}return n=this.compactMatches(n),n=this.removeUnwantedMatches(n)},compactMatches:function(t){t.sort(function(t,e){return t.getOffset()-e.getOffset()});for(var e=0;e<t.length-1;e++)for(var r=t[e],a=r.getOffset()+r.getMatchedText().length;e+1<t.length&&t[e+1].getOffset()<=a;)t.splice(e+1,1);return t},removeUnwantedMatches:function(e){var r=t.Util.remove;return this.hashtag||r(e,function(t){return"hashtag"===t.getType()}),this.email||r(e,function(t){return"email"===t.getType()}),this.phone||r(e,function(t){return"phone"===t.getType()}),this.twitter||r(e,function(t){return"twitter"===t.getType()}),this.urls.schemeMatches||r(e,function(t){return"url"===t.getType()&&"scheme"===t.getUrlMatchType()}),this.urls.wwwMatches||r(e,function(t){return"url"===t.getType()&&"www"===t.getUrlMatchType()}),this.urls.tldMatches||r(e,function(t){return"url"===t.getType()&&"tld"===t.getUrlMatchType()}),e},parseText:function(t,e){e=e||0;for(var r=this.getMatchers(),a=[],n=0,i=r.length;n<i;n++){for(var s=r[n].parseMatches(t),o=0,c=s.length;o<c;o++)s[o].setOffset(e+s[o].getOffset());a.push.apply(a,s)}return a},link:function(t){if(!t)return"";for(var e=this.parse(t),r=[],a=0,n=0,i=e.length;n<i;n++){var s=e[n];r.push(t.substring(a,s.getOffset())),r.push(this.createMatchReturnVal(s)),a=s.getOffset()+s.getMatchedText().length}return r.push(t.substring(a)),r.join("")},createMatchReturnVal:function(e){var r;if(this.replaceFn&&(r=this.replaceFn.call(this,this,e)),"string"==typeof r)return r;if(r===!1)return e.getMatchedText();if(r instanceof t.HtmlTag)return r.toAnchorString();var a=e.buildTag();return a.toAnchorString()},getHtmlParser:function(){var e=this.htmlParser;return e||(e=this.htmlParser=new t.htmlParser.HtmlParser),e},getMatchers:function(){if(this.matchers)return this.matchers;var e=t.matcher,r=this.getTagBuilder(),a=[new e.Hashtag({tagBuilder:r,serviceName:this.hashtag}),new e.Email({tagBuilder:r}),new e.Phone({tagBuilder:r}),new e.Twitter({tagBuilder:r}),new e.Url({tagBuilder:r,stripPrefix:this.stripPrefix})];return this.matchers=a},getTagBuilder:function(){var e=this.tagBuilder;return e||(e=this.tagBuilder=new t.AnchorTagBuilder({newWindow:this.newWindow,truncate:this.truncate,className:this.className})),e}},t.match={},t.matcher={},t.htmlParser={},t.truncate={},t.Util={abstractMethod:function(){throw"abstract"},trimRegex:/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,assign:function(t,e){for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);return t},defaults:function(t,e){for(var r in e)e.hasOwnProperty(r)&&void 0===t[r]&&(t[r]=e[r]);return t},extend:function(e,r){var a=e.prototype,n=function(){};n.prototype=a;var i;i=r.hasOwnProperty("constructor")?r.constructor:function(){a.constructor.apply(this,arguments)};var s=i.prototype=new n;return s.constructor=i,s.superclass=a,delete r.constructor,t.Util.assign(s,r),i},ellipsis:function(t,e,r){return t.length>e&&(r=null==r?"..":r,t=t.substring(0,e-r.length)+r),t},indexOf:function(t,e){if(Array.prototype.indexOf)return t.indexOf(e);for(var r=0,a=t.length;r<a;r++)if(t[r]===e)return r;return-1},remove:function(t,e){for(var r=t.length-1;r>=0;r--)e(t[r])===!0&&t.splice(r,1)},splitAndCapture:function(t,e){for(var r,a=[],n=0;r=e.exec(t);)a.push(t.substring(n,r.index)),a.push(r[0]),n=r.index+r[0].length;return a.push(t.substring(n)),a},trim:function(t){return t.replace(this.trimRegex,"")}},t.HtmlTag=t.Util.extend(Object,{whitespaceRegex:/\s+/,constructor:function(e){t.Util.assign(this,e),this.innerHtml=this.innerHtml||this.innerHTML},setTagName:function(t){return this.tagName=t,this},getTagName:function(){return this.tagName||""},setAttr:function(t,e){var r=this.getAttrs();return r[t]=e,this},getAttr:function(t){return this.getAttrs()[t]},setAttrs:function(e){var r=this.getAttrs();return t.Util.assign(r,e),this},getAttrs:function(){return this.attrs||(this.attrs={})},setClass:function(t){return this.setAttr("class",t)},addClass:function(e){for(var r,a=this.getClass(),n=this.whitespaceRegex,i=t.Util.indexOf,s=a?a.split(n):[],o=e.split(n);r=o.shift();)i(s,r)===-1&&s.push(r);return this.getAttrs()["class"]=s.join(" "),this},removeClass:function(e){for(var r,a=this.getClass(),n=this.whitespaceRegex,i=t.Util.indexOf,s=a?a.split(n):[],o=e.split(n);s.length&&(r=o.shift());){var c=i(s,r);c!==-1&&s.splice(c,1)}return this.getAttrs()["class"]=s.join(" "),this},getClass:function(){return this.getAttrs()["class"]||""},hasClass:function(t){return(" "+this.getClass()+" ").indexOf(" "+t+" ")!==-1},setInnerHtml:function(t){return this.innerHtml=t,this},getInnerHtml:function(){return this.innerHtml||""},toAnchorString:function(){var t=this.getTagName(),e=this.buildAttrsStr();return e=e?" "+e:"",["<",t,e,">",this.getInnerHtml(),"</",t,">"].join("")},buildAttrsStr:function(){if(!this.attrs)return"";var t=this.getAttrs(),e=[];for(var r in t)t.hasOwnProperty(r)&&e.push(r+'="'+t[r]+'"');return e.join(" ")}}),t.RegexLib=function(){var t="A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-ˈ-Ë‘Ë -ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ÒÒŠ-Ô¯Ô±-Õ–Õ™Õ¡-Ö‡×-תװ-×²Ø -يٮٯٱ-Û“Û•Û¥Û¦Û®Û¯Ûº-Û¼Û¿ÜÜ’-ܯÝ-ޥޱߊ-ßªß´ßµßºà €-à •à šà ¤à ¨à¡€-ࡘࢠ-ࢴऄ-हऽà¥à¥˜-ॡॱ-ঀঅ-ঌà¦à¦à¦“-নপ-রলশ-হঽৎড়à§à§Ÿ-ৡৰৱਅ-ਊà¨à¨à¨“-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-àªàª-ઑઓ-નપ-રલળવ-હઽà«à« ૡૹଅ-ଌà¬à¬à¬“-ନପ-ରଲଳଵ-ହଽàœààŸ-à¡à±à®ƒà®…-ஊஎ-à®à®’-கஙசஜஞடணதந-பம-ஹà¯à°…-ఌఎ-à°à°’-నప-హఽౘ-ౚౠౡಅ-ಌಎ-à²à²’-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-à´à´’-ഺഽൎൟ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-à·†à¸-ะาำเ-ๆàºàº‚ຄງຈຊàºàº”-ທນ-ຟມ-ຣລວສຫàº-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿá-á•áš-áá¡á¥á¦á®-á°áµ-á‚á‚Žá‚ -ჅჇáƒáƒ-ჺჼ-ቈቊ-á‰á‰-ቖቘቚ-á‰á‰ -ኈኊ-áŠáŠ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-áŒáŒ’-ጕጘ-ášáŽ€-áŽáŽ -áµá¸-á½á-ᙬᙯ-ᙿáš-áššáš -ᛪᛱ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱá€-á‘á -á¬á®-á°áž€-ឳៗៜá -ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤞá¥-á¥á¥°-ᥴᦀ-ᦫᦰ-ᧉᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳá…-á‹á®ƒ-ᮠᮮᮯᮺ-ᯥᰀ-á°£á±-á±á±š-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-á¼á¼ -ὅὈ-á½á½-ὗὙὛá½á½Ÿ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-á¿Œá¿-á¿“á¿–-Ίῠ-Ῥῲ-ῴῶ-ῼâ±â¿â‚-ₜℂℇℊ-â„“â„•â„™-â„ℤΩℨK-â„ℯ-ℹℼ-â„¿â……-ⅉⅎↃↄⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧâ´â´°-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-â·Žâ·-â·–â·˜-ⷞⸯ々〆〱-〵〻〼ã-ã‚–ã‚-ã‚Ÿã‚¡-ヺー-ヿㄅ-ã„ㄱ-ㆎㆠ-ㆺㇰ-ㇿã€-䶵一-鿕ꀀ-ê’Œê“-ꓽꔀ-ꘌê˜-ꘟꘪꘫꙀ-ꙮꙿ-êšêš -ꛥꜗ-ꜟꜢ-ꞈꞋ-êžêž°-ꞷꟷ-ê ê ƒ-ê …ê ‡-ê Šê Œ-ê ¢ê¡€-ꡳꢂ-ꢳꣲ-ꣷꣻꣽꤊ-ꤥꤰ-ê¥†ê¥ -ꥼꦄ-ꦲê§ê§ -ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ê©‚ê©„-ê©‹ê© -ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ê«ê« -ꫪꫲ-ê«´ê¬-ꬆꬉ-ꬎꬑ-ê¬–ê¬ -ꬦꬨ-ꬮꬰ-êšêœ-ê¥ê°-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-ï©ï©°-龎ff-stﬓ-ﬗï¬ï¬Ÿ-ﬨשׁ-זּטּ-לּמּï€ïïƒï„ï†-ﮱﯓ-ï´½ïµ-ï¶ï¶’-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Zï½-zヲ-하-ᅦᅧ-ï¿ï¿’-ï¿—ï¿š-ï¿œ",e="0-9Ù -Ù©Û°-۹߀-߉०-९০-৯੦-੯૦-૯à¦-à¯à¯¦-௯౦-౯೦-೯൦-൯෦-à·¯à¹-๙à»-໙༠-༩á€-á‰á‚-႙០-៩á -á ™á¥†-á¥á§-᧙᪀-᪉áª-᪙á-á™á®°-᮹᱀-᱉á±-á±™ê˜ -꘩ê£-꣙꤀-꤉ê§-꧙꧰-꧹ê©-꩙꯰-꯹ï¼-ï¼™",r=t+e,a=new RegExp("["+r+".\\-]*["+r+"\\-]"),n=/(?:travelersinsurance|sandvikcoromant|kerryproperties|cancerresearch|weatherchannel|kerrylogistics|spreadbetting|international|wolterskluwer|lifeinsurance|construction|pamperedchef|scholarships|versicherung|bridgestone|creditunion|kerryhotels|investments|productions|blackfriday|enterprises|lamborghini|photography|motorcycles|williamhill|playstation|contractors|barclaycard|accountants|redumbrella|engineering|management|telefonica|protection|consulting|tatamotors|creditcard|vlaanderen|schaeffler|associates|properties|foundation|republican|bnpparibas|boehringer|eurovision|extraspace|industries|immobilien|university|technology|volkswagen|healthcare|restaurant|cuisinella|vistaprint|apartments|accountant|travelers|homedepot|institute|vacations|furniture|fresenius|insurance|christmas|bloomberg|solutions|barcelona|firestone|financial|kuokgroup|fairwinds|community|passagens|goldpoint|equipment|lifestyle|yodobashi|aquarelle|marketing|analytics|education|amsterdam|statefarm|melbourne|allfinanz|directory|microsoft|stockholm|montblanc|accenture|lancaster|landrover|everbank|istanbul|graphics|grainger|ipiranga|softbank|attorney|pharmacy|saarland|catering|airforce|yokohama|mortgage|frontier|mutuelle|stcgroup|memorial|pictures|football|symantec|cipriani|ventures|telecity|cityeats|verisign|flsmidth|boutique|cleaning|firmdale|clinique|clothing|redstone|infiniti|deloitte|feedback|services|broadway|plumbing|commbank|training|barclays|exchange|computer|brussels|software|delivery|barefoot|builders|business|bargains|engineer|holdings|download|security|helsinki|lighting|movistar|discount|hdfcbank|supplies|marriott|property|diamonds|capetown|partners|democrat|jpmorgan|bradesco|budapest|rexroth|zuerich|shriram|academy|science|support|youtube|singles|surgery|alibaba|statoil|dentist|schwarz|android|cruises|cricket|digital|markets|starhub|systems|courses|coupons|netbank|country|domains|corsica|network|neustar|realtor|lincoln|limited|schmidt|yamaxun|cooking|contact|auction|spiegel|liaison|leclerc|latrobe|lasalle|abogado|compare|lanxess|exposed|express|company|cologne|college|avianca|lacaixa|fashion|recipes|ferrero|komatsu|storage|wanggou|clubmed|sandvik|fishing|fitness|bauhaus|kitchen|flights|florist|flowers|watches|weather|temasek|samsung|bentley|forsale|channel|theater|frogans|theatre|okinawa|website|tickets|jewelry|gallery|tiffany|iselect|shiksha|brother|organic|wedding|genting|toshiba|origins|philips|hyundai|hotmail|hoteles|hosting|rentals|windows|cartier|bugatti|holiday|careers|whoswho|hitachi|panerai|caravan|reviews|guitars|capital|trading|hamburg|hangout|finance|stream|family|abbott|health|review|travel|report|hermes|hiphop|gratis|career|toyota|hockey|dating|repair|google|social|soccer|reisen|global|otsuka|giving|unicom|casino|photos|center|broker|rocher|orange|bostik|garden|insure|ryukyu|bharti|safety|physio|sakura|oracle|online|jaguar|gallup|piaget|tienda|futbol|pictet|joburg|webcam|berlin|office|juegos|kaufen|chanel|chrome|xihuan|church|tennis|circle|kinder|flickr|bayern|claims|clinic|viajes|nowruz|xperia|norton|yachts|studio|coffee|camera|sanofi|nissan|author|expert|events|comsec|lawyer|tattoo|viking|estate|villas|condos|realty|yandex|energy|emerck|virgin|vision|durban|living|school|coupon|london|taobao|natura|taipei|nagoya|luxury|walter|aramco|sydney|madrid|credit|maison|makeup|schule|market|anquan|direct|design|swatch|suzuki|alsace|vuelos|dental|alipay|voyage|shouji|voting|airtel|mutual|degree|supply|agency|museum|mobily|dealer|monash|select|mormon|active|moscow|racing|datsun|quebec|nissay|rodeo|email|gifts|works|photo|chloe|edeka|cheap|earth|vista|tushu|koeln|glass|shoes|globo|tunes|gmail|nokia|space|kyoto|black|ricoh|seven|lamer|sener|epson|cisco|praxi|trust|citic|crown|shell|lease|green|legal|lexus|ninja|tatar|gripe|nikon|group|video|wales|autos|gucci|party|nexus|guide|linde|adult|parts|amica|lixil|boats|azure|loans|locus|cymru|lotte|lotto|stada|click|poker|quest|dabur|lupin|nadex|paris|faith|dance|canon|place|gives|trade|skype|rocks|mango|cloud|boots|smile|final|swiss|homes|honda|media|horse|cards|deals|watch|bosch|house|pizza|miami|osaka|tours|total|xerox|coach|sucks|style|delta|toray|iinet|tools|money|codes|beats|tokyo|salon|archi|movie|baidu|study|actor|yahoo|store|apple|world|forex|today|bible|tmall|tirol|irish|tires|forum|reise|vegas|vodka|sharp|omega|weber|jetzt|audio|promo|build|bingo|chase|gallo|drive|dubai|rehab|press|solar|sale|beer|bbva|bank|band|auto|sapo|sarl|saxo|audi|asia|arte|arpa|army|yoga|ally|zara|scor|scot|sexy|seat|zero|seek|aero|adac|zone|aarp|maif|meet|meme|menu|surf|mini|mobi|mtpc|porn|desi|star|ltda|name|talk|navy|love|loan|live|link|news|limo|like|spot|life|nico|lidl|lgbt|land|taxi|team|tech|kred|kpmg|sony|song|kiwi|kddi|jprs|jobs|sohu|java|itau|tips|info|immo|icbc|hsbc|town|host|page|toys|here|help|pars|haus|guru|guge|tube|goog|golf|gold|sncf|gmbh|gift|ggee|gent|gbiz|game|vana|pics|fund|ford|ping|pink|fish|film|fast|farm|play|fans|fail|plus|skin|pohl|fage|moda|post|erni|dvag|prod|doha|prof|docs|viva|diet|luxe|site|dell|sina|dclk|show|qpon|date|vote|cyou|voto|read|coop|cool|wang|club|city|chat|cern|cash|reit|rent|casa|cars|care|camp|rest|call|cafe|weir|wien|rich|wiki|buzz|wine|book|bond|room|work|rsvp|shia|ruhr|blue|bing|shaw|bike|safe|xbox|best|pwc|mtn|lds|aig|boo|fyi|nra|nrw|ntt|car|gal|obi|zip|aeg|vin|how|one|ong|onl|dad|ooo|bet|esq|org|htc|bar|uol|ibm|ovh|gdn|ice|icu|uno|gea|ifm|bot|top|wtf|lol|day|pet|eus|wtc|ubs|tvs|aco|ing|ltd|ink|tab|abb|afl|cat|int|pid|pin|bid|cba|gle|com|cbn|ads|man|wed|ceb|gmo|sky|ist|gmx|tui|mba|fan|ski|iwc|app|pro|med|ceo|jcb|jcp|goo|dev|men|aaa|meo|pub|jlc|bom|jll|gop|jmp|mil|got|gov|win|jot|mma|joy|trv|red|cfa|cfd|bio|moe|moi|mom|ren|biz|aws|xin|bbc|dnp|buy|kfh|mov|thd|xyz|fit|kia|rio|rip|kim|dog|vet|nyc|bcg|mtr|bcn|bms|bmw|run|bzh|rwe|tel|stc|axa|kpn|fly|krd|cab|bnl|foo|crs|eat|tci|sap|srl|nec|sas|net|cal|sbs|sfr|sca|scb|csc|edu|new|xxx|hiv|fox|wme|ngo|nhk|vip|sex|frl|lat|yun|law|you|tax|soy|sew|om|ac|hu|se|sc|sg|sh|sb|sa|rw|ru|rs|ro|re|qa|py|si|pw|pt|ps|sj|sk|pr|pn|pm|pl|sl|sm|pk|sn|ph|so|pg|pf|pe|pa|zw|nz|nu|nr|np|no|nl|ni|ng|nf|sr|ne|st|nc|na|mz|my|mx|mw|mv|mu|mt|ms|mr|mq|mp|mo|su|mn|mm|ml|mk|mh|mg|me|sv|md|mc|sx|sy|ma|ly|lv|sz|lu|lt|ls|lr|lk|li|lc|lb|la|tc|kz|td|ky|kw|kr|kp|kn|km|ki|kh|tf|tg|th|kg|ke|jp|jo|jm|je|it|is|ir|tj|tk|tl|tm|iq|tn|to|io|in|im|il|ie|ad|sd|ht|hr|hn|hm|tr|hk|gy|gw|gu|gt|gs|gr|gq|tt|gp|gn|gm|gl|tv|gi|tw|tz|ua|gh|ug|uk|gg|gf|ge|gd|us|uy|uz|va|gb|ga|vc|ve|fr|fo|fm|fk|fj|vg|vi|fi|eu|et|es|er|eg|ee|ec|dz|do|dm|dk|vn|dj|de|cz|cy|cx|cw|vu|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|wf|bz|by|bw|bv|bt|bs|br|bo|bn|bm|bj|bi|ws|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ye|ar|aq|ao|am|al|yt|ai|za|ag|af|ae|zm|id)\b/;return{alphaNumericCharsStr:r,domainNameRegex:a,tldRegex:n}}(),t.AnchorTagBuilder=t.Util.extend(Object,{constructor:function(e){t.Util.assign(this,e)},build:function(e){return new t.HtmlTag({tagName:"a",attrs:this.createAttrs(e.getType(),e.getAnchorHref()),innerHtml:this.processAnchorText(e.getAnchorText())})},createAttrs:function(t,e){var r={href:e},a=this.createCssClass(t);return a&&(r["class"]=a),this.newWindow&&(r.target="_blank",r.rel="noopener noreferrer"),r},createCssClass:function(t){var e=this.className;return e?e+" "+e+"-"+t:""},processAnchorText:function(t){return t=this.doTruncate(t)},doTruncate:function(e){var r=this.truncate;if(!r||!r.length)return e;var a=r.length,n=r.location;return"smart"===n?t.truncate.TruncateSmart(e,a,".."):"middle"===n?t.truncate.TruncateMiddle(e,a,".."):t.truncate.TruncateEnd(e,a,"..")}}),t.htmlParser.HtmlParser=t.Util.extend(Object,{htmlRegex:function(){var t=/!--([\s\S]+?)--/,e=/[0-9a-zA-Z][0-9a-zA-Z:]*/,r=/[^\s"'>\/=\x00-\x1F\x7F]+/,a=/(?:"[^"]*?"|'[^']*?'|[^'"=<>`\s]+)/,n=r.source+"(?:\\s*=\\s*"+a.source+")?";return new RegExp(["(?:","<(!DOCTYPE)","(?:","\\s+","(?:",n,"|",a.source+")",")*",">",")","|","(?:","<(/)?","(?:",t.source,"|","(?:","("+e.source+")","(?:","(?:\\s+|\\b)",n,")*","\\s*/?",")",")",">",")"].join(""),"gi")}(),htmlCharacterEntitiesRegex:/( | |<|<|>|>|"|"|')/gi,parse:function(t){for(var e,r,a=this.htmlRegex,n=0,i=[];null!==(e=a.exec(t));){var s=e[0],o=e[3],c=e[1]||e[4],h=!!e[2],l=e.index,u=t.substring(n,l);u&&(r=this.parseTextAndEntityNodes(n,u),i.push.apply(i,r)),o?i.push(this.createCommentNode(l,s,o)):i.push(this.createElementNode(l,s,c,h)),n=l+s.length}if(n<t.length){var g=t.substring(n);g&&(r=this.parseTextAndEntityNodes(n,g),i.push.apply(i,r))}return i},parseTextAndEntityNodes:function(e,r){for(var a=[],n=t.Util.splitAndCapture(r,this.htmlCharacterEntitiesRegex),i=0,s=n.length;i<s;i+=2){var o=n[i],c=n[i+1];o&&(a.push(this.createTextNode(e,o)),e+=o.length),c&&(a.push(this.createEntityNode(e,c)),e+=c.length)}return a},createCommentNode:function(e,r,a){return new t.htmlParser.CommentNode({offset:e,text:r,comment:t.Util.trim(a)})},createElementNode:function(e,r,a,n){return new t.htmlParser.ElementNode({offset:e,text:r,tagName:a.toLowerCase(),closing:n})},createEntityNode:function(e,r){return new t.htmlParser.EntityNode({offset:e,text:r})},createTextNode:function(e,r){return new t.htmlParser.TextNode({offset:e,text:r})}}),t.htmlParser.HtmlNode=t.Util.extend(Object,{offset:void 0,text:void 0,constructor:function(e){t.Util.assign(this,e)},getType:t.Util.abstractMethod,getOffset:function(){return this.offset},getText:function(){return this.text}}),t.htmlParser.CommentNode=t.Util.extend(t.htmlParser.HtmlNode,{comment:"",getType:function(){return"comment"},getComment:function(){return this.comment}}),t.htmlParser.ElementNode=t.Util.extend(t.htmlParser.HtmlNode,{tagName:"",closing:!1,getType:function(){return"element"},getTagName:function(){return this.tagName},isClosing:function(){return this.closing}}),t.htmlParser.EntityNode=t.Util.extend(t.htmlParser.HtmlNode,{getType:function(){return"entity"}}),t.htmlParser.TextNode=t.Util.extend(t.htmlParser.HtmlNode,{getType:function(){return"text"}}),t.match.Match=t.Util.extend(Object,{constructor:function(t){this.tagBuilder=t.tagBuilder,this.matchedText=t.matchedText,this.offset=t.offset},getType:t.Util.abstractMethod,getMatchedText:function(){return this.matchedText},setOffset:function(t){this.offset=t},getOffset:function(){return this.offset},getAnchorHref:t.Util.abstractMethod,getAnchorText:t.Util.abstractMethod,buildTag:function(){return this.tagBuilder.build(this)}}),t.match.Email=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.email=e.email},getType:function(){return"email"},getEmail:function(){return this.email},getAnchorHref:function(){return"mailto:"+this.email},getAnchorText:function(){return this.email}}),t.match.Hashtag=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.serviceName=e.serviceName,this.hashtag=e.hashtag},getType:function(){return"hashtag"},getServiceName:function(){return this.serviceName},getHashtag:function(){return this.hashtag},getAnchorHref:function(){var t=this.serviceName,e=this.hashtag;switch(t){case"twitter":return"https://twitter.com/hashtag/"+e;case"facebook":return"https://www.facebook.com/hashtag/"+e;case"instagram":return"https://instagram.com/explore/tags/"+e;default:throw new Error("Unknown service name to point hashtag to: ",t)}},getAnchorText:function(){return"#"+this.hashtag}}),t.match.Phone=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.number=e.number,this.plusSign=e.plusSign},getType:function(){return"phone"},getNumber:function(){return this.number},getAnchorHref:function(){return"tel:"+(this.plusSign?"+":"")+this.number},getAnchorText:function(){return this.matchedText}}),t.match.Twitter=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.twitterHandle=e.twitterHandle},getType:function(){return"twitter"},getTwitterHandle:function(){return this.twitterHandle},getAnchorHref:function(){return"https://twitter.com/"+this.twitterHandle},getAnchorText:function(){return"@"+this.twitterHandle}}),t.match.Url=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.urlMatchType=e.urlMatchType,this.url=e.url,this.protocolUrlMatch=e.protocolUrlMatch,this.protocolRelativeMatch=e.protocolRelativeMatch,this.stripPrefix=e.stripPrefix},urlPrefixRegex:/^(https?:\/\/)?(www\.)?/i,protocolRelativeRegex:/^\/\//,protocolPrepended:!1,getType:function(){return"url"},getUrlMatchType:function(){return this.urlMatchType},getUrl:function(){var t=this.url;return this.protocolRelativeMatch||this.protocolUrlMatch||this.protocolPrepended||(t=this.url="http://"+t,this.protocolPrepended=!0),t},getAnchorHref:function(){var t=this.getUrl();return t.replace(/&/g,"&")},getAnchorText:function(){var t=this.getMatchedText();return this.protocolRelativeMatch&&(t=this.stripProtocolRelativePrefix(t)),this.stripPrefix&&(t=this.stripUrlPrefix(t)),t=this.removeTrailingSlash(t)},stripUrlPrefix:function(t){return t.replace(this.urlPrefixRegex,"")},stripProtocolRelativePrefix:function(t){return t.replace(this.protocolRelativeRegex,"")},removeTrailingSlash:function(t){return"/"===t.charAt(t.length-1)&&(t=t.slice(0,-1)),t}}),t.matcher.Matcher=t.Util.extend(Object,{constructor:function(t){this.tagBuilder=t.tagBuilder},parseMatches:t.Util.abstractMethod}),t.matcher.Email=t.Util.extend(t.matcher.Matcher,{matcherRegex:function(){var e=t.RegexLib.alphaNumericCharsStr,r=new RegExp("["+e+"\\-_';:&=+$.,]+@"),a=t.RegexLib.domainNameRegex,n=t.RegexLib.tldRegex;return new RegExp([r.source,a.source,"\\.",n.source].join(""),"gi")}(),parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.tagBuilder,i=[];null!==(r=a.exec(e));){var s=r[0];i.push(new t.match.Email({tagBuilder:n,matchedText:s,offset:r.index,email:s}))}return i}}),t.matcher.Hashtag=t.Util.extend(t.matcher.Matcher,{matcherRegex:new RegExp("#[_"+t.RegexLib.alphaNumericCharsStr+"]{1,139}","g"),nonWordCharRegex:new RegExp("[^"+t.RegexLib.alphaNumericCharsStr+"]"),constructor:function(e){t.matcher.Matcher.prototype.constructor.call(this,e),this.serviceName=e.serviceName},parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.nonWordCharRegex,i=this.serviceName,s=this.tagBuilder,o=[];null!==(r=a.exec(e));){var c=r.index,h=e.charAt(c-1);if(0===c||n.test(h)){var l=r[0],u=r[0].slice(1);o.push(new t.match.Hashtag({tagBuilder:s,matchedText:l,offset:c,serviceName:i,hashtag:u}))}}return o}}),t.matcher.Phone=t.Util.extend(t.matcher.Matcher,{matcherRegex:/(?:(\+)?\d{1,3}[-\040.])?\(?\d{3}\)?[-\040.]?\d{3}[-\040.]\d{4}/g,parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.tagBuilder,i=[];null!==(r=a.exec(e));){var s=r[0],o=s.replace(/\D/g,""),c=!!r[1];i.push(new t.match.Phone({tagBuilder:n,matchedText:s,offset:r.index,number:o,plusSign:c}))}return i}}),t.matcher.Twitter=t.Util.extend(t.matcher.Matcher,{matcherRegex:new RegExp("@[_"+t.RegexLib.alphaNumericCharsStr+"]{1,20}","g"),nonWordCharRegex:new RegExp("[^"+t.RegexLib.alphaNumericCharsStr+"]"),parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.nonWordCharRegex,i=this.tagBuilder,s=[];null!==(r=a.exec(e));){var o=r.index,c=e.charAt(o-1);if(0===o||n.test(c)){var h=r[0],l=r[0].slice(1);s.push(new t.match.Twitter({tagBuilder:i,matchedText:h,offset:o,twitterHandle:l}))}}return s}}),t.matcher.Url=t.Util.extend(t.matcher.Matcher,{matcherRegex:function(){var e=/(?:[A-Za-z][-.+A-Za-z0-9]*:(?![A-Za-z][-.+A-Za-z0-9]*:\/\/)(?!\d+\/?)(?:\/\/)?)/,r=/(?:www\.)/,a=t.RegexLib.domainNameRegex,n=t.RegexLib.tldRegex,i=t.RegexLib.alphaNumericCharsStr,s=new RegExp("["+i+"\\-+&@#/%=~_()|'$*\\[\\]?!:,.;]*["+i+"\\-+&@#/%=~_()|'$*\\[\\]]");return new RegExp(["(?:","(",e.source,a.source,")","|","(","(//)?",r.source,a.source,")","|","(","(//)?",a.source+"\\.",n.source,")",")","(?:"+s.source+")?"].join(""),"gi")}(),wordCharRegExp:/\w/,openParensRe:/\(/g,closeParensRe:/\)/g,constructor:function(e){t.matcher.Matcher.prototype.constructor.call(this,e),this.stripPrefix=e.stripPrefix},parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.stripPrefix,i=this.tagBuilder,s=[];null!==(r=a.exec(e));){var o=r[0],c=r[1],h=r[2],l=r[3],u=r[5],g=r.index,m=l||u,f=e.charAt(g-1);if(t.matcher.UrlMatchValidator.isValid(o,c)&&!(g>0&&"@"===f||g>0&&m&&this.wordCharRegExp.test(f))){if(this.matchHasUnbalancedClosingParen(o))o=o.substr(0,o.length-1);else{var p=this.matchHasInvalidCharAfterTld(o,c);p>-1&&(o=o.substr(0,p))}var d=c?"scheme":h?"www":"tld",b=!!c;s.push(new t.match.Url({tagBuilder:i,matchedText:o,offset:g,urlMatchType:d,url:o,protocolUrlMatch:b,protocolRelativeMatch:!!m,stripPrefix:n}))}}return s},matchHasUnbalancedClosingParen:function(t){var e=t.charAt(t.length-1);if(")"===e){var r=t.match(this.openParensRe),a=t.match(this.closeParensRe),n=r&&r.length||0,i=a&&a.length||0;if(n<i)return!0}return!1},matchHasInvalidCharAfterTld:function(t,e){if(!t)return-1;var r=0;e&&(r=t.indexOf(":"),t=t.slice(r));var a=/^((.?\/\/)?[A-Za-z0-9\u00C0-\u017F\.\-]*[A-Za-z0-9\u00C0-\u017F\-]\.[A-Za-z]+)/,n=a.exec(t);return null===n?-1:(r+=n[1].length,t=t.slice(n[1].length),/^[^.A-Za-z:\/?#]/.test(t)?r:-1)}}),t.matcher.UrlMatchValidator={hasFullProtocolRegex:/^[A-Za-z][-.+A-Za-z0-9]*:\/\//,uriSchemeRegex:/^[A-Za-z][-.+A-Za-z0-9]*:/,hasWordCharAfterProtocolRegex:/:[^\s]*?[A-Za-z\u00C0-\u017F]/,ipRegex:/[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?/,isValid:function(t,e){return!(e&&!this.isValidUriScheme(e)||this.urlMatchDoesNotHaveProtocolOrDot(t,e)||this.urlMatchDoesNotHaveAtLeastOneWordChar(t,e)&&!this.isValidIpAddress(t))},isValidIpAddress:function(t){var e=new RegExp(this.hasFullProtocolRegex.source+this.ipRegex.source),r=t.match(e);return null!==r},isValidUriScheme:function(t){var e=t.match(this.uriSchemeRegex)[0].toLowerCase();return"javascript:"!==e&&"vbscript:"!==e},urlMatchDoesNotHaveProtocolOrDot:function(t,e){return!(!t||e&&this.hasFullProtocolRegex.test(e)||t.indexOf(".")!==-1)},urlMatchDoesNotHaveAtLeastOneWordChar:function(t,e){return!(!t||!e)&&!this.hasWordCharAfterProtocolRegex.test(t)}},t.truncate.TruncateEnd=function(e,r,a){return t.Util.ellipsis(e,r,a)},t.truncate.TruncateMiddle=function(t,e,r){if(t.length<=e)return t;var a=e-r.length,n="";return a>0&&(n=t.substr(-1*Math.floor(a/2))),(t.substr(0,Math.ceil(a/2))+r+n).substr(0,e)},t.truncate.TruncateSmart=function(t,e,r){var a=function(t){var e={},r=t,a=r.match(/^([a-z]+):\/\//i);return a&&(e.scheme=a[1],r=r.substr(a[0].length)),a=r.match(/^(.*?)(?=(\?|#|\/|$))/i),a&&(e.host=a[1],r=r.substr(a[0].length)),a=r.match(/^\/(.*?)(?=(\?|#|$))/i),a&&(e.path=a[1],r=r.substr(a[0].length)),a=r.match(/^\?(.*?)(?=(#|$))/i),a&&(e.query=a[1],r=r.substr(a[0].length)),a=r.match(/^#(.*?)$/i),a&&(e.fragment=a[1]),e},n=function(t){var e="";return t.scheme&&t.host&&(e+=t.scheme+"://"),t.host&&(e+=t.host),t.path&&(e+="/"+t.path),t.query&&(e+="?"+t.query),t.fragment&&(e+="#"+t.fragment),e},i=function(t,e){var a=e/2,n=Math.ceil(a),i=-1*Math.floor(a),s="";return i<0&&(s=t.substr(i)),t.substr(0,n)+r+s};if(t.length<=e)return t;var s=e-r.length,o=a(t);if(o.query){var c=o.query.match(/^(.*?)(?=(\?|\#))(.*?)$/i);c&&(o.query=o.query.substr(0,c[1].length),t=n(o))}if(t.length<=e)return t;if(o.host&&(o.host=o.host.replace(/^www\./,""),t=n(o)),t.length<=e)return t;var h="";if(o.host&&(h+=o.host),h.length>=s)return o.host.length==e?(o.host.substr(0,e-r.length)+r).substr(0,e):i(h,s).substr(0,e);var l="";if(o.path&&(l+="/"+o.path),o.query&&(l+="?"+o.query),l){if((h+l).length>=s){if((h+l).length==e)return(h+l).substr(0,e);var u=s-h.length;return(h+i(l,u)).substr(0,e)}h+=l}if(o.fragment){var g="#"+o.fragment;if((h+g).length>=s){if((h+g).length==e)return(h+g).substr(0,e);var m=s-h.length;return(h+i(g,m)).substr(0,e)}h+=g}if(o.scheme&&o.host){var f=o.scheme+"://";if((h+f).length<s)return(f+h).substr(0,e)}if(h.length<=e)return h;var p="";return s>0&&(p=h.substr(-1*Math.floor(s/2))),(h.substr(0,Math.ceil(s/2))+r+p).substr(0,e)},t}); diff --git a/packages/rocketchat-channel-settings/client/views/channelSettings.coffee b/packages/rocketchat-channel-settings/client/views/channelSettings.coffee index 43c310c52f64a754e6d3b5dd7de13b0d4d3a29da..9254eecf1c49441bba8c17c475dab2ebb41d3f4b 100644 --- a/packages/rocketchat-channel-settings/client/views/channelSettings.coffee +++ b/packages/rocketchat-channel-settings/client/views/channelSettings.coffee @@ -1,38 +1,35 @@ Template.channelSettings.helpers - canEdit: -> - return RocketChat.authz.hasAllPermission('edit-room', @rid) - canArchiveOrUnarchive: -> - return RocketChat.authz.hasAtLeastOnePermission(['archive-room', 'unarchive-room'], @rid) + toArray: (obj) -> + arr = [] + for key, value of obj + arr.push + $key: key + $value: value + return arr + + valueOf: (obj, key) -> + return obj?[key] + + showSetting: (setting, room) -> + if setting.showInDirect is false + return room.t isnt 'd' + return true + + settings: -> + return Template.instance().settings + + getRoom: -> + return ChatRoom.findOne(@rid) + editing: (field) -> return Template.instance().editing.get() is field - notDirect: -> - return ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t isnt 'd' - roomType: -> - return ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t + channelSettings: -> return RocketChat.ChannelSettings.getOptions() - roomTypeDescription: -> - roomType = ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t - if roomType is 'c' - return t('Channel') - else if roomType is 'p' - return t('Private_Group') - roomName: -> - return ChatRoom.findOne(@rid, { fields: { name: 1 }})?.name - roomTopic: -> - return ChatRoom.findOne(@rid, { fields: { topic: 1 }})?.topic - roomTopicUnescaped: -> - return s.unescapeHTML ChatRoom.findOne(@rid, { fields: { topic: 1 }})?.topic - roomDescription: -> - return ChatRoom.findOne(@rid, { fields: { description: 1 }})?.description - archivationState: -> - return ChatRoom.findOne(@rid, { fields: { archived: 1 }})?.archived - archivationStateDescription: -> - archivationState = ChatRoom.findOne(@rid, { fields: { archived: 1 }})?.archived - if archivationState is true - return t('Room_archivation_state_true') - else - return t('Room_archivation_state_false') + + unscape: (value) -> + return s.unescapeHTML value + canDeleteRoom: -> roomType = ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t return roomType? and RocketChat.authz.hasAtLeastOnePermission("delete-#{roomType}", @rid) @@ -95,85 +92,120 @@ Template.channelSettings.events Template.channelSettings.onCreated -> @editing = new ReactiveVar - @validateRoomType = => - type = @$('input[name=roomType]:checked').val() - if type not in ['c', 'p'] - toastr.error t('error-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('error-not-allowed') - return false - - name = $('input[name=roomName]').val() + @settings = + name: + type: 'text' + label: 'Name' + canView: (room) => room.t isnt 'd' + canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id) + save: (value, room) -> + if not RocketChat.authz.hasAllPermission('edit-room', room._id) or room.t not in ['c', 'p'] + return toastr.error t('error-not-allowed') + + try + nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' + catch + nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' + + if not nameValidation.test value + return toastr.error t('error-invalid-room-name', { room_name: name: value }) + + if @validateRoomName() + RocketChat.callbacks.run 'roomNameChanged', { _id: room._id, name: value } + Meteor.call 'saveRoomSettings', room._id, 'roomName', value, (err, result) -> + return handleError err if err + toastr.success TAPi18n.__ 'Room_name_changed_successfully' + + topic: + type: 'markdown' + label: 'Topic' + canView: (room) => true + canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id) + save: (value, room) -> + Meteor.call 'saveRoomSettings', room._id, 'roomTopic', value, (err, result) -> + return handleError err if err + toastr.success TAPi18n.__ 'Room_topic_changed_successfully' + RocketChat.callbacks.run 'roomTopicChanged', room + + description: + type: 'text' + label: 'Description' + canView: (room) => room.t isnt 'd' + canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id) + save: (value, room) -> + Meteor.call 'saveRoomSettings', room._id, 'roomDescription', value, (err, result) -> + return handleError err if err + toastr.success TAPi18n.__ 'Room_description_changed_successfully' + + t: + type: 'select' + label: 'Type' + options: + c: 'Channel' + p: 'Private_Group' + canView: (room) => room.t in ['c', 'p'] + canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id) + save: (value, room) -> + console.log value + if value not in ['c', 'p'] + return toastr.error t('error-invalid-room-type', value) + + RocketChat.callbacks.run 'roomTypeChanged', room + Meteor.call 'saveRoomSettings', room._id, 'roomType', value, (err, result) -> + return handleError err if err + toastr.success TAPi18n.__ 'Room_type_changed_successfully' + + ro: + type: 'boolean' + label: 'Read_only' + canView: (room) => room.t isnt 'd' + canEdit: (room) => RocketChat.authz.hasAllPermission('set-readonly', room._id) + save: (value, room) -> + Meteor.call 'saveRoomSettings', room._id, 'readOnly', value, (err, result) -> + return handleError err if err + toastr.success TAPi18n.__ 'Read_only_changed_successfully' + + archived: + type: 'boolean' + label: 'Room_archivation_state_true' + canView: (room) => room.t isnt 'd' + canEdit: (room) => RocketChat.authz.hasAtLeastOnePermission(['archive-room', 'unarchive-room'], room._id) + save: (value, room) -> + if value is true + Meteor.call 'archiveRoom', room._id, (err, results) -> + return handleError err if err + toastr.success TAPi18n.__ 'Room_archived' + RocketChat.callbacks.run 'archiveRoom', room + else + Meteor.call 'unarchiveRoom', room._id, (err, results) -> + return handleError err if err + toastr.success TAPi18n.__ 'Room_unarchived' + RocketChat.callbacks.run 'unarchiveRoom', room + + joinCode: + type: 'text' + label: 'Code' + canView: (room) => room.t is 'c' and RocketChat.authz.hasAllPermission('edit-room', room._id) + canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id) + save: (value, room) -> + Meteor.call 'saveRoomSettings', room._id, 'joinCode', value, (err, result) -> + return handleError err if err + toastr.success TAPi18n.__ 'Room_code_changed_successfully' + RocketChat.callbacks.run 'roomCodeChanged', room - try - nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' - catch - nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' - if not nameValidation.test name - toastr.error t('error-invalid-room-name', { room_name: name: name }) - return false + @saveSetting = => + room = ChatRoom.findOne @data?.rid + field = @editing.get() - return true + if @settings[field].type is 'select' + value = @$(".channel-settings form [name=#{field}]:checked").val() + else if @settings[field].type is 'boolean' + value = @$(".channel-settings form [name=#{field}]:checked").val() is 'true' + else + value = @$(".channel-settings form [name=#{field}]").val() - @validateRoomTopic = => - return true + if value isnt room[field] + @settings[field].save(value, room) - @saveSetting = => - room = ChatRoom.findOne @data?.rid - switch @editing.get() - when 'roomName' - if $('input[name=roomName]').val() is room.name - toastr.success TAPi18n.__ 'Room_name_changed_successfully' - RocketChat.callbacks.run 'roomNameChanged', ChatRoom.findOne(room._id) - else - if @validateRoomName() - RocketChat.callbacks.run 'roomNameChanged', { _id: room._id, name: @$('input[name=roomName]').val() } - Meteor.call 'saveRoomSettings', room._id, 'roomName', @$('input[name=roomName]').val(), (err, result) -> - return handleError err if err - toastr.success TAPi18n.__ 'Room_name_changed_successfully' - when 'roomTopic' - if @validateRoomTopic() - Meteor.call 'saveRoomSettings', room._id, 'roomTopic', @$('input[name=roomTopic]').val(), (err, result) -> - return handleError err if err - toastr.success TAPi18n.__ 'Room_topic_changed_successfully' - RocketChat.callbacks.run 'roomTopicChanged', ChatRoom.findOne(result.rid) - when 'roomType' - if @validateRoomType() - RocketChat.callbacks.run 'roomTypeChanged', room - Meteor.call 'saveRoomSettings', room._id, 'roomType', @$('input[name=roomType]:checked').val(), (err, result) -> - return handleError err if err - toastr.success TAPi18n.__ 'Room_type_changed_successfully' - when 'roomDescription' - if @validateRoomTopic() - Meteor.call 'saveRoomSettings', room._id, 'roomDescription', @$('input[name=roomDescription]').val(), (err, result) -> - return handleError err if err - toastr.success TAPi18n.__ 'Room_description_changed_successfully' - when 'archivationState' - if @$('input[name=archivationState]:checked').val() is 'true' - if room.archived isnt true - Meteor.call 'archiveRoom', room._id, (err, results) -> - return handleError err if err - toastr.success TAPi18n.__ 'Room_archived' - RocketChat.callbacks.run 'archiveRoom', ChatRoom.findOne(room._id) - else - if room.archived is true - Meteor.call 'unarchiveRoom', room._id, (err, results) -> - return handleError err if err - toastr.success TAPi18n.__ 'Room_unarchived' - RocketChat.callbacks.run 'unarchiveRoom', ChatRoom.findOne(room._id) - when 'readOnly' - Meteor.call 'saveRoomSettings', room._id, 'readOnly', @$('input[name=readOnly]:checked').val() is 'true', (err, result) -> - return handleError err if err - toastr.success TAPi18n.__ 'Read_only_changed_successfully' - when 'systemMessages' - Meteor.call 'saveRoomSettings', room._id, 'systemMessages', @$('input[name=systemMessages]:checked').val() is 'true', (err, result) -> - return handleError err if err - toastr.success TAPi18n.__ 'System_messages_setting_changed_successfully' @editing.set() diff --git a/packages/rocketchat-channel-settings/client/views/channelSettings.html b/packages/rocketchat-channel-settings/client/views/channelSettings.html index ed32761314fdc8f04ca0e9320bba5805b8034bb1..c1d0c15fd792d46665784f778b5e3dc4655f631b 100644 --- a/packages/rocketchat-channel-settings/client/views/channelSettings.html +++ b/packages/rocketchat-channel-settings/client/views/channelSettings.html @@ -6,92 +6,75 @@ </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="icon-pencil" data-edit="roomName"></i>{{/if}}</span> - {{/if}} - </div> - </li> - {{/if}} - <li> - <label>{{_ "Topic"}}</label> - <div> - {{#if editing 'roomTopic'}} - <input type="text" name="roomTopic" value="{{roomTopicUnescaped}}" class="editing" /> <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> <button type="button" class="button primary save">{{_ "Save"}}</button> - {{else}} - <span>{{{RocketChatMarkdown roomTopic}}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomTopic"></i>{{/if}}</span> + {{#let room=getRoom}} + {{#each toArray settings}} + + {{#if $value.canView room}} + {{#let value=(valueOf room $key)}} + <li> + <label>{{_ $value.label}}</label> + <div> + {{#if $eq $value.type 'text'}} + {{#if editing $key}} + <input type="text" name="{{$key}}" value="{{value}}" class="editing" /> + {{else}} + <span>{{value}}</span> + {{/if}} + {{/if}} + + {{#if $eq $value.type 'markdown'}} + {{#if editing $key}} + <input type="text" name="{{$key}}" value="{{unscape value}}" class="editing" /> + {{else}} + <span>{{{RocketChatMarkdown value}}}</span> + {{/if}} + {{/if}} + + {{#if $eq $value.type 'select'}} + {{#if editing $key}} + {{#each toArray $value.options}} + <label> + <input type="radio" name="{{../$key}}" value="{{$key}}" checked="{{$eq value $key}}" class="editing" /> + {{_ $value}} + </label> + {{/each}} + {{else}} + <span>{{_ (valueOf $value.options value)}}</span> + {{/if}} + {{/if}} + + {{#if $eq $value.type 'boolean'}} + {{#if editing $key}} + <label> + <input type="radio" name="{{$key}}" value="true" checked="{{$eq value true}}" class="editing" /> + {{_ 'True'}} + </label> + <label> + <input type="radio" name="{{$key}}" value="false" checked="{{$neq value true}}" class="editing" /> + {{_ 'False'}} + </label> + {{else}} + {{#if value}} + <span>{{_ 'True'}}</span> + {{else}} + <span>{{_ 'False'}}</span> + {{/if}} + {{/if}} + {{/if}} + + {{#if editing $key}} + <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> + <button type="button" class="button primary save">{{_ "Save"}}</button> + {{else}} + <span>{{#if $value.canEdit room}} <i class="icon-pencil" data-edit="{{$key}}"></i>{{/if}}</span> + {{/if}} + </div> + </li> + {{/let}} {{/if}} - </div> - </li> - {{#if notDirect}} - <li> - <label>{{_ "Description"}}</label> - <div> - {{#if editing 'roomDescription'}} - <input type="text" name="roomDescription" value="{{roomDescription}}" class="editing" /> <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button><button type="button" class="button primary save">{{_ "Save"}}</button> - {{else}} - <span>{{roomDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomDescription"></i>{{/if}}</span> - {{/if}} - </div> - </li> - <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="icon-pencil" data-edit="roomType"></i>{{/if}}</span> - {{/if}} - </div> - </li> - <li> - <label>{{_ "Room_archivation_state"}}</label> - <div> - {{#if editing 'archivationState'}} - <label><input type="radio" name="archivationState" class="editing" value="true" checked="{{$eq archivationState true}}" /> {{_ "Room_archivation_state_true"}}</label> - <label><input type="radio" name="archivationState" value="false" checked="{{$neq archivationState true}}" /> {{_ "Room_archivation_state_false"}}</label> - <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> - <button type="button" class="button primary save">{{_ "Save"}}</button> - {{else}} - <span>{{archivationStateDescription}}{{#if canArchiveOrUnarchive}} <i class="icon-pencil" data-edit="archivationState"></i>{{/if}}</span> - {{/if}} - </div> - </li> - <li> - <label>{{_ "Read_only"}}</label> - <div> - {{#if editing 'readOnly'}} - <label><input type="radio" name="readOnly" class="editing" value="true" checked="{{$eq readOnly true}}" /> {{_ "True"}}</label> - <label><input type="radio" name="readOnly" value="false" checked="{{$neq readOnly true}}" /> {{_ "False"}}</label> - <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> - <button type="button" class="button primary save">{{_ "Save"}}</button> - {{else}} - <span>{{#if readOnly}}{{_ "True"}}{{else}}{{_ "False"}}{{/if}}{{#if canEdit}} <i class="icon-pencil" data-edit="readOnly"></i>{{/if}}</span> - {{/if}} - </div> - </li> - <li> - <label>{{_ "System_messages"}}</label> - <div> - {{#if editing 'systemMessages'}} - <label><input type="radio" name="systemMessages" class="editing" value="true" checked="{{$eq systemMessages true}}" /> {{_ "On"}}</label> - <label><input type="radio" name="systemMessages" value="false" checked="{{$neq systemMessages true}}" /> {{_ "Off"}}</label> - <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> - <button type="button" class="button primary save">{{_ "Save"}}</button> - {{else}} - <span>{{#if systemMessages}}{{_ "On"}}{{else}}{{_ "Off"}}{{/if}}{{#if canEdit}} <i class="icon-pencil" data-edit="systemMessages"></i>{{/if}}</span> - {{/if}} - </div> - </li> - {{/if}} + {{/each}} + {{/let}} + {{#each channelSettings}} {{> Template.dynamic template=template data=data}} {{/each}} diff --git a/packages/rocketchat-channel-settings/server/functions/saveRoomName.coffee b/packages/rocketchat-channel-settings/server/functions/saveRoomName.coffee index 5f00ae48193431fcde32c9c739659ec4317e8783..929b05fa87120c0d811c83536a815714479fabc8 100644 --- a/packages/rocketchat-channel-settings/server/functions/saveRoomName.coffee +++ b/packages/rocketchat-channel-settings/server/functions/saveRoomName.coffee @@ -1,15 +1,9 @@ -RocketChat.saveRoomName = (rid, name) -> - if not Meteor.userId() - throw new Meteor.Error('error-invalid-user', "Invalid user", { function: 'RocketChat.saveRoomName' }) - +RocketChat.saveRoomName = (rid, name, user) -> room = RocketChat.models.Rooms.findOneById rid if room.t not in ['c', 'p'] throw new Meteor.Error 'error-not-allowed', 'Not allowed', { function: 'RocketChat.saveRoomName' } - unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-room', rid) - throw new Meteor.Error 'error-not-allowed', 'Not allowed', { function: 'RocketChat.saveRoomName' } - try nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' catch diff --git a/packages/rocketchat-channel-settings/server/functions/saveRoomTopic.coffee b/packages/rocketchat-channel-settings/server/functions/saveRoomTopic.coffee index 3ffc8b54f1cac85aa704d0fbdc98f26bd68e3b6b..216eb7a2d10fe36b805c29c83725b3b1bbe208a3 100644 --- a/packages/rocketchat-channel-settings/server/functions/saveRoomTopic.coffee +++ b/packages/rocketchat-channel-settings/server/functions/saveRoomTopic.coffee @@ -6,6 +6,4 @@ RocketChat.saveRoomTopic = (rid, roomTopic, user) -> update = RocketChat.models.Rooms.setTopicById(rid, roomTopic) - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_topic', rid, roomTopic, user - return update diff --git a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.coffee b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.coffee index 47da17997fe294c19eaa5a682168bc956d2ff65c..f9b394f3d80cb172b8231326718dc9a1a674a5ee 100644 --- a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.coffee +++ b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.coffee @@ -1,9 +1,12 @@ Meteor.methods saveRoomSettings: (rid, setting, value) -> + if not Meteor.userId() + throw new Meteor.Error('error-invalid-user', "Invalid user", { function: 'RocketChat.saveRoomName' }) + unless Match.test rid, String throw new Meteor.Error 'error-invalid-room', 'Invalid room', { method: 'saveRoomSettings' } - if setting not in ['roomName', 'roomTopic', 'roomDescription', 'roomType', 'readOnly', 'systemMessages', 'default'] + if setting not in ['roomName', 'roomTopic', 'roomDescription', 'roomType', 'readOnly', 'systemMessages', 'default', 'joincode'] throw new Meteor.Error 'error-invalid-settings', 'Invalid settings provided', { method: 'saveRoomSettings' } unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-room', rid) @@ -21,6 +24,7 @@ Meteor.methods when 'roomTopic' if value isnt room.topic RocketChat.saveRoomTopic(rid, value, Meteor.user()) + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_topic', rid, value, Meteor.user() when 'roomDescription' if value isnt room.description RocketChat.saveRoomDescription rid, value, Meteor.user() @@ -38,6 +42,8 @@ Meteor.methods when 'systemMessages' if value isnt room.sysMes RocketChat.saveRoomSystemMessages rid, value, Meteor.user() + when 'joinCode' + RocketChat.models.Rooms.setJoinCodeById rid, String(value) when 'default' RocketChat.models.Rooms.saveDefaultById rid, value diff --git a/packages/rocketchat-custom-oauth/custom_oauth_server.coffee b/packages/rocketchat-custom-oauth/custom_oauth_server.coffee index baa17cb6a6dcb18bb21f06528c49d9b886a508b0..5bb71cb06452990e6fbc3d58b397be3dae882fc9 100644 --- a/packages/rocketchat-custom-oauth/custom_oauth_server.coffee +++ b/packages/rocketchat-custom-oauth/custom_oauth_server.coffee @@ -122,16 +122,20 @@ class CustomOAuth if identity?.CharacterID and not identity.id identity.id = identity.CharacterID - + # Fix Dataporten having 'user.userid' instead of 'id' if identity?.user?.userid and not identity.id identity.id = identity.user.userid identity.email = identity.user.email - + + # Fix general 'phid' instead of 'id' from phabricator + if identity?.phid and not identity.id + identity.id = identity.phid + # Fix general 'userid' instead of 'id' from provider if identity?.userid and not identity.id identity.id = identity.userid - + # console.log 'id:', JSON.stringify identity, null, ' ' serviceData = @@ -144,7 +148,7 @@ class CustomOAuth serviceData: serviceData options: profile: - name: identity.name or identity.username or identity.nickname or identity.CharacterName or identity.user?.name + name: identity.name or identity.username or identity.nickname or identity.CharacterName or identity.userName or identity.user?.name # console.log data diff --git a/packages/rocketchat-gitlab/common.coffee b/packages/rocketchat-gitlab/common.coffee index 7d6cd560398f99966159fbe4c053b6d661349826..42bdd4ac82dfe1e6e7792bd4595d0741e51dd9e1 100644 --- a/packages/rocketchat-gitlab/common.coffee +++ b/packages/rocketchat-gitlab/common.coffee @@ -1,6 +1,7 @@ config = serverURL: 'https://gitlab.com' identityPath: '/api/v3/user' + scope: 'api' addAutopublishFields: forLoggedInUser: ['services.gitlab'] forOtherUsers: ['services.gitlab.username'] diff --git a/packages/rocketchat-importer-hipchat/server.coffee b/packages/rocketchat-importer-hipchat/server.coffee index 3644f0bd04fa95ba048c6083cea2db711f7b633a..137f1de40b82eeb89f2c624e4bcd42b55859931f 100644 --- a/packages/rocketchat-importer-hipchat/server.coffee +++ b/packages/rocketchat-importer-hipchat/server.coffee @@ -135,7 +135,7 @@ Importer.HipChat = class Importer.HipChat extends Importer.Base Meteor.call 'setUsername', user.mention_name Meteor.call 'joinDefaultChannels', true Meteor.call 'setAvatarFromService', user.photo_url, null, 'url' - Meteor.call 'updateUserUtcOffset', parseInt moment().tz(user.timezone).format('Z').toString().split(':')[0] + Meteor.call 'userSetUtcOffset', parseInt moment().tz(user.timezone).format('Z').toString().split(':')[0] if user.name? RocketChat.models.Users.setName userId, user.name diff --git a/packages/rocketchat-importer-slack/server.coffee b/packages/rocketchat-importer-slack/server.coffee index 242533f824dfb21884c4b39487b04f64691c51ee..1dde233446e9a047b98841daa95981b2bdb11294 100644 --- a/packages/rocketchat-importer-slack/server.coffee +++ b/packages/rocketchat-importer-slack/server.coffee @@ -135,7 +135,7 @@ Importer.Slack = class Importer.Slack extends Importer.Base Meteor.call 'setAvatarFromService', url, null, 'url' # Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600 if user.tz_offset - Meteor.call 'updateUserUtcOffset', user.tz_offset / 3600 + Meteor.call 'userSetUtcOffset', user.tz_offset / 3600 RocketChat.models.Users.update { _id: userId }, { $addToSet: { importIds: user.id } } diff --git a/packages/rocketchat-ldap/server/loginHandler.js b/packages/rocketchat-ldap/server/loginHandler.js index 13342ab8ddbdf952597b7494fe56ace6880f02e5..53bc272ca872ccb98ce239784392bb743d8a5681 100644 --- a/packages/rocketchat-ldap/server/loginHandler.js +++ b/packages/rocketchat-ldap/server/loginHandler.js @@ -60,7 +60,7 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) { ldap.disconnect(); - if (ldapUser === undefined) { + if (ldapUser === undefined && RocketChat.settings.get('LDAP_Login_Fallback') === true) { return fallbackDefaultAccountSystem(self, loginRequest.username, loginRequest.ldapPass); } diff --git a/packages/rocketchat-ldap/server/settings.js b/packages/rocketchat-ldap/server/settings.js index df702f515b21be8a604a52deb5f50ab5778a8979..544983b6bf5d5d73dd8d45c9ddca3e49341a3c3d 100644 --- a/packages/rocketchat-ldap/server/settings.js +++ b/packages/rocketchat-ldap/server/settings.js @@ -19,6 +19,7 @@ Meteor.startup(function() { ]; this.add('LDAP_Enable', false, { type: 'boolean', public: true }); + this.add('LDAP_Login_Fallback', true, { type: 'boolean', enableQuery: enableQuery }); this.add('LDAP_Host', '', { type: 'string', enableQuery: enableQuery }); this.add('LDAP_Port', '389', { type: 'string', enableQuery: enableQuery }); this.add('LDAP_Encryption', 'plain', { type: 'select', values: [ { key: 'plain', i18nLabel: 'No_Encryption' }, { key: 'tls', i18nLabel: 'StartTLS' }, { key: 'ssl', i18nLabel: 'SSL/LDAPS' } ], enableQuery: enableQuery }); diff --git a/packages/rocketchat-lib/i18n/ca.i18n.json b/packages/rocketchat-lib/i18n/ca.i18n.json index 3ba9716c3630b837ffd8a3db8ff5d459acf2314e..bfd9d6c1e4eee828ba29ce2e729dbabd05660744 100644 --- a/packages/rocketchat-lib/i18n/ca.i18n.json +++ b/packages/rocketchat-lib/i18n/ca.i18n.json @@ -29,6 +29,8 @@ "Accounts_BlockedDomainsList_Description" : "Llista de dominis bloquejats separada per comes", "Accounts_BlockedUsernameList" : "Llista de noms d'usuari bloquejats", "Accounts_BlockedUsernameList_Description" : "Llista separada per comes de noms d'usuari bloquejats (no distingeix majúscules/minúscules)", + "Accounts_CustomFields" : "Traduccions personalitzades", + "Accounts_CustomFields_Description" : "Ha de ser un objecte JSON và lid on les claus són els noms dels camps i contenen un diccionari amb les opcions del camp. Exemple:</br><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ", "Accounts_denyUnverifiedEmail" : "Denegar correu electrònic sense verificar", "Accounts_EmailVerification" : "Verificació de correu electrònic", "Accounts_EmailVerification_Description" : "Assegura't que la configuració SMTP és correcta per fer servir aquesta funcionalitat", @@ -100,6 +102,7 @@ "Accounts_RegistrationForm_SecretURL" : "URL secret del fomulari de registre", "Accounts_RegistrationForm_SecretURL_Description" : "Cal proporcionar una cadena de text aleatori que s'afegirà a l'URL de registre. Exemple: https://demo.rocket.chat/register/[secret_hash]", "Accounts_RequireNameForSignUp" : "Requerir el nom per registrar-se", + "Accounts_RequirePasswordConfirmation" : "Requereix confirmació de la contrasenya", "Accounts_ShowFormLogin" : "Mostra inici de sessió basat en formulari", "Accounts_UseDefaultBlockedDomainsList" : "Utilitza la llista predeterminada de dominis bloquejats", "Accounts_UseDNSDomainCheck" : "Utilitza la comprovació DNS de dominis", @@ -276,6 +279,8 @@ "Custom_oauth_unique_name" : "Nom únic de OAuth personalitzat", "Custom_Script_Logged_In" : "Script personalitzat per als usuaris en identificar-se", "Custom_Script_Logged_Out" : "Script personalitzat per als usuaris en sortir", + "Custom_Translations" : "Traduccions personalitzades", + "Custom_Translations_Description" : "Ha de ser un objecte JSON và lid on les claus són el codi de l'idioma i contenen un diccionari de clau (key) i traduccions. Exemple:</br><code>{\n \"en\": {\n  \"key\": \"translation\"\n },\n \"ca\": {\n  \"key\": \"traducció\"\n }\n}</code> ", "Dashboard" : "Tauler", "Date" : "Data", "days" : "dies", @@ -427,6 +432,7 @@ "Features_Enabled" : "Funcionalitats habilitades", "Field" : "Camp", "Field_removed" : "Camp eliminat", + "Field_required" : "Camp obligatori", "File_exceeds_allowed_size_of_bytes" : "L'arxiu supera la mida mà xima de __size__ bytes", "FileUpload" : "Pujar arxius", "FileUpload_Enabled" : "Habilita pujar arxius", @@ -626,6 +632,8 @@ "LDAP_Encryption_Description" : "Mètode de xifrat utilitzat per a la comunicació segura cap al servidor LDAP. Alguns exemples 'sense xifrat', 'SSL / LDAPS (xifrat des de l'inici), i' StartTLS '(actualitzar a comunicacions xifrades una vegada connectat).", "LDAP_Host" : "Amfitrió (host) ", "LDAP_Host_Description" : "Amfitrió (host) LDAP, exemple: `ldap.exemple.com` o `10.0.0.30`.", + "LDAP_Import_Users" : "Importa usuaris LDAP", + "LDAP_Import_Users_Description" : "Si s'activa, el procés de sincronització importarà tots els usuaris LDAP<br/> *Compte!* Cal especificar un filtre de cerca per no importar més usuaris del compte.", "LDAP_Merge_Existing_Users" : "Uneix usuaris existents", "LDAP_Merge_Existing_Users_Description" : "*Atenció!* Quan s'importa un usuari LDAP i ja existeix un usuari amb el mateix nom d'usuari, es canviarà la informació LDAP i el password de l'usuari ja existent.", "LDAP_Port" : "Port", @@ -706,6 +714,7 @@ "Markdown_Headers" : "Encapçalaments Markdown", "Markdown_SupportSchemesForLink" : "Markdown detecta scheme:// com a enllaç", "Markdown_SupportSchemesForLink_Description" : "Llista dels scheme:// permesos separats per comes", + "Max_length_is" : "La llargada mà xima és %s", "Members_List" : "Llista de membres", "Mentions" : "Mencions", "Mentions_default" : "Mencions (per defecte)", @@ -756,6 +765,7 @@ "Meta_language" : "Idioma", "Meta_msvalidate01" : "MSValidate.01", "Meta_robots" : "Robots", + "Min_length_is" : "La llargada mÃnima és %s", "minutes" : "minuts", "Mobile_push" : "Push mòbil ", "Monitor_history_for_changes_on" : "Monitoritza l'historial per canvis a ", @@ -1307,6 +1317,7 @@ "You_have_been_muted" : "Has estat silenciat i no podrà s dir res en aquesta sala", "You_have_not_verified_your_email" : "Encara no has verificat la teva adreça de correu electrònic.", "You_have_successfully_unsubscribed" : "T'has donat de baixa correctament de la nostra llista de distribució de correu.", + "You_must_join_to_view_messages_in_this_channel" : "Has d'unir-te per veure els missatges d'aquest canal", "You_need_confirm_email" : "Cal que confirmis la teva adreça de correu-e per poder identificar-te.", "You_need_install_an_extension_to_allow_screen_sharing" : "Necessita instal·lar una extensió per poder compartir la pantalla", "You_need_to_change_your_password" : "Cal canvïs la contrasenya", diff --git a/packages/rocketchat-lib/i18n/cs.i18n.json b/packages/rocketchat-lib/i18n/cs.i18n.json index be5caed885225c64b494d15454cbf049a101a942..ac194c461ad71feca6e9d4f228fd34a6b55b8f6f 100644 --- a/packages/rocketchat-lib/i18n/cs.i18n.json +++ b/packages/rocketchat-lib/i18n/cs.i18n.json @@ -29,6 +29,8 @@ "Accounts_BlockedDomainsList_Description" : "Čárkami oddÄ›lený seznam blokovaných domén", "Accounts_BlockedUsernameList" : "Zakázaná uživatelská jména", "Accounts_BlockedUsernameList_Description" : "Äárkou oddÄ›lený seznam uživatelských jmen (na velikosti pÃsmen nezáležÃ)", + "Accounts_CustomFields" : "Vlastnà pÅ™eklad", + "Accounts_CustomFields_Description" : "Validnà JSON obsahujÃcà klÃÄe polà s nastavenÃm. NapÅ™Ãklad:<br/> <code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code>", "Accounts_denyUnverifiedEmail" : "Zakázat neověřené e-mailové adresy", "Accounts_EmailVerification" : "Ověřenà e-mailu", "Accounts_EmailVerification_Description" : "Pro použità této funkce se ujistÄ›te, že máte správné nastavenà SMTP", @@ -100,6 +102,7 @@ "Accounts_RegistrationForm_SecretURL" : "Tajná URL registrace", "Accounts_RegistrationForm_SecretURL_Description" : "Vložte náhodný retÄ›zec, který bude pÅ™idán do vaÅ¡Ã registraÄnà URL. PÅ™Ãklad: https://demo.rocket.chat/register/[tajny_kod]", "Accounts_RequireNameForSignUp" : "Vyžadovat jméno", + "Accounts_RequirePasswordConfirmation" : "Vyžadovat potvrzenà hesla", "Accounts_ShowFormLogin" : "Zobrazit formulářové pÅ™ihlášenÃ", "Accounts_UseDefaultBlockedDomainsList" : "PoužÃt výchozà seznam blokovaných domén", "Accounts_UseDNSDomainCheck" : "PoužÃt ověřenà DNS domény", @@ -170,6 +173,7 @@ "Are_you_sure_you_want_to_delete_your_account" : "Jste si jisti, že chcete smazat svůj úÄet?", "Assign_admin" : "PÅ™iÅ™adit administrátora", "at" : "v", + "Attachment_File_Uploaded" : "Soubor nahrán", "Auth_Token" : "Auth Token", "Author" : "Autor", "Authorization_URL" : "URL autorizace", @@ -203,6 +207,9 @@ "Back_to_permissions" : "ZpÄ›t na práva", "Body" : "Obsah", "bold" : "tuÄný", + "bot_request" : "Request bota", + "BotHelpers_userFields" : "Uživatelská pole", + "BotHelpers_userFields_Description" : "CSV uživatelských polÃ, která budou pÅ™Ãstupná botům", "Branch" : "VÄ›tev", "busy" : "zaneprázdnÄ›ný", "Busy" : "ZaneprázdnÄ›ný", @@ -272,6 +279,8 @@ "Custom_oauth_unique_name" : "Název vlastnà OAuth", "Custom_Script_Logged_In" : "Vlastnà skripty pro pÅ™ihlášené uživatele", "Custom_Script_Logged_Out" : "Vlastnà skripty pro odhlášené uživatele", + "Custom_Translations" : "Vlastnà pÅ™eklady", + "Custom_Translations_Description" : "Validnà JSON obsahujÃcà slovnÃk. NapÅ™Ãklad <br/><code>{\n \"en\": {\n  \"key\": \"translation\"\n },\n \"pt\": {\n  \"key\": \"tradução\"\n }\n}</code>\n", "Dashboard" : "Hlavnà panel", "Date" : "Datum", "days" : "dny", @@ -423,6 +432,7 @@ "Features_Enabled" : "Funkce Povoleny", "Field" : "Pole", "Field_removed" : "Pole odebráno", + "Field_required" : "Pole vyžadováno", "File_exceeds_allowed_size_of_bytes" : "Soubor pÅ™ekraÄuje povolenou velikost __size__ bajtů", "FileUpload" : "Nahránà souboru", "FileUpload_Enabled" : "Nahrávánà souborů povoleno", @@ -491,6 +501,14 @@ "How_satisfied_were_you_with_this_chat" : "Jak jste byli celkovÄ› spokojeni?", "If_you_are_sure_type_in_your_password" : "Pokud si jste jisti, zadejte své heslo:", "If_you_are_sure_type_in_your_username" : "Pokud jste si jisti, zadejte své uživatelské jméno:", + "Iframe_Integration_receive_enable" : "Povolit pÅ™Ãjem", + "Iframe_Integration_receive_enable_Description" : "Povolit původnÃmu oknu odesÃlat požadavky na Rocket.Chat", + "Iframe_Integration_receive_origin" : "Povolené domény", + "Iframe_Integration_receive_origin_Description" : "Jen stránky z povolených domén můžou odesÃlat požadavky. `*` pro povolenà vÅ¡ech domén. Lze vložit vÃce hodnot oddÄ›lených `,`. NapÅ™Ãklad `http://localhost,https://localhost`", + "Iframe_Integration_send_enable" : "Povolit odeslánÃ", + "Iframe_Integration_send_enable_Description" : "OdesÃlat události do původnÃho okna", + "Iframe_Integration_send_target_origin" : "OdesÃlat cÃlovou doménu", + "Iframe_Integration_send_target_origin_Description" : "Jen stránky z povolených domén můžou Äekat na události. `*` pro povolenà vÅ¡ech domén. Lze vložit vÃce hodnot oddÄ›lených `,`. NapÅ™Ãklad `http://localhost,https://localhost`", "Importer_Archived" : "Archivováno", "Importer_done" : "Import dokonÄen!", "Importer_finishing" : "DokonÄenà importu.", @@ -614,6 +632,8 @@ "LDAP_Encryption_Description" : "Metoda Å¡ifrovánà použÃvaná k zabezpeÄené komunikace se serverem LDAP. Jako pÅ™Ãklady lze uvést `plain` (bez Å¡ifrovánÃ),` SSL/LDAPS` (Å¡ifrovaný od zaÄátku), a `StartTLS` (Å ifrovaná komunikaci až po pÅ™ipojenÃ).", "LDAP_Host" : "Hostitel", "LDAP_Host_Description" : "Hostitel LDAP, napÅ™Ãklad `ldap.example.com` nebo 10.0.0.30`.", + "LDAP_Import_Users" : "Importovat LDAP uživatele", + "LDAP_Import_Users_Description" : "Pokud povoleno, vÅ¡ichni LDAP uživatelé budou naimportováni<br /> *POZOR* Nastavte filtr pokud chcete omezit poÄet uživatelů.", "LDAP_Merge_Existing_Users" : "SlouÄit existujÃcà uživatele", "LDAP_Merge_Existing_Users_Description" : "*UpozornÄ›nÃ!* Pokud importujete uživatele z LDAP a uživatel se stejným jménem již existuje, LDAP údaje a heslo budou pÅ™iÅ™azeny tomuto uživateli", "LDAP_Port" : "Port", @@ -694,6 +714,7 @@ "Markdown_Headers" : "Markdownu nadpisy", "Markdown_SupportSchemesForLink" : "Schémata použÃvaná pro automatické odkazy markdown", "Markdown_SupportSchemesForLink_Description" : "Čárkami oddÄ›lený seznam povolených schémat", + "Max_length_is" : "Maximálnà délka je %s", "Members_List" : "Seznam Älenů", "Mentions" : "ZmÃnky", "Mentions_default" : "ZmÃnky (výchozÃ)", @@ -744,6 +765,7 @@ "Meta_language" : "Jazyk", "Meta_msvalidate01" : "MSValidate.01", "Meta_robots" : "Roboti", + "Min_length_is" : "Minimálnà délka je %s", "minutes" : "minuty", "Mobile_push" : "Mobilnà notifikace", "Monitor_history_for_changes_on" : "Sledovat historii na zmÄ›ny:", @@ -1018,6 +1040,7 @@ "Settings" : "NastavenÃ", "Settings_updated" : "Nastavenà aktualizováno", "Share_Location_Title" : "SdÃlet polohu", + "Shared_Location" : "SdÃlená lokalita", "Should_be_a_URL_of_an_image" : "MÄ›la by být URL obrázku.", "Should_exists_a_user_with_this_username" : "Uživatel musà již existovat.", "Show_all" : "Ukázat vÅ¡e", @@ -1138,6 +1161,7 @@ "theme-color-tertiary-font-color" : "Terciárnà Barva pÃsma", "theme-color-unread-notification-color" : "Barva NepÅ™eÄtených upozornÄ›nÃ", "theme-custom-css" : "Vlastnà CSS", + "theme-font-body-font-family" : "Font obsahu", "There_are_no_agents_added_to_this_department_yet" : "Neexistujà žádnà operátoÅ™i v tomto oddÄ›lenÃ", "There_are_no_integrations" : "Neexistujà žádná integrace", "There_are_no_users_in_this_role" : "Pro tuto roli nejsou pÅ™iÅ™azenà žádnà uživatelé", @@ -1293,6 +1317,7 @@ "You_have_been_muted" : "Byli jste ztiÅ¡eni a nemůžete v této mÃstnosti mluvit", "You_have_not_verified_your_email" : "Neověřili jste svůj e-mail.", "You_have_successfully_unsubscribed" : "ÚspěšnÄ› jste se odhlásili z naÅ¡eho seznamu.", + "You_must_join_to_view_messages_in_this_channel" : "Pro zobrazenà zpráv v této mÃstnosti je tÅ™eba se pÅ™ipojit", "You_need_confirm_email" : "Pro pÅ™ihlášenà nejprve potvrÄte svůj e-mail!", "You_need_install_an_extension_to_allow_screen_sharing" : "MusÃte nainstalovat rozÅ¡ÃÅ™enà pro sdÃlenà obrazovky", "You_need_to_change_your_password" : "MusÃte zmÄ›nit své heslo", diff --git a/packages/rocketchat-lib/i18n/en.i18n.json b/packages/rocketchat-lib/i18n/en.i18n.json index 3f28d74114fe9bfa95792fa631e6c2c9244d5e8f..26a9f9083cbf14efef7af3e570b728cdc49c8189 100644 --- a/packages/rocketchat-lib/i18n/en.i18n.json +++ b/packages/rocketchat-lib/i18n/en.i18n.json @@ -146,9 +146,10 @@ "Animals_and_Nature" : "Animals & Nature", "API" : "API", "API_Analytics" : "Analytics", - "API_Embed" : "Embed", + "API_Embed" : "Embed Link Previews", + "API_Embed_Description": "Whether embedded link previews are enabled or not when a user posts a link to a website.", "API_EmbedDisabledFor" : "Disable Embed for Users", - "API_EmbedDisabledFor_Description" : "Comma-separated list of usernames", + "API_EmbedDisabledFor_Description" : "Comma-separated list of usernames to disable the embedded link previews.", "API_EmbedIgnoredHosts" : "Embed Ignored Hosts", "API_EmbedIgnoredHosts_Description" : "Comma-separated list of hosts or CIDR addresses, eg. localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16", "API_EmbedSafePorts" : "Safe Ports", @@ -346,6 +347,7 @@ "Empty_title" : "Empty title", "Enable" : "Enable", "Enable_Desktop_Notifications" : "Enable Desktop Notifications", + "Office_Hours_Enabled" : "Office Hours Enabled", "Enabled" : "Enabled", "Encrypted_message" : "Encrypted message", "End_OTR" : "End OTR", @@ -424,6 +426,9 @@ "error-you-are-last-owner" : "You are the last owner. Please set new owner before leaving the room.", "Error_changing_password" : "Error changing password", "Esc_to" : "Esc to", + "every_30_minutes": "Once every 30 minutes", + "every_hour": "Once every hour", + "every_six_hours": "Once every six hours", "Example_s" : "Example: <code class=\"inline\">%s</code>", "Exclude_Botnames" : "Exclude bots", "Exclude_Botnames_Description" : "Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated.", @@ -468,6 +473,7 @@ "Forward_to_department" : "Forward to department", "Forward_to_user" : "Forward to user", "Frequently_Used" : "Frequently Used", + "Friday" : "Friday", "From" : "From", "From_Email" : "From Email", "From_email_warning" : "<b>Warning</b>: The field <b>From</b> is subject to your mail server settings.", @@ -496,6 +502,7 @@ "History" : "History", "Host" : "Host", "hours" : "hours", + "Hours" : "Hours", "How_friendly_was_the_chat_agent" : "How friendly was the chat agent?", "How_knowledgeable_was_the_chat_agent" : "How knowledgeable was the chat agent?", "How_responsive_was_the_chat_agent" : "How responsive was the chat agent?", @@ -633,10 +640,12 @@ "LDAP_Encryption_Description" : "The encryption method used to secure communications to the LDAP server. Examples include `plain` (no encryption), `SSL/LDAPS` (encrypted from the start), and `StartTLS` (upgrade to encrypted communication once connected).", "LDAP_Host" : "Host", "LDAP_Host_Description" : "The LDAP host, e.g. `ldap.example.com` or `10.0.0.30`.", - "LDAP_Merge_Existing_Users" : "Merge existing users", - "LDAP_Merge_Existing_Users_Description" : "*Caution!* When importing an user from LDAP and an user with same username already exists the LDAP info and password will be set into the existing user.", "LDAP_Import_Users" : "Import LDAP users", "LDAP_Import_Users_Description" : "It True sync process will be import all LDAP users <br/> *Caution!* Specify search filter to not import excess users.", + "LDAP_Login_Fallback" : "Login Fallback", + "LDAP_Login_Fallback_Description" : "If the login on LDAP is not successfull try to login in default/local account system. Helps when the LDAP is down for some reason.", + "LDAP_Merge_Existing_Users" : "Merge existing users", + "LDAP_Merge_Existing_Users_Description" : "*Caution!* When importing an user from LDAP and an user with same username already exists the LDAP info and password will be set into the existing user.", "LDAP_Port" : "Port", "LDAP_Port_Description" : "Port to access LDAP. eg: `389` or `636` for LDAPS", "LDAP_Reject_Unauthorized" : "Reject Unauthorized", @@ -770,6 +779,7 @@ "minutes" : "minutes", "Mobile_push" : "Mobile push", "Monitor_history_for_changes_on" : "Monitor history for changes on", + "Monday" : "Monday", "More_channels" : "More channels", "More_direct_messages" : "More direct messages", "More_groups" : "More private groups", @@ -830,6 +840,7 @@ "Off" : "Off", "Off_the_record_conversation" : "Off-the-record Conversation", "Off_the_record_conversation_is_not_available_for_your_browser_or_device" : "Off-the-record conversation is not available for your browser or device.", + "Office_Hours": "Office Hours", "Offline" : "Offline", "Offline_DM_Email" : "You have been direct messaged by __user__", "Offline_form" : "Offline form", @@ -845,6 +856,7 @@ "Oops!" : "Oops", "Open" : "Open", "Open_Livechats" : "Open Livechats", + "Open_days_of_the_week" : "Open Days of the Week", "Opened" : "Opened", "Opens_a_channel_group_or_direct_message" : "Opens a channel, group or direct message", "optional" : "optional", @@ -997,6 +1009,7 @@ "SAML_Custom_Issuer" : "Custom Issuer", "SAML_Custom_Provider" : "Custom Provider", "Sandstorm_Powerbox_Share" : "Share a Sandstorm grain", + "Saturday" : "Saturday", "Save" : "Save", "Save_changes" : "Save changes", "Save_Mobile_Bandwidth" : "Save Mobile Bandwidth", @@ -1062,9 +1075,9 @@ "Site_Url" : "Site URL", "Site_Url_Description" : "Example: https://chat.domain.com/", "Skip" : "Skip", - "SlackBridge_got_an_error_while_importing_your_messages_at_s__s_" : "SlackBridge got an error while importing your messages at %s: %s", - "SlackBridge_has_finished_importing_your_messages_at_s_" : "SlackBridge has finished importing your messages at %s", - "SlackBridge_is_importing_your_messages_at_s_" : "SlackBridge is importing your messages at %s", + "SlackBridge_error" : "SlackBridge got an error while importing your messages at %s: %s", + "SlackBridge_finish" : "SlackBridge has finished importing the messages at %s. Please reload to view all messages.", + "SlackBridge_start" : "@%s has started a SlackBridge import at `#%s`. We'll let you know when it's finished.", "Slash_Gimme_Description" : "Displays ༼ 㤠◕_â—• ༽㤠before your message", "Slash_LennyFace_Description" : "Displays ( ͡° ͜ʖ ͡°) after your message", "Slash_Shrug_Description" : "Displays ¯\\_(ツ)_/¯ after your message", @@ -1072,6 +1085,14 @@ "Slash_TableUnflip_Description" : "Displays ┬─┬ ノ( ã‚œ-゜ノ)", "Slash_Topic_Description" : "Set topic", "Slash_Topic_Params" : "Topic message", + "Smarsh_Enabled": "Smarsh Enabled", + "Smarsh_Enabled_Description": "Whether the Smarsh eml connector is enabled or not (needs 'From Email' filled in under Email -> SMTP).", + "Smarsh_Email": "Smarsh Email", + "Smarsh_Email_Description": "Smarsh Email Address to send the .eml file to.", + "Smarsh_Interval": "Smarsh Interval", + "Smarsh_Interval_Description": "The amount of time to wait before sending the chats (needs 'From Email' filled in under Email -> SMTP).", + "Smarsh_MissingEmail_Email": "Missing Email", + "Smarsh_MissingEmail_Email_Description": "The email to show for a user account when their email address is missing, generally happens with bot accounts.", "Smileys_and_People" : "Smileys & People", "SMS_Enabled" : "SMS Enabled", "SMTP" : "SMTP", @@ -1114,6 +1135,7 @@ "Submit" : "Submit", "Success" : "Success", "Success_message" : "Success message", + "Sunday" : "Sunday", "Survey" : "Survey", "Survey_instructions" : "Rate each question according to your satisfaction, 1 meaning you are completely unsatisfied and 5 meaning you are completely satisfied.", "Symbols" : "Symbols", @@ -1177,6 +1199,7 @@ "This_is_a_push_test_messsage" : "This is a push test messsage", "This_room_has_been_archived_by__username_" : "This room has been archived by __username__", "This_room_has_been_unarchived_by__username_" : "This room has been unarchived by __username__", + "Thursday" : "Thursday", "Time_in_seconds" : "Time in seconds", "Title" : "Title", "Title_bar_color" : "Title bar color", @@ -1191,6 +1214,7 @@ "Trigger_Words" : "Trigger Words", "Triggers" : "Triggers", "True" : "True", + "Tuesday" : "Tuesday", "Type" : "Type", "Type_your_email" : "Type your email", "Type_your_message" : "Type your message", @@ -1300,6 +1324,7 @@ "WebRTC_Enable_Private" : "Enable for Private Channels", "WebRTC_Servers" : "STUN/TURN Servers", "WebRTC_Servers_Description" : "A list of STUN and TURN servers separated by comma.<br/>Username, password and port are allowed in the format `username:password@stun:host:port` or `username:password@turn:host:port`.", + "Wednesday" : "Wednesday", "Welcome" : "Welcome <em>%s</em>.", "Welcome_to_the" : "Welcome to the", "Why_do_you_want_to_report_question_mark" : "Why do you want to report?", @@ -1324,6 +1349,7 @@ "You_have_been_muted" : "You have been muted and cannot speak in this room", "You_have_not_verified_your_email" : "You have not verified your email.", "You_have_successfully_unsubscribed" : "You have successfully unsubscribed from our Mailling List.", + "You_must_join_to_view_messages_in_this_channel" : "You must join to view messages in this channel", "You_need_confirm_email" : "You need to confirm your email to login!", "You_need_install_an_extension_to_allow_screen_sharing" : "You need install an extension to allow screen sharing", "You_need_to_change_your_password" : "You need to change your password", @@ -1342,4 +1368,4 @@ "Your_mail_was_sent_to_s" : "Your mail was sent to %s", "Your_password_is_wrong" : "Your password is wrong!", "Your_push_was_sent_to_s_devices" : "Your push was sent to %s devices" -} \ No newline at end of file +} diff --git a/packages/rocketchat-lib/i18n/fr.i18n.json b/packages/rocketchat-lib/i18n/fr.i18n.json index d4ce3cd5546f72c84295f78312fa08a28a5d0a27..722d059b34d017c7550da839b05f6412d066f085 100644 --- a/packages/rocketchat-lib/i18n/fr.i18n.json +++ b/packages/rocketchat-lib/i18n/fr.i18n.json @@ -29,6 +29,8 @@ "Accounts_BlockedDomainsList_Description" : "Liste de domaines bloqués, séparés par des virgules", "Accounts_BlockedUsernameList" : "Liste des noms d'utilisateurs bloqués", "Accounts_BlockedUsernameList_Description" : "Liste de noms d'utilisateurs bloqués (insensible à la casse), séparée par des virgules", + "Accounts_CustomFields" : "Traductions personnalisées", + "Accounts_CustomFields_Description" : "Devrait être un JSON valide où les clés sont les noms des champs contenant un dictionnaire de champs de paramètrage. Exemple :</br>\n<code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"eleve\",\n  \"options\": [\"enseignant\", \"eleve\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> \n", "Accounts_denyUnverifiedEmail" : "Refuser les e-mails non vérifiés", "Accounts_EmailVerification" : "Vérification de l'adresse e-mail", "Accounts_EmailVerification_Description" : "Vous devez avoir des paramètres SMTP corrects pour utiliser cette fonctionnalité", @@ -100,6 +102,7 @@ "Accounts_RegistrationForm_SecretURL" : "URL secrète du formulaire d'inscription", "Accounts_RegistrationForm_SecretURL_Description" : "Vous devez fournir une chaîne de caractères aléatoire qui sera ajoutée à votre URL d'inscription. Exemple: https://demo.rocket.chat/register/[secret_hash]", "Accounts_RequireNameForSignUp" : "Exiger un nom pour s'inscrire", + "Accounts_RequirePasswordConfirmation" : "Confirmation du mot de passe requise", "Accounts_ShowFormLogin" : "Afficher le formulaire de connexion", "Accounts_UseDefaultBlockedDomainsList" : "Utiliser la liste de domaines bloqués par défaut ", "Accounts_UseDNSDomainCheck" : "Utiliser la vérification de Domaine du DNS", @@ -170,6 +173,7 @@ "Are_you_sure_you_want_to_delete_your_account" : "Êtes-vous sûr(e) de vouloir supprimer votre compte ?", "Assign_admin" : "Attribution d'un administrateur", "at" : "à ", + "Attachment_File_Uploaded" : "Fichier envoyé", "Auth_Token" : "Jeton d'Auth", "Author" : "Auteur", "Authorization_URL" : "URL d’autorisation", @@ -275,6 +279,8 @@ "Custom_oauth_unique_name" : "Nom unique de l'OAuth personnalisé", "Custom_Script_Logged_In" : "Script personnalisé pour les utilisateurs connectés", "Custom_Script_Logged_Out" : "Script personnalisé pour les utilisateurs déconnectés", + "Custom_Translations" : "Traductions personnalisées", + "Custom_Translations_Description" : "Devrait être un JSON valide où les clefs sont des langues conenant un dictionnaire de clefs et de traductions.\nExemple:</br><code>{\n \"en\": {\n  \"key\": \"translation\"\n },\n \"fr\": {\n  \"key\": \"traduction\"\n }\n}</code> ", "Dashboard" : "Tableau de bord", "Date" : "Date", "days" : "jours", @@ -426,6 +432,7 @@ "Features_Enabled" : "Caractéristiques Enabled", "Field" : "Champ", "Field_removed" : "Champ supprimé", + "Field_required" : "Champs requis", "File_exceeds_allowed_size_of_bytes" : "Le fichier dépasse la taille maximale autorisée : __size__ octets", "FileUpload" : "Envoi de fichiers", "FileUpload_Enabled" : "Envois de fichiers activés", @@ -625,6 +632,8 @@ "LDAP_Encryption_Description" : "La méthode de chiffrement utilisée pour sécuriser les communications avec le serveur LDAP. Parmi les exemples on trouve `plain` (pas de chiffrement), `SSL/LDAPS` (chiffrement dès le début) et `StartTLS` (chiffrement une fois la connexion établie).", "LDAP_Host" : "Hôte", "LDAP_Host_Description" : "L'hôte LDAP, par exemple `ldap.exemple.com` ou `10.0.0.30`.", + "LDAP_Import_Users" : "Importer les utilisateurs LDAP", + "LDAP_Import_Users_Description" : "Si VRAI les processus de synchronisation importeront l'ensemble des utilisateurs LDAP <br/>\n\n*Attention !* Spécifiez les filtres de recherche pour ne pas importer trop d'utilisateurs.", "LDAP_Merge_Existing_Users" : "Fusionner les utilisateurs existants", "LDAP_Merge_Existing_Users_Description" : "*Attention !* Si vous importez un utilisateur depuis le LDAP et qu'un utilisateur avec le même nom existe déjà , les informations et le mot de passe du LDAP lui seront attribués.", "LDAP_Port" : "Port LDAP", @@ -705,6 +714,7 @@ "Markdown_Headers" : "Titres Markdown", "Markdown_SupportSchemesForLink" : "Schémas de Markdown supportés pour les liens", "Markdown_SupportSchemesForLink_Description" : "Liste de schémas séparés par des virgules", + "Max_length_is" : "La longueur maximale est %s", "Members_List" : "Liste des membres", "Mentions" : "Mentions", "Mentions_default" : "Mentions (défaut)", @@ -755,6 +765,7 @@ "Meta_language" : "Langue", "Meta_msvalidate01" : "MSValidate.01", "Meta_robots" : "Robots", + "Min_length_is" : "La longueur minimale est %s", "minutes" : "minutes", "Mobile_push" : "Notifications push sur mobile", "Monitor_history_for_changes_on" : "Surveiller l'historique des changements sur", @@ -1029,6 +1040,7 @@ "Settings" : "Paramètres", "Settings_updated" : "Paramètres mis à jour", "Share_Location_Title" : "Partager votre position ?", + "Shared_Location" : "Dossier partagé", "Should_be_a_URL_of_an_image" : "Doit être l'URL d'une image.", "Should_exists_a_user_with_this_username" : "L'utilisateur doit déjà exister", "Show_all" : "Afficher tout", @@ -1305,6 +1317,7 @@ "You_have_been_muted" : "Vous avez été rendu muet et ne pouvez donc pas parler dans ce salon", "You_have_not_verified_your_email" : "Vous n'avez pas vérifié votre adresse e-mail.", "You_have_successfully_unsubscribed" : "Vous êtes désabonné avec succès de notre liste de diffusion.", + "You_must_join_to_view_messages_in_this_channel" : "Vous devez rejoindre ce canal pour voir les messages", "You_need_confirm_email" : "Vous devez confirmer votre adresse email pour vous connecter !", "You_need_install_an_extension_to_allow_screen_sharing" : "Vous devez installer une extension pour permettre le partage d'écran", "You_need_to_change_your_password" : "Vous devez changer votre mot de passe.", diff --git a/packages/rocketchat-lib/i18n/it.i18n.json b/packages/rocketchat-lib/i18n/it.i18n.json index 149f284a5e23fef28045c17b1c7f1ecc183efa99..4de30e0f97e719867e34a89e4e28698f5b998aa4 100644 --- a/packages/rocketchat-lib/i18n/it.i18n.json +++ b/packages/rocketchat-lib/i18n/it.i18n.json @@ -632,6 +632,8 @@ "LDAP_Encryption_Description" : "Il metodo di cifratura utilizzato per proteggere le comunicazioni con il server LDAP. Gli esempi includono `plain` (senza crittografia),` SSL / LDAPS` (criptato dall'inizio), e `StartTLS` (passaggio ad una comunicazione criptata una volta connesso).", "LDAP_Host" : "Host", "LDAP_Host_Description" : "Host LDAP, ad esempio `ldap.example.com` o` 10.0.0.30`.", + "LDAP_Import_Users" : "Importa utenti LDAP", + "LDAP_Import_Users_Description" : "Il processo true sync importerà tutti gli utenti LDAP<br/> *Attenzione!* Specificare il filtro di ricerca per non importare gli utenti in eccesso.", "LDAP_Merge_Existing_Users" : "Unisci gli utenti esistenti", "LDAP_Merge_Existing_Users_Description" : "*Attenzione* Quando importi un utente da LDAP e un utente con lo stesso username esiste già le informazioni LDRP e la password saranno impostate sull'utente già esistente.", "LDAP_Port" : "Porta", @@ -1315,6 +1317,7 @@ "You_have_been_muted" : "Hai silenziato e non puoi parlare in questa stanza", "You_have_not_verified_your_email" : "Non hai verificato la tua email.", "You_have_successfully_unsubscribed" : "Sei stato disiscritto con successo dalla nostra Mailing List.", + "You_must_join_to_view_messages_in_this_channel" : "Devi entrare nel canale per poter vedere i messaggi contenuti", "You_need_confirm_email" : "Devi confermare la tua email per accedere!", "You_need_install_an_extension_to_allow_screen_sharing" : "Devi installare una estensione per permettere la condivisione dello schermo", "You_need_to_change_your_password" : "Devi cambiare la tua password", diff --git a/packages/rocketchat-lib/i18n/tr.i18n.json b/packages/rocketchat-lib/i18n/tr.i18n.json index 91dc32896021e5c5361d8b138c468eb7dcb96e4a..b00ab162ab7a2618ca921debb8dbe4d9c071a13d 100644 --- a/packages/rocketchat-lib/i18n/tr.i18n.json +++ b/packages/rocketchat-lib/i18n/tr.i18n.json @@ -459,6 +459,10 @@ "Force_SSL" : "kuvvet SSL", "Force_SSL_Description" : "* Dikkat! * _Force SSL_ ters proxy ile asla kullanılmamalıdır. EÄŸer ters proxy varsa, ORADA yönlendirme yapmak gerekir. Bu seçenek, ters vekil de yönlendirme yapılandırması izin vermez Heroku gibi dağıtımlar için var.", "Forgot_password" : "Åžifrenizi mi unuttunuz", + "Forward" : "Ä°let", + "Forward_chat" : "Sohbeti Ä°let", + "Forward_to_department" : "Bölüme Ä°let", + "Forward_to_user" : "Kullanıcıya Ä°let", "Frequently_Used" : "Sıklıkla kullanılan", "From" : "itibaren", "From_Email" : "E-posta Gönderen", @@ -470,10 +474,13 @@ "Global" : "global", "GoogleSiteVerification_id" : "Google Site DoÄŸrulama KimliÄŸi", "GoogleTagManager_id" : "Google Etiket Yöneticisi KimliÄŸi", + "Guest_Pool" : "Misafir Havuzu", "Has_more" : "Daha fazla var", "Hash" : "esrar", "Header" : "üstbilgi", "Hidden" : "Gizli", + "Hide_Avatars" : "Profil Resimlerini Gizle", + "Hide_flextab" : "Tıklayarak SaÄŸ Taraftaki Barı Gizle", "Hide_Group_Warning" : "EÄŸer grup \" %s\" gizlemek istediÄŸinizden emin misiniz?", "Hide_Private_Warning" : "EÄŸer \" %s\" ile tartışma gizlemek istediÄŸiniz emin misiniz?", "Hide_room" : "Odayı gizle", @@ -491,6 +498,10 @@ "How_satisfied_were_you_with_this_chat" : "Bu sohbet ile ne kadar memnun kaldınız?", "If_you_are_sure_type_in_your_password" : "Åžifrenizin emin türü ise:", "If_you_are_sure_type_in_your_username" : "EÄŸer kullanıcı adınızı emin türü ise:", + "Iframe_Integration_receive_enable" : "Almayı EtkinleÅŸtir", + "Iframe_Integration_receive_enable_Description" : "Ata Pencere'nin Rocket Chat'e Komutlar Göndermesine Ä°zin Ver", + "Iframe_Integration_send_enable" : "Göndermeyi EtkinleÅŸtir", + "Iframe_Integration_send_enable_Description" : "Olayları Ata Pencere'ye Gönder", "Importer_Archived" : "arÅŸivlenen", "Importer_done" : "Alma tamamlandığı!", "Importer_finishing" : "ithalat kadar bitirme.", @@ -508,6 +519,7 @@ "Importer_Prepare_Uncheck_Deleted_Users" : "IÅŸaretini kaldırın SilinmiÅŸ Kullanıcıları", "Importer_progress_error" : "ithalat için ilerleme alınamadı.", "Importer_setup_error" : "ileti kurarken bir hata oluÅŸtu.", + "Incoming_Livechats" : "Gelen Canlı Sohbetler", "inline_code" : "kod_satırı", "Install_Extension" : "Uzantısı yükleyin", "Install_FxOs" : "Firefox üzerinde Rocket.Chat yükleyin", @@ -613,6 +625,8 @@ "LDAP_Encryption_Description" : "LDAP sunucusuna iletiÅŸim güvenliÄŸini saÄŸlamak için kullanılan ÅŸifreleme yöntemi. Örnekler `plain` (hiçbir ÅŸifreleme), (baÅŸtan ÅŸifreli)` SSL / LDAPS` ve `StartTLS` (ÅŸifreli iletiÅŸim için bir kez baÄŸlı yükseltme) içerir.", "LDAP_Host" : "evsahibi", "LDAP_Host_Description" : "LDAP ana örneÄŸin 'ldap.example.com` ya da' 10.0.0.30`.", + "LDAP_Import_Users" : "LDAP Kullanıcılarını İçe Aktar", + "LDAP_Merge_Existing_Users" : "Varolan Kullanıcıları BirleÅŸtir", "LDAP_Port" : "LDAP Port", "LDAP_Port_Description" : "Liman LDAP eriÅŸmek için. LDAPS için `389` ya da '636`: örneÄŸin", "LDAP_Reject_Unauthorized" : "Yetkisiz Reddet", diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 8b7ca207f3ff0bb4e0fc8bf308741aa65dd0cce6..143145d5b550be836195148db4f88ed099f5c39b 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -32,6 +32,7 @@ Package.onUse(function(api) { api.use('rocketchat:streamer'); api.use('rocketchat:version'); api.use('rocketchat:logger'); + api.use('rocketchat:custom-oauth'); api.use('templating', 'client'); api.use('kadira:flow-router'); @@ -56,13 +57,22 @@ Package.onUse(function(api) { api.addFiles('server/lib/RateLimiter.coffee', 'server'); // SERVER FUNCTIONS + api.addFiles('server/functions/addUserToDefaultChannels.js', 'server'); + api.addFiles('server/functions/addUserToRoom.js', 'server'); + api.addFiles('server/functions/archiveRoom.js', 'server'); api.addFiles('server/functions/checkUsernameAvailability.coffee', 'server'); api.addFiles('server/functions/checkEmailAvailability.js', 'server'); + api.addFiles('server/functions/createRoom.js', 'server'); + api.addFiles('server/functions/deleteMessage.js', 'server'); api.addFiles('server/functions/deleteUser.js', 'server'); + api.addFiles('server/functions/removeUserFromRoom.js', 'server'); api.addFiles('server/functions/sendMessage.coffee', 'server'); api.addFiles('server/functions/settings.coffee', 'server'); + api.addFiles('server/functions/setUserAvatar.js', 'server'); api.addFiles('server/functions/setUsername.coffee', 'server'); api.addFiles('server/functions/setEmail.js', 'server'); + api.addFiles('server/functions/unarchiveRoom.js', 'server'); + api.addFiles('server/functions/updateMessage.js', 'server'); api.addFiles('server/functions/Notifications.coffee', 'server'); // SERVER LIB @@ -88,14 +98,21 @@ Package.onUse(function(api) { // SERVER METHODS api.addFiles('server/methods/addOAuthService.coffee', 'server'); + api.addFiles('server/methods/addUserToRoom.coffee', 'server'); + api.addFiles('server/methods/archiveRoom.coffee', 'server'); api.addFiles('server/methods/checkRegistrationSecretURL.coffee', 'server'); + api.addFiles('server/methods/createChannel.coffee', 'server'); + api.addFiles('server/methods/createPrivateGroup.coffee', 'server'); + api.addFiles('server/methods/deleteMessage.coffee', 'server'); api.addFiles('server/methods/deleteUserOwnAccount.js', 'server'); api.addFiles('server/methods/getRoomRoles.js', 'server'); api.addFiles('server/methods/getUserRoles.js', 'server'); + api.addFiles('server/methods/joinRoom.coffee', 'server'); api.addFiles('server/methods/joinDefaultChannels.coffee', 'server'); + api.addFiles('server/methods/leaveRoom.coffee', 'server'); api.addFiles('server/methods/removeOAuthService.coffee', 'server'); api.addFiles('server/methods/robotMethods.coffee', 'server'); - api.addFiles('server/methods/saveSetting.coffee', 'server'); + api.addFiles('server/methods/saveSetting.js', 'server'); api.addFiles('server/methods/sendInvitationEmail.coffee', 'server'); api.addFiles('server/methods/sendMessage.coffee', 'server'); api.addFiles('server/methods/sendSMTPTestEmail.coffee', 'server'); @@ -105,6 +122,8 @@ Package.onUse(function(api) { api.addFiles('server/methods/insertOrUpdateUser.coffee', 'server'); api.addFiles('server/methods/setEmail.js', 'server'); api.addFiles('server/methods/restartServer.coffee', 'server'); + api.addFiles('server/methods/unarchiveRoom.coffee', 'server'); + api.addFiles('server/methods/updateMessage.coffee', 'server'); api.addFiles('server/methods/filterBadWords.js', ['server']); api.addFiles('server/methods/filterATAllTag.js', 'server'); diff --git a/packages/rocketchat-lib/rocketchat.info b/packages/rocketchat-lib/rocketchat.info index e79a5d15db7cc71a2c9bf3a55b42a67644b2fa3f..b9e103377944f144fe3d53581a154762306cb61b 100644 --- a/packages/rocketchat-lib/rocketchat.info +++ b/packages/rocketchat-lib/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "0.37.1" + "version": "0.38.0" } diff --git a/packages/rocketchat-lib/server/functions/addUserToDefaultChannels.js b/packages/rocketchat-lib/server/functions/addUserToDefaultChannels.js new file mode 100644 index 0000000000000000000000000000000000000000..a520ff941f59a406b45d3402ba046efbbbc7449c --- /dev/null +++ b/packages/rocketchat-lib/server/functions/addUserToDefaultChannels.js @@ -0,0 +1,25 @@ +RocketChat.addUserToDefaultChannels = function(user, silenced) { + RocketChat.callbacks.run('beforeJoinDefaultChannels', user); + let defaultRooms = RocketChat.models.Rooms.findByDefaultAndTypes(true, ['c', 'p'], {fields: {usernames: 0}}).fetch(); + defaultRooms.forEach((room) => { + + // put user in default rooms + RocketChat.models.Rooms.addUsernameById(room._id, user.username); + + if (!RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(room._id, user._id)) { + + // Add a subscription to this user + RocketChat.models.Subscriptions.createWithRoomAndUser(room, user, { + ts: new Date(), + open: true, + alert: true, + unread: 1 + }); + + // Insert user joined message + if (!silenced) { + RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(room._id, user); + } + } + }); +}; diff --git a/packages/rocketchat-lib/server/functions/addUserToRoom.js b/packages/rocketchat-lib/server/functions/addUserToRoom.js new file mode 100644 index 0000000000000000000000000000000000000000..f5f9424c2e04703095880089bcffb3bd9038a388 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/addUserToRoom.js @@ -0,0 +1,45 @@ +RocketChat.addUserToRoom = function(rid, user, inviter, silenced) { + let now = new Date(); + let room = RocketChat.models.Rooms.findOneById(rid); + + // Check if user is already in room + let subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(rid, user._id); + if (subscription) { + return; + } + + if (room.t === 'c') { + RocketChat.callbacks.run('beforeJoinRoom', user, room); + } + + var muted = room.ro && !RocketChat.authz.hasPermission(user._id, 'post-read-only'); + RocketChat.models.Rooms.addUsernameById(rid, user.username, muted); + RocketChat.models.Subscriptions.createWithRoomAndUser(room, user, { + ts: now, + open: true, + alert: true, + unread: 1 + }); + + if (!silenced) { + if (inviter) { + RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(rid, user, { + ts: now, + u: { + _id: inviter._id, + username: inviter.username + } + }); + } else { + RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(rid, user, { ts: now }); + } + } + + if (room.t === 'c') { + Meteor.defer(function() { + RocketChat.callbacks.run('afterJoinRoom', user, room); + }); + } + + return true; +}; diff --git a/packages/rocketchat-lib/server/functions/archiveRoom.js b/packages/rocketchat-lib/server/functions/archiveRoom.js new file mode 100644 index 0000000000000000000000000000000000000000..ef2aafeffe47aea1e2b839be8c6c011a4332ab35 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/archiveRoom.js @@ -0,0 +1,4 @@ +RocketChat.archiveRoom = function(rid) { + RocketChat.models.Rooms.archiveById(rid); + RocketChat.models.Subscriptions.archiveByRoomId(rid); +}; diff --git a/packages/rocketchat-lib/server/functions/createPrivateGroup.js b/packages/rocketchat-lib/server/functions/createPrivateGroup.js new file mode 100644 index 0000000000000000000000000000000000000000..3038bac2315fcdd9f484fb2ec146217670dce505 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/createPrivateGroup.js @@ -0,0 +1,65 @@ +/* globals RocketChat */ +RocketChat.createPrivateGroup = function(name, owner, members) { + name = s.trim(name); + owner = s.trim(owner); + members = [].concat(members); + + if (!name) { + throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createPrivateGroup' }); + } + + if (!owner) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createPrivateGroup' }); + } + + let nameValidation; + try { + nameValidation = new RegExp('^' + RocketChat.settings.get('UTF8_Names_Validation') + '$'); + } catch (error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + + if (!nameValidation.test(name)) { + throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createPrivateGroup' }); + } + + let now = new Date(); + if (!_.contains(members, owner)) { + members.push(owner); + } + + // avoid duplicate names + let room = RocketChat.models.Rooms.findOneByName(name); + if (room) { + if (room.archived) { + throw new Meteor.Error('error-archived-duplicate-name', 'There\'s an archived channel with name ' + name, { function: 'RocketChat.createPrivateGroup', room_name: name }); + } else { + throw new Meteor.Error('error-duplicate-channel-name', 'A channel with name \'' + name + '\' exists', { function: 'RocketChat.createPrivateGroup', room_name: name }); + } + } + + room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames('p', name, owner, members, { ts: now }); + + for (let username of members) { + let member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }}); + if (!member) { + continue; + } + + let extra = { open: true }; + + if (username === owner) { + extra.ls = now; + } + + RocketChat.models.Subscriptions.createWithRoomAndUser(room, member, extra); + } + + // set owner + owner = RocketChat.models.Users.findOneByUsername(owner, { fields: { username: 1 }}); + RocketChat.authz.addUserRoles(owner._id, ['owner'], room._id); + + return { + rid: room._id + }; +}; diff --git a/packages/rocketchat-lib/server/functions/createRoom.js b/packages/rocketchat-lib/server/functions/createRoom.js new file mode 100644 index 0000000000000000000000000000000000000000..e0eab0d8744e54db9171f789f59647696c94b801 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/createRoom.js @@ -0,0 +1,95 @@ +/* globals RocketChat */ +RocketChat.createRoom = function(type, name, owner, members, readOnly) { + name = s.trim(name); + owner = s.trim(owner); + members = [].concat(members); + + if (!name) { + throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createRoom' }); + } + + owner = RocketChat.models.Users.findOneByUsername(owner, { fields: { username: 1 }}); + if (!owner) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createRoom' }); + } + + let nameValidation; + try { + nameValidation = new RegExp('^' + RocketChat.settings.get('UTF8_Names_Validation') + '$'); + } catch (error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + + if (!nameValidation.test(name)) { + throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createRoom' }); + } + + let now = new Date(); + if (!_.contains(members, owner.username)) { + members.push(owner.username); + } + + // avoid duplicate names + let room = RocketChat.models.Rooms.findOneByName(name); + if (room) { + if (room.archived) { + throw new Meteor.Error('error-archived-duplicate-name', 'There\'s an archived channel with name ' + name, { function: 'RocketChat.createRoom', room_name: name }); + } else { + throw new Meteor.Error('error-duplicate-channel-name', 'A channel with name \'' + name + '\' exists', { function: 'RocketChat.createRoom', room_name: name }); + } + } + + if (type === 'c') { + RocketChat.callbacks.run('beforeCreateChannel', owner, { + t: 'c', + name: name, + ts: now, + ro: readOnly === true, + sysMes: readOnly !== true, + usernames: members, + u: { + _id: owner._id, + username: owner.username + } + }); + } + + room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames(type, name, owner.username, members, { + ts: now, + ro: readOnly === true, + sysMes: readOnly !== true, + }); + + for (let username of members) { + let member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }}); + if (!member) { + continue; + } + + // make all room members muted by default, unless they have the post-read-only permission + if (readOnly === true && !RocketChat.authz.hasPermission(member._id, 'post-read-only')) + { + RocketChat.models.Rooms.muteUsernameByRoomId(room._id, username); + } + + let extra = { open: true }; + + if (username === owner.username) { + extra.ls = now; + } + + RocketChat.models.Subscriptions.createWithRoomAndUser(room, member, extra); + } + + RocketChat.authz.addUserRoles(owner._id, ['owner'], room._id); + + if (type === 'c') { + Meteor.defer(() => { + RocketChat.callbacks.run('afterCreateChannel', owner, room); + }); + } + + return { + rid: room._id + }; +}; diff --git a/packages/rocketchat-lib/server/functions/deleteMessage.js b/packages/rocketchat-lib/server/functions/deleteMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..54ac4dd917589bf9965905af321a6570f103ee27 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/deleteMessage.js @@ -0,0 +1,31 @@ +/* globals FileUpload */ +RocketChat.deleteMessage = function(message, user) { + let keepHistory = RocketChat.settings.get('Message_KeepHistory'); + let showDeletedStatus = RocketChat.settings.get('Message_ShowDeletedStatus'); + + if (keepHistory) { + if (showDeletedStatus) { + RocketChat.models.Messages.cloneAndSaveAsHistoryById(message._id); + } else { + RocketChat.models.Messages.setHiddenById(message._id, true); + } + + if (message.file && message.file._id) { + RocketChat.models.Uploads.update(message.file._id, { $set: { _hidden: true } }); + } + } else { + if (!showDeletedStatus) { + RocketChat.models.Messages.removeById(message._id); + } + + if (message.file && message.file._id) { + FileUpload.delete(message.file._id); + } + } + + if (showDeletedStatus) { + RocketChat.models.Messages.setAsDeletedByIdAndUser(message._id, user); + } else { + RocketChat.Notifications.notifyRoom(message.rid, 'deleteMessage', { _id: message._id }); + } +}; diff --git a/packages/rocketchat-lib/server/functions/removeUserFromRoom.js b/packages/rocketchat-lib/server/functions/removeUserFromRoom.js new file mode 100644 index 0000000000000000000000000000000000000000..57fa231c86bb0e6f2f3cd81f35dbc5b83fd8be1c --- /dev/null +++ b/packages/rocketchat-lib/server/functions/removeUserFromRoom.js @@ -0,0 +1,23 @@ +RocketChat.removeUserFromRoom = function(rid, user) { + let room = RocketChat.models.Rooms.findOneById(rid); + + if (room) { + RocketChat.callbacks.run('beforeLeaveRoom', user, room); + RocketChat.models.Rooms.removeUsernameById(rid, user.username); + + if (room.usernames.indexOf(user.username) !== -1) { + let removedUser = user; + RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(rid, removedUser); + } + + if (room.t === 'l') { + RocketChat.models.Messages.createCommandWithRoomIdAndUser('survey', rid, user); + } + + RocketChat.models.Subscriptions.removeByRoomIdAndUserId(rid, user._id); + + Meteor.defer(function() { + RocketChat.callbacks.run('afterLeaveRoom', user, room); + }); + } +}; diff --git a/packages/rocketchat-lib/server/functions/setUserAvatar.js b/packages/rocketchat-lib/server/functions/setUserAvatar.js new file mode 100644 index 0000000000000000000000000000000000000000..40213653b40bc0fad94abcafbb3efea49f53247a --- /dev/null +++ b/packages/rocketchat-lib/server/functions/setUserAvatar.js @@ -0,0 +1,55 @@ +RocketChat.setUserAvatar = function(user, dataURI, contentType, service) { + if (service === 'initials') { + return RocketChat.models.Users.setAvatarOrigin(user._id, service); + } + + if (service === 'url') { + let result = null; + + try { + result = HTTP.get(dataURI, { npmRequestOptions: {encoding: 'binary'} }); + } catch (error) { + console.log(`Error while handling the setting of the avatar from a url (${dataURI}) for ${user.username}:`, error); + throw new Meteor.Error('error-avatar-url-handling', `Error while handling avatar setting from a URL (${dataURI}) for ${user.username}`, { function: 'RocketChat.setUserAvatar', url: dataURI, username: user.username }); + } + + if (result.statusCode !== 200) { + console.log(`Not a valid response, ${result.statusCode}, from the avatar url: ${dataURI}`); + throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'RocketChat.setUserAvatar', url: dataURI }); + } + + if (!/image\/.+/.test(result.headers['content-type'])) { + console.log(`Not a valid content-type from the provided url, ${result.headers['content-type']}, from the avatar url: ${dataURI}`); + throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'RocketChat.setUserAvatar', url: dataURI }); + } + + let ars = RocketChatFile.bufferToStream(new Buffer(result.content, 'binary')); + RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${user.username}.jpg`)); + let aws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${user.username}.jpg`), result.headers['content-type']); + aws.on('end', Meteor.bindEnvironment(function() { + Meteor.setTimeout(function() { + console.log(`Set ${user.username}'s avatar from the url: ${dataURI}`); + RocketChat.models.Users.setAvatarOrigin(user._id, service); + RocketChat.Notifications.notifyAll('updateAvatar', { username: user.username }); + }, 500); + })); + + ars.pipe(aws); + return; + } + + let fileData = RocketChatFile.dataURIParse(dataURI); + let image = fileData.image; + contentType = fileData.contentType; + + let rs = RocketChatFile.bufferToStream(new Buffer(image, 'base64')); + RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${user.username}.jpg`)); + let ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${user.username}.jpg`), contentType); + ws.on('end', Meteor.bindEnvironment(function() { + Meteor.setTimeout(function() { + RocketChat.models.Users.setAvatarOrigin(user._id, service); + RocketChat.Notifications.notifyAll('updateAvatar', {username: user.username}); + }, 500); + })); + rs.pipe(ws); +}; diff --git a/packages/rocketchat-lib/server/functions/unarchiveRoom.js b/packages/rocketchat-lib/server/functions/unarchiveRoom.js new file mode 100644 index 0000000000000000000000000000000000000000..3884e06c4041967dbc58c07d91dbaa488258ae12 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/unarchiveRoom.js @@ -0,0 +1,4 @@ +RocketChat.unarchiveRoom = function(rid) { + RocketChat.models.Rooms.unarchiveById(rid); + RocketChat.models.Subscriptions.unarchiveByRoomId(rid); +}; diff --git a/packages/rocketchat-lib/server/functions/updateMessage.js b/packages/rocketchat-lib/server/functions/updateMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..38141a03d61cb630cddc892f895b3a2811452c4c --- /dev/null +++ b/packages/rocketchat-lib/server/functions/updateMessage.js @@ -0,0 +1,30 @@ +RocketChat.updateMessage = function(message, user) { + // If we keep history of edits, insert a new message to store history information + if (RocketChat.settings.get('Message_KeepHistory')) { + RocketChat.models.Messages.cloneAndSaveAsHistoryById(message._id); + } + + message.editedAt = new Date(); + message.editedBy = { + _id: user._id, + username: user.username + }; + + let urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g); + if (urls) { + message.urls = urls.map((url) => { return { url: url }; }); + } + + message = RocketChat.callbacks.run('beforeSaveMessage', message); + + let tempid = message._id; + delete message._id; + + RocketChat.models.Messages.update({ _id: tempid }, { $set: message }); + + let room = RocketChat.models.Rooms.findOneById(message.rid); + + Meteor.defer(function() { + RocketChat.callbacks.run('afterSaveMessage', RocketChat.models.Messages.findOneById(tempid), room); + }); +}; diff --git a/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js b/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js index e1e126f91728f5b431acbee17a9c9ea0a4ad0585..641d6b87198d14c50b4be304e872d919d0c26a98 100644 --- a/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js +++ b/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js @@ -73,7 +73,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { // Update all other subscriptions to alert their owners but witout incrementing // the unread counter, as it is only for mentions and direct messages - RocketChat.models.Subscriptions.setAlertForRoomIdExcludingUserId(message.rid, message.u._id, true); + RocketChat.models.Subscriptions.setAlertForRoomIdExcludingUserId(message.rid, message.u._id); return message; diff --git a/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js b/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js index c80b88e6f9e461db777db377814d83eeaa3c3124..8ca68065ad7a70d2f797d2201b7570612089c91f 100644 --- a/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js +++ b/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js @@ -36,17 +36,13 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { var path = Meteor.absoluteUrl(roomPath ? roomPath.replace(/^\//, '') : ''); var style = [ 'color: #fff;', - 'padding: .5em;', + 'padding: 9px 12px;', + 'border-radius: 4px;', 'background-color: #04436a;', - 'display: block;', - 'width: 10em;', - 'text-align: center;', - 'text-decoration: none;', - 'margin: auto;', - 'margin-bottom: 8px;' + 'text-decoration: none;' ].join(' '); var message = TAPi18n.__('Offline_Link_Message'); - return `<a style="${ style }" href="${ path }">${ message }</a>`; + return `<p style="text-align:center;margin-bottom:8px;"><a style="${ style }" href="${ path }">${ message }</a>`; }; var divisorMessage = '<hr style="margin: 20px auto; border: none; border-bottom: 1px solid #dddddd;">'; diff --git a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js index b5e60e337d4235ab8d7719a44ea280a221373bfb..0f931d1c4461d5bb90ffd2b50706cf2429461437 100644 --- a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js +++ b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js @@ -6,6 +6,10 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { return message; } + if (message.ts && Math.abs(moment(message.ts).diff()) > 60000) { + return message; + } + var user = RocketChat.models.Users.findOneById(message.u._id); /* diff --git a/packages/rocketchat-lib/server/methods/addOAuthService.coffee b/packages/rocketchat-lib/server/methods/addOAuthService.coffee index 19606e836c58e9c651db3d53692f51a66fea397b..b3ff589bc14a925714f18e5b3376cb6709b43267 100644 --- a/packages/rocketchat-lib/server/methods/addOAuthService.coffee +++ b/packages/rocketchat-lib/server/methods/addOAuthService.coffee @@ -1,5 +1,8 @@ Meteor.methods addOAuthService: (name) -> + + check name, String + if not Meteor.userId() throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addOAuthService' }) diff --git a/server/methods/addUserToRoom.coffee b/packages/rocketchat-lib/server/methods/addUserToRoom.coffee similarity index 60% rename from server/methods/addUserToRoom.coffee rename to packages/rocketchat-lib/server/methods/addUserToRoom.coffee index 41a086d4beaf78e8c6a01f3546b56d0aa3c274d2..47287b4d05c294f7dd0ebe636c8410a9ff178a0c 100644 --- a/server/methods/addUserToRoom.coffee +++ b/packages/rocketchat-lib/server/methods/addUserToRoom.coffee @@ -3,7 +3,6 @@ Meteor.methods if not Meteor.userId() throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'addUserToRoom' } - fromId = Meteor.userId() unless Match.test data?.rid, String throw new Meteor.Error 'error-invalid-room', 'Invalid room', { method: 'addUserToRoom' } @@ -15,37 +14,15 @@ Meteor.methods if room.usernames.indexOf(Meteor.user().username) is -1 throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'addUserToRoom' } - # if room.username isnt Meteor.user().username and room.t is 'c' + fromId = Meteor.userId() if not RocketChat.authz.hasPermission(fromId, 'add-user-to-room', room._id) throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'addUserToRoom' } if room.t is 'd' throw new Meteor.Error 'error-cant-invite-for-direct-room', 'Can\'t invite user to direct rooms', { method: 'addUserToRoom' } - # verify if user is already in room - if room.usernames.indexOf(data.username) isnt -1 - return newUser = RocketChat.models.Users.findOneByUsername data.username - - - muted = room.ro and not RocketChat.authz.hasPermission(newUser._id, 'post-read-only') - - RocketChat.models.Rooms.addUsernameById data.rid, data.username, muted - - now = new Date() - - RocketChat.models.Subscriptions.createWithRoomAndUser room, newUser, - ts: now - open: true - alert: true - unread: 1 - - fromUser = RocketChat.models.Users.findOneById fromId - RocketChat.models.Messages.createUserAddedWithRoomIdAndUser data.rid, newUser, - ts: now - u: - _id: fromUser._id - username: fromUser.username + RocketChat.addUserToRoom(data.rid, newUser, Meteor.user()); return true diff --git a/server/methods/archiveRoom.coffee b/packages/rocketchat-lib/server/methods/archiveRoom.coffee similarity index 63% rename from server/methods/archiveRoom.coffee rename to packages/rocketchat-lib/server/methods/archiveRoom.coffee index 6f8030e2df203b38e6ccf1fef92389164fb23a34..9f7d75cfce1ade85c3339d09aade1060cc48670d 100644 --- a/server/methods/archiveRoom.coffee +++ b/packages/rocketchat-lib/server/methods/archiveRoom.coffee @@ -11,11 +11,4 @@ Meteor.methods unless RocketChat.authz.hasPermission(Meteor.userId(), 'archive-room', room._id) throw new Meteor.Error 'error-not-authorized', 'Not authorized', { method: 'archiveRoom' } - RocketChat.models.Rooms.archiveById rid - - for username in room.usernames - member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }}) - if not member? - continue - - RocketChat.models.Subscriptions.archiveByRoomIdAndUserId rid, member._id + RocketChat.archiveRoom(rid) diff --git a/packages/rocketchat-lib/server/methods/checkRegistrationSecretURL.coffee b/packages/rocketchat-lib/server/methods/checkRegistrationSecretURL.coffee index 592b85470995143fba4caaf3ba22e11de7ec55f9..e2b376ebcfe127b7b796589ca336152622fb3d8d 100644 --- a/packages/rocketchat-lib/server/methods/checkRegistrationSecretURL.coffee +++ b/packages/rocketchat-lib/server/methods/checkRegistrationSecretURL.coffee @@ -1,3 +1,6 @@ Meteor.methods checkRegistrationSecretURL: (hash) -> + + check hash, String + return hash is RocketChat.settings.get 'Accounts_RegistrationForm_SecretURL' diff --git a/packages/rocketchat-lib/server/methods/createChannel.coffee b/packages/rocketchat-lib/server/methods/createChannel.coffee new file mode 100644 index 0000000000000000000000000000000000000000..3fbe6bd3174f821d3da06ddbfe02eef2aa155ea0 --- /dev/null +++ b/packages/rocketchat-lib/server/methods/createChannel.coffee @@ -0,0 +1,9 @@ +Meteor.methods + createChannel: (name, members, readOnly) -> + if not Meteor.userId() + throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'createChannel' } + + if RocketChat.authz.hasPermission(Meteor.userId(), 'create-c') isnt true + throw new Meteor.Error 'error-not-allowed', "Not allowed", { method: 'createChannel' } + + return RocketChat.createRoom('c', name, Meteor.user()?.username, members, readOnly); diff --git a/packages/rocketchat-lib/server/methods/createPrivateGroup.coffee b/packages/rocketchat-lib/server/methods/createPrivateGroup.coffee new file mode 100644 index 0000000000000000000000000000000000000000..e90eec5033381b61a7942bc41021674d4fe92684 --- /dev/null +++ b/packages/rocketchat-lib/server/methods/createPrivateGroup.coffee @@ -0,0 +1,9 @@ +Meteor.methods + createPrivateGroup: (name, members) -> + if not Meteor.userId() + throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'createPrivateGroup' } + + unless RocketChat.authz.hasPermission(Meteor.userId(), 'create-p') + throw new Meteor.Error 'error-not-allowed', "Not allowed", { method: 'createPrivateGroup' } + + return RocketChat.createRoom('p', name, Meteor.user()?.username, members); diff --git a/server/methods/deleteMessage.coffee b/packages/rocketchat-lib/server/methods/deleteMessage.coffee similarity index 60% rename from server/methods/deleteMessage.coffee rename to packages/rocketchat-lib/server/methods/deleteMessage.coffee index ac87740869e153dfd5e4eb3f2e070a7cf3ea683d..fa7ddfe58e1e7e9bce8f756e8ea5c90e5ab54392 100644 --- a/server/methods/deleteMessage.coffee +++ b/packages/rocketchat-lib/server/methods/deleteMessage.coffee @@ -22,27 +22,4 @@ Meteor.methods if currentTsDiff > blockDeleteInMinutes throw new Meteor.Error 'error-message-deleting-blocked', 'Message deleting is blocked', { method: 'deleteMessage' } - - keepHistory = RocketChat.settings.get 'Message_KeepHistory' - showDeletedStatus = RocketChat.settings.get 'Message_ShowDeletedStatus' - - if keepHistory - if showDeletedStatus - RocketChat.models.Messages.cloneAndSaveAsHistoryById originalMessage._id - else - RocketChat.models.Messages.setHiddenById originalMessage._id, true - - if originalMessage.file?._id? - RocketChat.models.Uploads.update originalMessage.file._id, {$set: {_hidden: true}} - - else - if not showDeletedStatus - RocketChat.models.Messages.removeById originalMessage._id - - if originalMessage.file?._id? - FileUpload.delete(originalMessage.file._id) - - if showDeletedStatus - RocketChat.models.Messages.setAsDeletedById originalMessage._id - else - RocketChat.Notifications.notifyRoom originalMessage.rid, 'deleteMessage', {_id: originalMessage._id} + RocketChat.deleteMessage(originalMessage, Meteor.user()); diff --git a/packages/rocketchat-lib/server/methods/deleteUserOwnAccount.js b/packages/rocketchat-lib/server/methods/deleteUserOwnAccount.js index ee4d15239cfd572124e85b09f8b55590e771d755..ed7f361c4d3b3192bfe53ed59e80fcd23a9ed8f4 100644 --- a/packages/rocketchat-lib/server/methods/deleteUserOwnAccount.js +++ b/packages/rocketchat-lib/server/methods/deleteUserOwnAccount.js @@ -1,5 +1,8 @@ Meteor.methods({ deleteUserOwnAccount: function(password) { + + check(password, String); + if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'deleteUserOwnAccount' }); } diff --git a/packages/rocketchat-lib/server/methods/filterATAllTag.js b/packages/rocketchat-lib/server/methods/filterATAllTag.js index 38e4f05aea12b1ea533fddd5dc0e9769ad681e45..4691004b5f3b9432d9ff08421cd24a517b59c509 100644 --- a/packages/rocketchat-lib/server/methods/filterATAllTag.js +++ b/packages/rocketchat-lib/server/methods/filterATAllTag.js @@ -1,5 +1,4 @@ RocketChat.callbacks.add('beforeSaveMessage', function(message) { - // Test if the message mentions include @all. if (message.mentions != null && _.pluck(message.mentions, '_id').some((item) => item === 'all')) { diff --git a/packages/rocketchat-lib/server/methods/getRoomRoles.js b/packages/rocketchat-lib/server/methods/getRoomRoles.js index 75e953a890552249516b19f684b14b248ddefe4a..18ccaed6dbefd517b1015ddff0bff699905cca2c 100644 --- a/packages/rocketchat-lib/server/methods/getRoomRoles.js +++ b/packages/rocketchat-lib/server/methods/getRoomRoles.js @@ -1,5 +1,8 @@ Meteor.methods({ getRoomRoles(rid) { + + check(rid, String); + if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getRoomRoles' }); } diff --git a/packages/rocketchat-lib/server/methods/getUserRoles.js b/packages/rocketchat-lib/server/methods/getUserRoles.js index cff85925e8768e7e000972ffeb3df649bcb6176b..f51e7c9e75885e6c693d5877dec88f7d150687df 100644 --- a/packages/rocketchat-lib/server/methods/getUserRoles.js +++ b/packages/rocketchat-lib/server/methods/getUserRoles.js @@ -1,5 +1,6 @@ Meteor.methods({ getUserRoles() { + if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getUserRoles' }); } diff --git a/packages/rocketchat-lib/server/methods/insertOrUpdateUser.coffee b/packages/rocketchat-lib/server/methods/insertOrUpdateUser.coffee index 2cb02948589ab9b7580b0eca451f1d2be6797cb4..221c2f3c824ddb9f495bdf3d7556624d37c8704d 100644 --- a/packages/rocketchat-lib/server/methods/insertOrUpdateUser.coffee +++ b/packages/rocketchat-lib/server/methods/insertOrUpdateUser.coffee @@ -1,5 +1,8 @@ Meteor.methods insertOrUpdateUser: (userData) -> + + check userData, Object + if not Meteor.userId() throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'insertOrUpdateUser' }) @@ -97,11 +100,6 @@ Meteor.methods return _id else - # prevent removing admin role of last admin - adminCount = Meteor.users.find({ roles: { $in: ['admin'] } }).count() - if adminCount is 1 and userData.role isnt 'admin' - throw new Meteor.Error 'error-action-not-allowed', 'Leaving the app without admins is not allowed', { method: 'insertOrUpdateUser', action: 'Remove_last_admin' } - #update user updateUser = $set: {} diff --git a/packages/rocketchat-lib/server/methods/joinDefaultChannels.coffee b/packages/rocketchat-lib/server/methods/joinDefaultChannels.coffee index 120eef213c6781af8c4e494d58c5743031f78671..fd748483a86d147fa835ad56ddeb30fd0b1a2ed0 100644 --- a/packages/rocketchat-lib/server/methods/joinDefaultChannels.coffee +++ b/packages/rocketchat-lib/server/methods/joinDefaultChannels.coffee @@ -1,30 +1,10 @@ Meteor.methods joinDefaultChannels: (silenced) -> - if not Meteor.userId() - throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'joinDefaultChannels' }) - - this.unblock() - - user = Meteor.user() - - RocketChat.callbacks.run 'beforeJoinDefaultChannels', user - defaultRooms = RocketChat.models.Rooms.findByDefaultAndTypes(true, ['c', 'p'], {fields: {usernames: 0}}).fetch() + check silenced, Match.Optional(Boolean) - defaultRooms.forEach (room) -> - - # put user in default rooms - RocketChat.models.Rooms.addUsernameById room._id, user.username - - if not RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(room._id, user._id)? - - # Add a subscription to this user - RocketChat.models.Subscriptions.createWithRoomAndUser room, user, - ts: new Date() - open: true - alert: true - unread: 1 + if not Meteor.userId() + throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'joinDefaultChannels' }) - # Insert user joined message - if not silenced - RocketChat.models.Messages.createUserJoinWithRoomIdAndUser room._id, user + this.unblock(); + RocketChat.addUserToDefaultChannels(Meteor.user(), silenced); diff --git a/packages/rocketchat-lib/server/methods/joinRoom.coffee b/packages/rocketchat-lib/server/methods/joinRoom.coffee new file mode 100644 index 0000000000000000000000000000000000000000..095e23dd22c5d6d0aef06c7755cd75211cb71220 --- /dev/null +++ b/packages/rocketchat-lib/server/methods/joinRoom.coffee @@ -0,0 +1,17 @@ +Meteor.methods + joinRoom: (rid, code) -> + if not Meteor.userId() + throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'joinRoom' } + + room = RocketChat.models.Rooms.findOneById rid + + if not room? + throw new Meteor.Error 'error-invalid-room', 'Invalid room', { method: 'joinRoom' } + + if room.t isnt 'c' or RocketChat.authz.hasPermission(Meteor.userId(), 'view-c-room') isnt true + throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'joinRoom' } + + if room.joinCodeRequired is true and code isnt room.joinCode + throw new Meteor.Error 'error-code-invalid', 'Invalid Code', { method: 'joinRoom' } + + RocketChat.addUserToRoom(rid, Meteor.user()) diff --git a/server/methods/leaveRoom.coffee b/packages/rocketchat-lib/server/methods/leaveRoom.coffee similarity index 56% rename from server/methods/leaveRoom.coffee rename to packages/rocketchat-lib/server/methods/leaveRoom.coffee index c7931f0c7587262cfeab5e4b5c150e269c660c08..f00ab1de6a55923c4384b2564b94acf872419ec7 100644 --- a/server/methods/leaveRoom.coffee +++ b/packages/rocketchat-lib/server/methods/leaveRoom.coffee @@ -15,19 +15,4 @@ Meteor.methods if numOwners is 1 throw new Meteor.Error 'error-you-are-last-owner', 'You are the last owner. Please set new owner before leaving the room.', { method: 'leaveRoom' } - RocketChat.callbacks.run 'beforeLeaveRoom', user, room - - RocketChat.models.Rooms.removeUsernameById rid, user.username - - if room.usernames.indexOf(user.username) isnt -1 - removedUser = user - RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser rid, removedUser - - if room.t is 'l' - RocketChat.models.Messages.createCommandWithRoomIdAndUser 'survey', rid, user - - RocketChat.models.Subscriptions.removeByRoomIdAndUserId rid, Meteor.userId() - - Meteor.defer -> - - RocketChat.callbacks.run 'afterLeaveRoom', user, room + RocketChat.removeUserFromRoom(rid, Meteor.user()); diff --git a/packages/rocketchat-lib/server/methods/removeOAuthService.coffee b/packages/rocketchat-lib/server/methods/removeOAuthService.coffee index f13b9abdd7dc7faf0afea6890fd9367a4b62a5a7..689609246ecfd52809a0220f3ad14755b9de0a92 100644 --- a/packages/rocketchat-lib/server/methods/removeOAuthService.coffee +++ b/packages/rocketchat-lib/server/methods/removeOAuthService.coffee @@ -1,5 +1,8 @@ Meteor.methods removeOAuthService: (name) -> + + check name, String + if not Meteor.userId() throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'removeOAuthService' }) diff --git a/packages/rocketchat-lib/server/methods/robotMethods.coffee b/packages/rocketchat-lib/server/methods/robotMethods.coffee index 22cf5ba98560ace324d1aec67ba42e58e699b310..593a915416ab4baa172ef4b0e4ec1cdc72beac74 100644 --- a/packages/rocketchat-lib/server/methods/robotMethods.coffee +++ b/packages/rocketchat-lib/server/methods/robotMethods.coffee @@ -1,5 +1,9 @@ Meteor.methods 'robot.modelCall': (model, method, args) -> + + check model, String + check method, String + unless Meteor.userId() throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'robot.modelCall' } diff --git a/packages/rocketchat-lib/server/methods/saveSetting.coffee b/packages/rocketchat-lib/server/methods/saveSetting.coffee deleted file mode 100644 index cbec9d4d2d1d1f6da91d6dd70507ad24d80bd34e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/methods/saveSetting.coffee +++ /dev/null @@ -1,11 +0,0 @@ -Meteor.methods - saveSetting: (_id, value) -> - if Meteor.userId()? - user = Meteor.users.findOne Meteor.userId() - - unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-privileged-setting') is true - throw new Meteor.Error 'error-action-not-allowed', 'Editing settings is not allowed', { method: 'saveSetting' } - - # console.log "saveSetting -> ".green, _id, value - RocketChat.settings.updateById _id, value - return true diff --git a/packages/rocketchat-lib/server/methods/saveSetting.js b/packages/rocketchat-lib/server/methods/saveSetting.js new file mode 100644 index 0000000000000000000000000000000000000000..d5bb8224eb30e441f6e12bd5f0bdefe6565d13ff --- /dev/null +++ b/packages/rocketchat-lib/server/methods/saveSetting.js @@ -0,0 +1,36 @@ +Meteor.methods({ + saveSetting: function(_id, value) { + if (Meteor.userId() === null) { + throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { + method: 'saveSetting' + }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'edit-privileged-setting')) { + throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { + method: 'saveSetting' + }); + } + + //Verify the _id passed in is a string. + check(_id, String); + + const setting = RocketChat.models.Settings.findOneById(_id); + + //Verify the value is what it should be + switch (setting.type) { + case 'boolean': + check(value, Boolean); + break; + case 'int': + check(value, Number); + break; + default: + check(value, String); + break; + } + + RocketChat.settings.updateById(_id, value); + return true; + } +}); diff --git a/packages/rocketchat-lib/server/methods/sendInvitationEmail.coffee b/packages/rocketchat-lib/server/methods/sendInvitationEmail.coffee index 9be056c3b2867f5b24870760e96da50eab8fe610..f9d8080a619258631f04dfb792b3b7ef9cd3106b 100644 --- a/packages/rocketchat-lib/server/methods/sendInvitationEmail.coffee +++ b/packages/rocketchat-lib/server/methods/sendInvitationEmail.coffee @@ -1,5 +1,8 @@ Meteor.methods sendInvitationEmail: (emails) -> + + check emails, [String] + if not Meteor.userId() throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'sendInvitationEmail' } diff --git a/packages/rocketchat-lib/server/methods/sendMessage.coffee b/packages/rocketchat-lib/server/methods/sendMessage.coffee index f9d5c56043bd834014ca8c469b2089fb87bc1aee..a2d52af246a806c71c7847d8393117a700999a86 100644 --- a/packages/rocketchat-lib/server/methods/sendMessage.coffee +++ b/packages/rocketchat-lib/server/methods/sendMessage.coffee @@ -1,6 +1,8 @@ Meteor.methods sendMessage: (message) -> + check message, Object + if message.ts tsDiff = Math.abs(moment(message.ts).diff()) if tsDiff > 60000 diff --git a/packages/rocketchat-lib/server/methods/setAdminStatus.coffee b/packages/rocketchat-lib/server/methods/setAdminStatus.coffee index 23df6a18b8a6a306ce8538f33c1ec27d0752d13d..ca1439aa92434a5375e207e5727499627f9c9840 100644 --- a/packages/rocketchat-lib/server/methods/setAdminStatus.coffee +++ b/packages/rocketchat-lib/server/methods/setAdminStatus.coffee @@ -1,5 +1,9 @@ Meteor.methods setAdminStatus: (userId, admin) -> + + check userId, String + check admin, Match.Optional(Boolean) + if not Meteor.userId() throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'setAdminStatus' } diff --git a/packages/rocketchat-lib/server/methods/setEmail.js b/packages/rocketchat-lib/server/methods/setEmail.js index c7d456b52e388af30a0608fa486f4835966aad92..e71db53808d4ea17fafe90f117924826d05d3fe7 100644 --- a/packages/rocketchat-lib/server/methods/setEmail.js +++ b/packages/rocketchat-lib/server/methods/setEmail.js @@ -1,5 +1,8 @@ Meteor.methods({ setEmail: function(email) { + + check (email, String); + if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setEmail' }); } diff --git a/packages/rocketchat-lib/server/methods/setRealName.coffee b/packages/rocketchat-lib/server/methods/setRealName.coffee index f2aa510b4fe29ff8c53e8bd3be7c122af1ed7b2f..072a1f4dfa027a243432d5bfc0235f12f1c24bdd 100644 --- a/packages/rocketchat-lib/server/methods/setRealName.coffee +++ b/packages/rocketchat-lib/server/methods/setRealName.coffee @@ -1,5 +1,8 @@ Meteor.methods setRealName: (name) -> + + check name, String + if not Meteor.userId() throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'setRealName' }) diff --git a/packages/rocketchat-lib/server/methods/setUsername.coffee b/packages/rocketchat-lib/server/methods/setUsername.coffee index fe319d1b8bf6928bf3a14d945893e2a92dd7e818..4031056c91d13b13eaa86b1babc96a1e5e2d109e 100644 --- a/packages/rocketchat-lib/server/methods/setUsername.coffee +++ b/packages/rocketchat-lib/server/methods/setUsername.coffee @@ -1,5 +1,8 @@ Meteor.methods setUsername: (username) -> + + check username, String + if not Meteor.userId() throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'setUsername' }) diff --git a/server/methods/unarchiveRoom.coffee b/packages/rocketchat-lib/server/methods/unarchiveRoom.coffee similarity index 63% rename from server/methods/unarchiveRoom.coffee rename to packages/rocketchat-lib/server/methods/unarchiveRoom.coffee index 8bf78dea7b1249300030457e9b66c71de74ff0c1..699398bf06e03229ba6cda34ca41f6c909bdfb2e 100644 --- a/server/methods/unarchiveRoom.coffee +++ b/packages/rocketchat-lib/server/methods/unarchiveRoom.coffee @@ -11,11 +11,4 @@ Meteor.methods unless RocketChat.authz.hasPermission(Meteor.userId(), 'unarchive-room', room._id) throw new Meteor.Error 'error-not-authorized', 'Not authorized', { method: 'unarchiveRoom' } - RocketChat.models.Rooms.unarchiveById rid - - for username in room.usernames - member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }}) - if not member? - continue - - RocketChat.models.Subscriptions.unarchiveByRoomIdAndUserId rid, member._id + RocketChat.unarchiveRoom(rid); diff --git a/server/methods/updateMessage.coffee b/packages/rocketchat-lib/server/methods/updateMessage.coffee similarity index 56% rename from server/methods/updateMessage.coffee rename to packages/rocketchat-lib/server/methods/updateMessage.coffee index 162dc26972de9be7233fe75c6966c87b5b3adbef..1e9d27e656965983481a2b421ba8688759dc9734 100644 --- a/server/methods/updateMessage.coffee +++ b/packages/rocketchat-lib/server/methods/updateMessage.coffee @@ -24,29 +24,4 @@ Meteor.methods if currentTsDiff > blockEditInMinutes throw new Meteor.Error 'error-message-editing-blocked', 'Message editing is blocked', { method: 'updateMessage' } - # If we keep history of edits, insert a new message to store history information - if RocketChat.settings.get 'Message_KeepHistory' - RocketChat.models.Messages.cloneAndSaveAsHistoryById originalMessage._id - - message.editedAt = new Date() - message.editedBy = - _id: Meteor.userId() - username: me.username - - if urls = message.msg.match /([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g - message.urls = urls.map (url) -> url: url - - message = RocketChat.callbacks.run 'beforeSaveMessage', message - - tempid = message._id - delete message._id - - RocketChat.models.Messages.update - _id: tempid - , - $set: message - - room = RocketChat.models.Rooms.findOneById message.rid - - Meteor.defer -> - RocketChat.callbacks.run 'afterSaveMessage', RocketChat.models.Messages.findOneById(tempid), room + RocketChat.updateMessage(message, Meteor.user()); diff --git a/packages/rocketchat-lib/server/models/Messages.coffee b/packages/rocketchat-lib/server/models/Messages.coffee index 12a26916e8a305c0406ea3126fa19fa9cd3130ba..2641b093455f85655f8b80c42f0436b610551f98 100644 --- a/packages/rocketchat-lib/server/models/Messages.coffee +++ b/packages/rocketchat-lib/server/models/Messages.coffee @@ -152,8 +152,7 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base return @update query, update - setAsDeletedById: (_id) -> - me = RocketChat.models.Users.findOneById Meteor.userId() + setAsDeletedByIdAndUser: (_id, user) -> query = _id: _id @@ -166,8 +165,8 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base attachments: [] editedAt: new Date() editedBy: - _id: Meteor.userId() - username: me.username + _id: user._id + username: user.username return @update query, update diff --git a/packages/rocketchat-lib/server/models/Rooms.coffee b/packages/rocketchat-lib/server/models/Rooms.coffee index 752e7388eab979f2cf484e0dfa1d692493a1cbe6..97c9be55d85c258c1f1039c1f47bd9b53df1237b 100644 --- a/packages/rocketchat-lib/server/models/Rooms.coffee +++ b/packages/rocketchat-lib/server/models/Rooms.coffee @@ -366,6 +366,24 @@ RocketChat.models.Rooms = new class extends RocketChat.models._Base return @update query, update, { multi: true } + setJoinCodeById: (_id, joinCode) -> + query = + _id: _id + + if joinCode?.trim() isnt '' + update = + $set: + joinCodeRequired: true + joinCode: joinCode + else + update = + $set: + joinCodeRequired: false + $unset: + joinCode: 1 + + return @update query, update + setUserById: (_id, user) -> query = _id: _id diff --git a/packages/rocketchat-lib/server/models/Subscriptions.coffee b/packages/rocketchat-lib/server/models/Subscriptions.coffee index b40095e3faf18968ab550704e61e9707373da69f..c5b30c6139d4448793af5f89d9b3881f3e898a24 100644 --- a/packages/rocketchat-lib/server/models/Subscriptions.coffee +++ b/packages/rocketchat-lib/server/models/Subscriptions.coffee @@ -87,10 +87,9 @@ RocketChat.models.Subscriptions = new class extends RocketChat.models._Base return @find query # UPDATE - archiveByRoomIdAndUserId: (roomId, userId) -> + archiveByRoomId: (roomId) -> query = rid: roomId - 'u._id': userId update = $set: @@ -100,10 +99,9 @@ RocketChat.models.Subscriptions = new class extends RocketChat.models._Base return @update query, update - unarchiveByRoomIdAndUserId: (roomId, userId) -> + unarchiveByRoomId: (roomId) -> query = rid: roomId - 'u._id': userId update = $set: @@ -262,17 +260,19 @@ RocketChat.models.Subscriptions = new class extends RocketChat.models._Base return @update query, update, { multi: true } - setAlertForRoomIdExcludingUserId: (roomId, userId, alert=true) -> + setAlertForRoomIdExcludingUserId: (roomId, userId) -> query = rid: roomId - alert: - $ne: alert 'u._id': $ne: userId + $or: [ + { alert: { $ne: true } } + { open: { $ne: true } } + ] update = $set: - alert: alert + alert: true open: true return @update query, update, { multi: true } diff --git a/packages/rocketchat-lib/server/models/Users.coffee b/packages/rocketchat-lib/server/models/Users.coffee index 9f8dc6cac6c069ffc113db6f2cf655fb9aa15df7..90b583664b28cb33fa7301819e4b953656d86b65 100644 --- a/packages/rocketchat-lib/server/models/Users.coffee +++ b/packages/rocketchat-lib/server/models/Users.coffee @@ -371,15 +371,16 @@ RocketChat.models.Users = new class extends RocketChat.models._Base - he is not online - has a verified email - has not disabled email notifications + - `active` is equal to true (false means they were deactivated and can't login) ### getUsersToSendOfflineEmail: (usersIds) -> query = _id: $in: usersIds + active: true status: 'offline' statusConnection: $ne: 'online' 'emails.verified': true return @find query, { fields: { name: 1, username: 1, emails: 1, 'settings.preferences.emailNotificationMode': 1 } } - diff --git a/packages/rocketchat-lib/server/models/_Base.js b/packages/rocketchat-lib/server/models/_Base.js index 89dad0e62f179d4731907ceed8480b9ed2eccfbf..d0f3de53f5f9d70d13a9feab0cc31dfd4c083221 100644 --- a/packages/rocketchat-lib/server/models/_Base.js +++ b/packages/rocketchat-lib/server/models/_Base.js @@ -74,10 +74,10 @@ class ModelsBase extends EventEmitter { } } - query = { _id: { $in: _.pluck(ids, '_id') } }; const result = this.model.update(query, update, options); - this.emit('update', query, update); - this.emit('change', 'update', query, update); + const idQuery = { _id: { $in: _.pluck(ids, '_id') } }; + this.emit('update', idQuery, update); + this.emit('change', 'update', idQuery, update); return result; } diff --git a/packages/rocketchat-lib/server/startup/settings.coffee b/packages/rocketchat-lib/server/startup/settings.coffee index 121c6fccd16c8f8355b0e3972b628cc5888503da..941f3c673726f085ffb54ee691f07cee6f61f737 100644 --- a/packages/rocketchat-lib/server/startup/settings.coffee +++ b/packages/rocketchat-lib/server/startup/settings.coffee @@ -196,12 +196,12 @@ RocketChat.settings.addGroup 'Meta', -> RocketChat.settings.addGroup 'Push', -> - @add 'Push_debug', false, { type: 'boolean', public: true } @add 'Push_enable', true, { type: 'boolean', public: true } - @add 'Push_enable_gateway', true, { type: 'boolean' } - @add 'Push_gateway', 'https://rocket.chat', { type: 'string' } - @add 'Push_production', true, { type: 'boolean', public: true } - @add 'Push_test_push', 'push_test', { type: 'action', actionText: 'Send_a_test_push_to_my_user' } + @add 'Push_debug', false, { type: 'boolean', public: true, enableQuery: { _id: 'Push_enable', value: true } } + @add 'Push_enable_gateway', true, { type: 'boolean', enableQuery: { _id: 'Push_enable', value: true } } + @add 'Push_gateway', 'https://gateway.rocket.chat', { type: 'string', enableQuery: [{ _id: 'Push_enable', value: true }, { _id: 'Push_enable_gateway', value: true }] } + @add 'Push_production', true, { type: 'boolean', public: true, enableQuery: [{ _id: 'Push_enable', value: true }, { _id: 'Push_enable_gateway', value: false }] } + @add 'Push_test_push', 'push_test', { type: 'action', actionText: 'Send_a_test_push_to_my_user', enableQuery: { _id: 'Push_enable', value: true } } @section 'Certificates_and_Keys', -> @add 'Push_apn_passphrase', '', { type: 'string' } diff --git a/packages/rocketchat-livechat/app/i18n/cs.i18n.json b/packages/rocketchat-livechat/app/i18n/cs.i18n.json index 9951caca7c162eb1f168e6c545d9cecd4cdcdc83..b602548fbb4deedc3e7d8292391dfc6134bf3bab 100644 --- a/packages/rocketchat-livechat/app/i18n/cs.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/cs.i18n.json @@ -33,5 +33,6 @@ "User_left" : "Uživatel odeÅ¡el", "We_are_offline_Sorry_for_the_inconvenience" : "Jsme offline. Omluváme se za nepÅ™Ãjemnosti.", "Yes" : "Ano", + "You" : "Vy", "You_must_complete_all_fields" : "Je potÅ™eba vyplnit vÅ¡echna pole" } \ No newline at end of file diff --git a/packages/rocketchat-livechat/client/collections/livechatOfficeHour.js b/packages/rocketchat-livechat/client/collections/livechatOfficeHour.js new file mode 100644 index 0000000000000000000000000000000000000000..a741186d23577420a1060471b69531e253bbe80e --- /dev/null +++ b/packages/rocketchat-livechat/client/collections/livechatOfficeHour.js @@ -0,0 +1 @@ +this.LivechatOfficeHour = new Mongo.Collection('rocketchat_livechat_office_hour'); \ No newline at end of file diff --git a/packages/rocketchat-livechat/client/route.js b/packages/rocketchat-livechat/client/route.js index 44b379b40fe2f5d1e95d01ce80edbd9fc4c7cf92..1958f4e4d44a706c085412a818623bdccebf9359 100644 --- a/packages/rocketchat-livechat/client/route.js +++ b/packages/rocketchat-livechat/client/route.js @@ -75,6 +75,14 @@ AccountBox.addRoute({ pageTemplate: 'livechatAppearance' }, livechatManagerRoutes); +AccountBox.addRoute({ + name: 'livechat-officeHours', + path: '/officeHours', + sideNav: 'livechatFlex', + i18nPageTitle: 'Office_Hours', + pageTemplate: 'livechatOfficeHours' +}, livechatManagerRoutes); + AccountBox.addRoute({ name: 'livechat-customfields', path: '/customfields', @@ -113,3 +121,5 @@ AccountBox.addRoute({ i18nPageTitle: 'Livechat_Queue', pageTemplate: 'livechatQueue' }); + + diff --git a/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.html b/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.html new file mode 100644 index 0000000000000000000000000000000000000000..d6f03caacefcc481d00c27b62164efc53def78bb --- /dev/null +++ b/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.html @@ -0,0 +1,59 @@ +<template name="livechatOfficeHours"> + <div class="livechat-officeHours-div"> + <form class="rocket-form" id="officeHoursForm"> + + <fieldset> + <legend>{{_ "Office_hours_enabled"}}</legend> + <input type="radio" class="preview-settings" name="enableOfficeHours" id="enableOfficeHoursTrue" checked="{{enableOfficeHoursTrueChecked}}" value="true"> + <label for="displayOfflineFormTrue">{{_ "True"}}</label> + <input type="radio" class="preview-settings" name="enableOfficeHours" id="enableOfficeHoursFalse" checked="{{enableOfficeHoursFalseChecked}}" value="false"> + <label for="displayOfflineFormFalse">{{_ "False"}}</label> + </fieldset> + + <!-- days open --> + <fieldset> + <legend>{{_ "Open_days_of_the_week"}}</legend> + {{#each day in days}} + {{#if open day}} + <label class="dayOpenCheck"><input type="checkbox" name={{openName day}} checked>{{name day}}</label> + {{else}} + <label class="dayOpenCheck"><input type="checkbox" name={{openName day}}>{{name day}}</label> + {{/if}} + {{/each}} + </fieldset> + + <!-- times --> + <fieldset> + <legend>{{_ "Hours"}}</legend> + {{#each day in days}} + <div class="input-line"> + <h1><strong>{{name day}}</strong></h1> + <table style="width:100%;"> + <tr> + <td>{{_ "Open"}}:</td> + <td>{{_ "Close"}}:</td> + </tr> + <tr> + <td> + <div style="margin-right:30px"> + <input type="time" class="preview-settings" name={{startName day}} id={{startName day}} value={{start day}} style="width=100px;"> + </div> + </td> + <td> + <div style="margin-right:30px"> + <input type="time" class="preview-settings" name={{finishName day}} id={{finishName day}} value={{finish day}} style="width=100px;"> + </div> + </td> + </tr> + </table> + </div> + {{/each}} + </fieldset> + + + <div class="submit"> + <button class="button"><i class="icon-floppy"></i>{{_ "Save"}}</button> + </div> + </form> + </div> +</template> \ No newline at end of file diff --git a/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.js b/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.js new file mode 100644 index 0000000000000000000000000000000000000000..298e84d4d11e06bc75a0673cd8f7023f4c740ef6 --- /dev/null +++ b/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.js @@ -0,0 +1,157 @@ +/* globals LivechatOfficeHour */ + +Template.livechatOfficeHours.helpers({ + days() { + return LivechatOfficeHour.find(); + }, + startName(day) { + return day.day + '_start'; + }, + finishName(day) { + return day.day + '_finish'; + }, + openName(day) { + return day.day + '_open'; + }, + start(day) { + return Template.instance().dayVars[day.day].start.get(); + }, + finish(day) { + return Template.instance().dayVars[day.day].finish.get(); + + }, + name(day) { + return day.day; + }, + open(day) { + return Template.instance().dayVars[day.day].open.get(); + }, + enableOfficeHoursTrueChecked() { + if (Template.instance().enableOfficeHours.get()) { + return 'checked'; + } + }, + enableOfficeHoursFalseChecked() { + if (!Template.instance().enableOfficeHours.get()) { + return 'checked'; + } + } +}); + +Template.livechatOfficeHours.events({ + 'change .preview-settings, keydown .preview-settings'(e, instance) { + var temp = e.currentTarget.name.split('_'); + + var newTime = moment(e.currentTarget.value, 'HH:mm'); + + // check if start and stop do not cross + if (temp[1] === 'start') { + if (newTime.isSameOrBefore(moment(instance.dayVars[temp[0]].finish.get(), 'HH:mm'))) { + instance.dayVars[temp[0]].start.set(e.currentTarget.value); + } else { + e.currentTarget.value = instance.dayVars[temp[0]].start.get(); + } + } else if (temp[1] === 'finish') { + if (newTime.isSameOrAfter(moment(instance.dayVars[temp[0]].start.get(), 'HH:mm'))) { + instance.dayVars[temp[0]].finish.set(e.currentTarget.value); + } else { + e.currentTarget.value = instance.dayVars[temp[0]].finish.get(); + } + } + }, + 'change .dayOpenCheck input'(e, instance) { + var temp = e.currentTarget.name.split('_'); + instance.dayVars[temp[0]][temp[1]].set(e.target.checked); + }, + 'change .preview-settings, keyup .preview-settings'(e, instance) { + let value = e.currentTarget.value; + if (e.currentTarget.type === 'radio') { + value = value === 'true'; + } + + instance[e.currentTarget.name].set(value); + }, + 'submit .rocket-form'(e, instance) { + e.preventDefault(); + + // convert all times to utc then update them in db + for (var d in instance.dayVars) { + if (instance.dayVars.hasOwnProperty(d)) { + var day = instance.dayVars[d]; + var start_utc = moment(day.start.get(), 'HH:mm').utc().format('HH:mm'); + var finish_utc = moment(day.finish.get(), 'HH:mm').utc().format('HH:mm'); + + Meteor.call('livechat:saveOfficeHours', d, start_utc, finish_utc, day.open.get(), function(err /*,result*/) { + if (err) { + return handleError(err); + } + }); + } + } + + RocketChat.settings.set('Livechat_enable_office_hours', instance.enableOfficeHours.get(), (err/*, success*/) => { + if (err) { + return handleError(err); + } + toastr.success(t('Office_Hours_updated')); + }); + } +}); + +Template.livechatOfficeHours.onCreated(function() { + this.dayVars = { + Monday: { + start: new ReactiveVar('08:00'), + finish: new ReactiveVar('20:00'), + open: new ReactiveVar(true) + }, + Tuesday: { + start: new ReactiveVar('00:00'), + finish: new ReactiveVar('00:00'), + open: new ReactiveVar(true) + }, + Wednesday: { + start: new ReactiveVar('00:00'), + finish: new ReactiveVar('00:00'), + open: new ReactiveVar(true) + }, + Thursday: { + start: new ReactiveVar('00:00'), + finish: new ReactiveVar('00:00'), + open: new ReactiveVar(true) + }, + Friday: { + start: new ReactiveVar('00:00'), + finish: new ReactiveVar('00:00'), + open: new ReactiveVar(true) + }, + Saturday: { + start: new ReactiveVar('00:00'), + finish: new ReactiveVar('00:00'), + open: new ReactiveVar(false) + }, + Sunday: { + start: new ReactiveVar('00:00'), + finish: new ReactiveVar('00:00'), + open: new ReactiveVar(false) + } + }; + + this.autorun(() => { + this.subscribe('livechat:officeHour'); + + if (this.subscriptionsReady()) { + LivechatOfficeHour.find().forEach(function(d) { + Template.instance().dayVars[d.day].start.set(moment.utc(d.start, 'HH:mm').local().format('HH:mm')); + Template.instance().dayVars[d.day].finish.set(moment.utc(d.finish, 'HH:mm').local().format('HH:mm')); + Template.instance().dayVars[d.day].open.set(d.open); + }); + } + }); + + this.enableOfficeHours = new ReactiveVar(null); + + this.autorun(() => { + this.enableOfficeHours.set(RocketChat.settings.get('Livechat_enable_office_hours')); + }); +}); \ No newline at end of file diff --git a/packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html b/packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html index d607c7bc323400eb8f19f53607d247f2696f62b6..064a644e0d1256785749a98192d43c84b1d2f11c 100644 --- a/packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html +++ b/packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html @@ -17,6 +17,7 @@ <a href="{{pathFor 'livechat-installation'}}" class="{{active 'livechat-installation'}}">{{_ "Installation"}}</a> <a href="{{pathFor 'livechat-appearance'}}" class="{{active 'livechat-appearance'}}">{{_ "Appearance"}}</a> <a href="{{pathFor 'livechat-integrations'}}" class="{{active 'livechat-integrations'}}">{{_ "Integrations"}}</a> + <a href="{{pathFor 'livechat-officeHours'}}" class="{{active 'livechat-officeHours'}}">{{_ "Office_Hours"}}</a> </li> </ul> </div> diff --git a/packages/rocketchat-livechat/config.js b/packages/rocketchat-livechat/config.js index 01879dd9140626d91bf6226cdbea84c5faa85d1e..22ef78651f3081b7a71ce577f1aa049b5335141a 100644 --- a/packages/rocketchat-livechat/config.js +++ b/packages/rocketchat-livechat/config.js @@ -164,4 +164,11 @@ Meteor.startup(function() { public: true, i18nLabel: 'Show_queue_list_to_all_agents' }); + + RocketChat.settings.add('Livechat_enable_office_hours', false, { + type: 'boolean', + group: 'Livechat', + public: true, + i18nLabel: 'Office_Hours_Enabled' + }); }); diff --git a/packages/rocketchat-livechat/package.js b/packages/rocketchat-livechat/package.js index bcd13384d4a89ff3ed66867be75a67a6437660c9..04728ca4b3c3edd37f7dc4bbbaf8ab35fb6e873d 100644 --- a/packages/rocketchat-livechat/package.js +++ b/packages/rocketchat-livechat/package.js @@ -62,6 +62,7 @@ Package.onUse(function(api) { api.addFiles('client/collections/LivechatQueueUser.js', 'client'); api.addFiles('client/collections/LivechatTrigger.js', 'client'); api.addFiles('client/collections/LivechatInquiry.js', 'client'); + api.addFiles('client/collections/livechatOfficeHour.js', 'client'); api.addFiles('client/methods/changeLivechatStatus.js', 'client'); @@ -90,6 +91,8 @@ Package.onUse(function(api) { api.addFiles('client/views/app/livechatTriggers.js', 'client'); api.addFiles('client/views/app/livechatUsers.html', 'client'); api.addFiles('client/views/app/livechatUsers.js', 'client'); + api.addFiles('client/views/app/livechatOfficeHours.html', 'client'); + api.addFiles('client/views/app/livechatOfficeHours.js', 'client'); api.addFiles('client/views/app/tabbar/externalSearch.html', 'client'); api.addFiles('client/views/app/tabbar/externalSearch.js', 'client'); @@ -149,6 +152,7 @@ Package.onUse(function(api) { api.addFiles('server/methods/webhookTest.js', 'server'); api.addFiles('server/methods/takeInquiry.js', 'server'); api.addFiles('server/methods/returnAsInquiry.js', 'server'); + api.addFiles('server/methods/saveOfficeHours.js', 'server'); // models api.addFiles('server/models/Users.js', 'server'); @@ -161,10 +165,12 @@ Package.onUse(function(api) { api.addFiles('server/models/LivechatTrigger.js', 'server'); api.addFiles('server/models/indexes.js', 'server'); api.addFiles('server/models/LivechatInquiry.js', 'server'); + api.addFiles('server/models/LivechatOfficeHour.js', 'server'); // server lib api.addFiles('server/lib/Livechat.js', 'server'); api.addFiles('server/lib/QueueMethods.js', 'server'); + api.addFiles('server/lib/OfficeClock.js', 'server'); api.addFiles('server/sendMessageBySMS.js', 'server'); api.addFiles('server/forwardUnclosedLivechats.js', 'server'); @@ -183,6 +189,7 @@ Package.onUse(function(api) { api.addFiles('server/publications/visitorInfo.js', 'server'); api.addFiles('server/publications/visitorPageVisited.js', 'server'); api.addFiles('server/publications/livechatInquiries.js', 'server'); + api.addFiles('server/publications/livechatOfficeHours.js', 'server'); // api api.addFiles('server/api.js', 'server'); diff --git a/packages/rocketchat-livechat/server/lib/OfficeClock.js b/packages/rocketchat-livechat/server/lib/OfficeClock.js new file mode 100644 index 0000000000000000000000000000000000000000..24591730e71a659f577ddd2da3eda3faf9506398 --- /dev/null +++ b/packages/rocketchat-livechat/server/lib/OfficeClock.js @@ -0,0 +1,10 @@ +// Every minute check if office closed +Meteor.setInterval(function() { + if (RocketChat.settings.get('Livechat_enable_office_hours')) { + if (RocketChat.models.LivechatOfficeHour.isOpeningTime()) { + RocketChat.models.Users.openOffice(); + } else if (RocketChat.models.LivechatOfficeHour.isClosingTime()) { + RocketChat.models.Users.closeOffice(); + } + } +}, 60000); \ No newline at end of file diff --git a/packages/rocketchat-livechat/server/methods/saveOfficeHours.js b/packages/rocketchat-livechat/server/methods/saveOfficeHours.js new file mode 100644 index 0000000000000000000000000000000000000000..dc3b57fd16775843032b60e8909f62b23684234c --- /dev/null +++ b/packages/rocketchat-livechat/server/methods/saveOfficeHours.js @@ -0,0 +1,5 @@ +Meteor.methods({ + 'livechat:saveOfficeHours'(day, start, finish, open) { + RocketChat.models.LivechatOfficeHour.updateHours(day, start, finish, open); + } +}); \ No newline at end of file diff --git a/packages/rocketchat-livechat/server/models/LivechatOfficeHour.js b/packages/rocketchat-livechat/server/models/LivechatOfficeHour.js new file mode 100644 index 0000000000000000000000000000000000000000..ef77da9a560f1c946994f9921d64d3c6be4927eb --- /dev/null +++ b/packages/rocketchat-livechat/server/models/LivechatOfficeHour.js @@ -0,0 +1,110 @@ +class LivechatOfficeHour extends RocketChat.models._Base { + constructor() { + super(); + this._initModel('livechat_office_hour'); + + this.tryEnsureIndex({ 'day': 1 }); // the day of the week monday - sunday + this.tryEnsureIndex({ 'start': 1 }); // the opening hours of the office + this.tryEnsureIndex({ 'finish': 1 }); // the closing hours of the office + this.tryEnsureIndex({ 'open': 1 }); // whether or not the offices are open on this day + + // if there is nothing in the collection, add defaults + if (this.find().count() === 0) { + this.insert({'day' : 'Monday', 'start' : '08:00', 'finish' : '20:00', 'code' : 1, 'open' : true }); + this.insert({'day' : 'Tuesday', 'start' : '08:00', 'finish' : '20:00', 'code' : 2, 'open' : true }); + this.insert({'day' : 'Wednesday', 'start' : '08:00', 'finish' : '20:00', 'code' : 3, 'open' : true }); + this.insert({'day' : 'Thursday', 'start' : '08:00', 'finish' : '20:00', 'code' : 4, 'open' : true }); + this.insert({'day' : 'Friday', 'start' : '08:00', 'finish' : '20:00', 'code' : 5, 'open' : true }); + this.insert({'day' : 'Saturday', 'start' : '08:00', 'finish' : '20:00', 'code' : 6, 'open' : false }); + this.insert({'day' : 'Sunday', 'start' : '08:00', 'finish' : '20:00', 'code' : 0, 'open' : false }); + + } + } + + /* + * update the given days start and finish times and whether the office is open on that day + */ + updateHours(day, newStart, newFinish, newOpen) { + this.update({ + 'day': day + }, { + $set: { + start: newStart, + finish: newFinish, + open: newOpen + } + }); + } + + /* + * Check if the current server time (utc) is within the office hours of that day + * returns true or false + */ + isNowWithinHours() { + // get current time on server in utc + // var ct = moment().utc(); + var currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm'); + + // get todays office hours from db + var todaysOfficeHours = this.findOne({day: currentTime.format('dddd')}); + if (!todaysOfficeHours) { + return false; + } + + // check if offices are open today + if (todaysOfficeHours.open === false) { + return false; + } + + var start = moment.utc(todaysOfficeHours.day + ':' + todaysOfficeHours.start, 'dddd:HH:mm'); + var finish = moment.utc(todaysOfficeHours.day + ':' + todaysOfficeHours.finish, 'dddd:HH:mm'); + + // console.log(finish.isBefore(start)); + if (finish.isBefore(start)) { + // finish.day(finish.day()+1); + finish.add(1, 'days'); + } + + var result = currentTime.isBetween(start, finish); + + // inBetween check + return result; + } + + isOpeningTime() { + // get current time on server in utc + var currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm'); + + // get todays office hours from db + var todaysOfficeHours = this.findOne({day: currentTime.format('dddd')}); + if (!todaysOfficeHours) { + return false; + } + + // check if offices are open today + if (todaysOfficeHours.open === false) { + return false; + } + + var start = moment.utc(todaysOfficeHours.day + ':' + todaysOfficeHours.start, 'dddd:HH:mm'); + + return start.isSame(currentTime, 'minute'); + } + + isClosingTime() { + // get current time on server in utc + var currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm'); + + // get todays office hours from db + var todaysOfficeHours = this.findOne({day: currentTime.format('dddd')}); + if (!todaysOfficeHours) { + return false; + } + + var finish = moment.utc(todaysOfficeHours.day + ':' + todaysOfficeHours.finish, 'dddd:HH:mm'); + + return finish.isSame(currentTime, 'minute'); + } +} + +RocketChat.models.LivechatOfficeHour = new LivechatOfficeHour(); diff --git a/packages/rocketchat-livechat/server/models/Rooms.js b/packages/rocketchat-livechat/server/models/Rooms.js index c507791ec91b93b533b16c1486d1cce51bf9f3ec..5e585f412b030210191c47ae5e56cea1000b1ead 100644 --- a/packages/rocketchat-livechat/server/models/Rooms.js +++ b/packages/rocketchat-livechat/server/models/Rooms.js @@ -18,7 +18,8 @@ RocketChat.models.Rooms.updateSurveyFeedbackById = function(_id, surveyFeedback) RocketChat.models.Rooms.updateLivechatDataByToken = function(token, key, value) { const query = { - 'v.token': token + 'v.token': token, + open: true }; const update = { diff --git a/packages/rocketchat-livechat/server/models/Users.js b/packages/rocketchat-livechat/server/models/Users.js index f9cfbc3594a866e346ffcfdb4a4f602293f1c65e..a57f738121a485bd20cc481cb36a10ec6f4ddaed 100644 --- a/packages/rocketchat-livechat/server/models/Users.js +++ b/packages/rocketchat-livechat/server/models/Users.js @@ -137,6 +137,26 @@ RocketChat.models.Users.setLivechatStatus = function(userId, status) { return this.update(query, update); }; +/** + * change all livechat agents livechat status to "not-available" + */ +RocketChat.models.Users.closeOffice = function() { + self = this; + self.findAgents().forEach(function(agent) { + self.setLivechatStatus(agent._id, 'not-available'); + }); +}; + +/** + * change all livechat agents livechat status to "available" + */ +RocketChat.models.Users.openOffice = function() { + self = this; + self.findAgents().forEach(function(agent) { + self.setLivechatStatus(agent._id, 'available'); + }); +}; + RocketChat.models.Users.updateLivechatDataByToken = function(token, key, value) { const query = { 'profile.token': token diff --git a/packages/rocketchat-livechat/server/publications/livechatOfficeHours.js b/packages/rocketchat-livechat/server/publications/livechatOfficeHours.js new file mode 100644 index 0000000000000000000000000000000000000000..b81ff8a53b0475fdfb9b4f79ae2eb6e8380c7593 --- /dev/null +++ b/packages/rocketchat-livechat/server/publications/livechatOfficeHours.js @@ -0,0 +1,7 @@ +Meteor.publish('livechat:officeHour', function() { + if (!RocketChat.authz.hasPermission(this.userId, 'view-l-room')) { + return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' })); + } + + return RocketChat.models.LivechatOfficeHour.find(); +}); \ No newline at end of file diff --git a/packages/rocketchat-oembed/client/oembedSandstormGrain.html b/packages/rocketchat-oembed/client/oembedSandstormGrain.html index be0533600ec5a5889f92b78bac1a92ddb77a38be..97157a6b0b7c82fb45341fc28f8fd21b2de5f8d5 100644 --- a/packages/rocketchat-oembed/client/oembedSandstormGrain.html +++ b/packages/rocketchat-oembed/client/oembedSandstormGrain.html @@ -1,12 +1,11 @@ <template name="oembedSandstormGrain"> <blockquote class="sandstorm-grain"> <label> - <h3>{{grainTitle}}</h3> - <img src="{{appIconUrl}}" /> <button onclick="sandstormOembed(event)" data-token="{{token}}" data-descriptor="{{descriptor}}"> - Click to open the grain + {{grainTitle}} </button> + <img src="{{appIconUrl}}" /> </label> </blockquote> </template> diff --git a/packages/rocketchat-slackbridge/slackbridge.js b/packages/rocketchat-slackbridge/slackbridge.js index d093c317238b4f3827f9af3816212ff1c40afa6b..5285493ee35f22bfde9ab7ee51add77525266774 100644 --- a/packages/rocketchat-slackbridge/slackbridge.js +++ b/packages/rocketchat-slackbridge/slackbridge.js @@ -87,10 +87,12 @@ class SlackBridge { } findChannel(channelId) { + logger.class.debug('Searching for Rocket.Chat channel', channelId); return RocketChat.models.Rooms.findOneByImportId(channelId); } addChannel(channelId, hasRetried = false) { + logger.class.debug('Adding channel from Slack', channelId); let data = null; let isGroup = false; if (channelId.charAt(0) === 'C') { @@ -102,10 +104,9 @@ class SlackBridge { if (data && data.data && data.data.ok === true) { let channelData = isGroup ? data.data.group : data.data.channel; let existingRoom = RocketChat.models.Rooms.findOneByName(channelData.name); + + // If the room exists, make sure we have its id in importIds if (existingRoom || channelData.is_general) { - if (channelData.is_general && channelData.name !== (existingRoom && existingRoom.name)) { - Meteor.call('saveRoomSettings', 'GENERAL', 'roomName', channelData.name); - } channelData.rocketId = channelData.is_general ? 'GENERAL' : existingRoom._id; RocketChat.models.Rooms.update({ _id: channelData.rocketId }, { $addToSet: { importIds: channelData.id } }); } else { @@ -113,32 +114,28 @@ class SlackBridge { for (let member of channelData.members) { if (member !== channelData.creator) { let user = this.findUser(member) || this.addUser(member); - if (user) { + if (user && user.username) { users.push(user.username); } } } let creator = channelData.creator ? this.findUser(channelData.creator) || this.addUser(channelData.creator) : null; if (!creator) { - logger.events.error('Could not fetch room creator information', channelData.creator); + logger.class.error('Could not fetch room creator information', channelData.creator); return; } try { - Meteor.runAsUser(creator._id, () => { - if (isGroup) { - let channel = Meteor.call('createPrivateGroup', channelData.name, users); - channelData.rocketId = channel._id; - } else { - let channel = Meteor.call('createChannel', channelData.name, users); - channelData.rocketId = channel._id; - } - }); + let channel = RocketChat.createRoom(isGroup ? 'p' : 'c', channelData.name, creator.username, users); + channelData.rocketId = channel.rid; } catch (e) { if (!hasRetried) { + logger.class.debug('Error adding channel from Slack. Will retry in 1s.', e.message); // If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s. Meteor._sleepForMs(1000); return this.findChannel(channelId) || this.addChannel(channelId, true); + } else { + console.log(e.message); } } @@ -164,6 +161,7 @@ class SlackBridge { } findUser(userId) { + logger.class.debug('Searching for Rocket.Chat user', userId); let user = RocketChat.models.Users.findOneByImportId(userId); if (user && !this.userTags[userId]) { this.userTags[userId] = { slack: `<@${userId}>`, rocket: `@${user.username}` }; @@ -172,6 +170,7 @@ class SlackBridge { } addUser(userId) { + logger.class.debug('Adding user from Slack', userId); let data = HTTP.get('https://slack.com/api/users.info', { params: { token: this.apiToken, user: userId } }); if (data && data.data && data.data.ok === true && data.data.user && data.data.user.profile && data.data.user.profile.email) { let userData = data.data.user; @@ -181,37 +180,47 @@ class SlackBridge { userData.name = existingUser.username; } else { userData.rocketId = Accounts.createUser({ email: userData.profile.email, password: Date.now() + userData.name + userData.profile.email.toUpperCase() }); - Meteor.runAsUser(userData.rocketId, () => { - Meteor.call('setUsername', userData.name); - Meteor.call('joinDefaultChannels', true); - let url = null; - if (userData.profile.image_original) { - url = userData.profile.image_original; - } else if (userData.profile.image_512) { - url = userData.profile.image_512; - } - Meteor.call('setAvatarFromService', url, null, 'url'); - // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600 - if (userData.tz_offset) { - Meteor.call('updateUserUtcOffset', userData.tz_offset / 3600); - } - if (userData.profile.real_name) { - RocketChat.models.Users.setName(userData.rocketId, userData.profile.real_name); - } - }); - // Deleted users are 'inactive' users in Rocket.Chat + let userUpdate = { + username: userData.name, + utcOffset: userData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600, + roles: [ 'user' ] + }; + + if (userData.profile.real_name) { + userUpdate['name'] = userData.profile.real_name; + } + if (userData.deleted) { - RocketChat.models.Users.setUserActive(userData.rocketId, false); - RocketChat.models.Users.unsetLoginTokens(userData.rocketId); + userUpdate['active'] = false; + userUpdate['services.resume.loginTokens'] = []; } + + RocketChat.models.Users.update({ _id: userData.rocketId }, { $set: userUpdate }); + + let user = RocketChat.models.Users.findOneById(userData.rocketId); + + let url = null; + if (userData.profile.image_original) { + url = userData.profile.image_original; + } else if (userData.profile.image_512) { + url = userData.profile.image_512; + } + try { + RocketChat.setUserAvatar(user, url, null, 'url'); + } catch (error) { + logger.class.debug('Error setting user avatar', error.message); + } + RocketChat.addUserToDefaultChannels(user, true); } + RocketChat.models.Users.update({ _id: userData.rocketId }, { $addToSet: { importIds: userData.id } }); if (!this.userTags[userId]) { this.userTags[userId] = { slack: `<@${userId}>`, rocket: `@${userData.name}` }; } + logger.class.debug('User: ', userData.rocketId); return RocketChat.models.Users.findOneById(userData.rocketId); } - + logger.class.debug('User not added'); return; } @@ -227,11 +236,11 @@ class SlackBridge { return msgObj; } - sendMessage(room, user, message, msgDataDefaults) { + sendMessage(room, user, message, msgDataDefaults, importing) { if (message.type === 'message') { let msgObj = {}; if (!_.isEmpty(message.subtype)) { - msgObj = this.processSubtypedMessage(room, user, message, msgDataDefaults); + msgObj = this.processSubtypedMessage(room, user, message, importing); if (!msgObj) { return; } @@ -264,7 +273,7 @@ class SlackBridge { } } - saveMessage(message) { + saveMessage(message, importing) { let channel = message.channel ? this.findChannel(message.channel) || this.addChannel(message.channel) : null; let user = null; if (message.subtype === 'message_deleted' || message.subtype === 'message_changed') { @@ -277,11 +286,24 @@ class SlackBridge { _id: `slack-${message.channel}-${message.ts.replace(/\./g, '-')}`, ts: new Date(parseInt(message.ts.split('.')[0]) * 1000) }; - this.sendMessage(channel, user, message, msgDataDefaults); + if (importing) { + msgDataDefaults['imported'] = 'slackbridge'; + } + try { + this.sendMessage(channel, user, message, msgDataDefaults, importing); + } catch (e) { + // http://www.mongodb.org/about/contributors/error-codes/ + // 11000 == duplicate key error + if (e.name === 'MongoError' && e.code === 11000) { + return; + } + + throw e; + } } } - processSubtypedMessage(room, user, message) { + processSubtypedMessage(room, user, message, importing) { let msgObj = null; switch (message.subtype) { case 'bot_message': @@ -309,45 +331,84 @@ class SlackBridge { this.editMessage(room, user, message); return; case 'message_deleted': - msgObj = RocketChat.models.Messages.findOneById(`${message.channel}S${message.deleted_ts}`); - if (msgObj) { - Meteor.runAsUser(user._id, () => { - Meteor.call('deleteMessage', msgObj); - }); + if (message.previous_message) { + let _id = `slack-${message.channel}-${message.previous_message.ts.replace(/\./g, '-')}`; + msgObj = RocketChat.models.Messages.findOneById(_id); + if (msgObj) { + RocketChat.deleteMessage(msgObj, user); + } } return; case 'channel_join': - return this.joinRoom(room, user); + if (importing) { + RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(room._id, user, { ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.addUserToRoom(room._id, user); + } + return; case 'group_join': if (message.inviter) { let inviter = message.inviter ? this.findUser(message.inviter) || this.addUser(message.inviter) : null; - if (inviter) { - return this.joinPrivateGroup(inviter, room, user); + if (importing) { + RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(room._id, user, { + ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), + u: { + _id: inviter._id, + username: inviter.username + }, + imported: 'slackbridge' + }); + } else { + RocketChat.addUserToRoom(room._id, user, inviter); } } - break; + return; case 'channel_leave': case 'group_leave': - return this.leaveRoom(room, user); + if (importing) { + RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(room._id, user, { + ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), + imported: 'slackbridge' + }); + } else { + RocketChat.removeUserFromRoom(room._id, user); + } + return; case 'channel_topic': case 'group_topic': - this.setRoomTopic(room, user, message.topic); + if (importing) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', room._id, message.topic, user, { ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomTopic(room._id, message.topic, user); + } return; case 'channel_purpose': case 'group_purpose': - this.setRoomTopic(room, user, message.purpose); + if (importing) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', room._id, message.purpose, user, { ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomTopic(room._id, message.purpose, user); + } return; case 'channel_name': case 'group_name': - this.setRoomName(room, user, message.name); + if (importing) { + RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(room._id, message.name, user, { ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomName(room._id, message.name, user); + } return; case 'channel_archive': case 'group_archive': - this.archiveRoom(room, user); + if (!importing) { + RocketChat.archiveRoom(room); + } return; case 'channel_unarchive': case 'group_unarchive': - this.unarchiveRoom(room, user); + if (!importing) { + RocketChat.unarchiveRoom(room); + } return; case 'file_share': if (message.file && message.file.url_private_download !== undefined) { @@ -358,14 +419,14 @@ class SlackBridge { type: message.file.mimetype, rid: room._id }; - return this.uploadFile(details, message.file.url_private_download, user, room, new Date(parseInt(message.ts.split('.')[0]) * 1000)); + return this.uploadFile(details, message.file.url_private_download, user, room, new Date(parseInt(message.ts.split('.')[0]) * 1000), importing); } break; case 'file_comment': - logger.events.error('File comment not implemented'); + logger.class.error('File comment not implemented'); return; case 'file_mention': - logger.events.error('File mentioned not implemented'); + logger.class.error('File mentioned not implemented'); return; case 'pinned_item': if (message.attachments && message.attachments[0] && message.attachments[0].text) { @@ -385,94 +446,33 @@ class SlackBridge { }] }; - RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${message.attachments[0].channel_id}-${message.attachments[0].ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(message.ts.split('.')[0]) * 1000)); + if (!importing) { + RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${message.attachments[0].channel_id}-${message.attachments[0].ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(message.ts.split('.')[0]) * 1000)); + } return msgObj; } else { - logger.events.error('Pinned item with no attachment'); + logger.class.error('Pinned item with no attachment'); } return; case 'unpinned_item': - logger.events.error('Unpinned item not implemented'); + logger.class.error('Unpinned item not implemented'); return; } } - /** - * Archives a room - **/ - archiveRoom(room, user) { - Meteor.runAsUser(user._id, () => { - return Meteor.call('archiveRoom', room._id); - }); - } - - /** - * Unarchives a room - **/ - unarchiveRoom(room, user) { - Meteor.runAsUser(user._id, () => { - return Meteor.call('unarchiveRoom', room._id); - }); - } - - /** - * Adds user to room and sends a message - **/ - joinRoom(room, user) { - Meteor.runAsUser(user._id, () => { - return Meteor.call('joinRoom', room._id); - }); - } - - /** - * Adds user to room and sends a message - **/ - joinPrivateGroup(inviter, room, user) { - Meteor.runAsUser(inviter._id, () => { - return Meteor.call('addUserToRoom', { rid: room._id, username: user.username }); - }); - } - - /** - * Removes user from room and sends a message - **/ - leaveRoom(room, user) { - Meteor.runAsUser(user._id, () => { - return Meteor.call('leaveRoom', room._id); - }); - } - - /** - * Sets room topic - **/ - setRoomTopic(room, user, topic) { - Meteor.runAsUser(user._id, () => { - return Meteor.call('saveRoomSettings', room._id, 'roomTopic', topic); - }); - } - - /** - * Sets room name - **/ - setRoomName(room, user, name) { - Meteor.runAsUser(user._id, () => { - return Meteor.call('saveRoomSettings', room._id, 'roomName', name); - }); - } - /** * Edits a message **/ editMessage(room, user, message) { let msgObj = { - _id: `${message.channel}S${message.message.ts}`, + //@TODO _id + _id: `slack-${message.channel}-${message.message.ts.replace(/\./g, '-')}`, rid: room._id, msg: this.convertSlackMessageToRocketChat(message.message.text) }; - Meteor.runAsUser(user._id, () => { - return Meteor.call('updateMessage', msgObj); - }); + + RocketChat.updateMessage(msgObj, user); } /** @@ -483,7 +483,7 @@ class SlackBridge { @param [Object] room the Rocket.Chat room @param [Date] timeStamp the timestamp the file was uploaded **/ - uploadFile(details, fileUrl, user, room, timeStamp) { + uploadFile(details, fileUrl, user, room, timeStamp, importing) { let url = Npm.require('url'); let requestModule = /https/i.test(fileUrl) ? Npm.require('https') : Npm.require('http'); var parsedUrl = url.parse(fileUrl, true); @@ -530,6 +530,10 @@ class SlackBridge { attachments: [attachment] }; + if (importing) { + msg.imported = 'slackbridge'; + } + if (details.message_id && (typeof details.message_id === 'string')) { msg['_id'] = details.message_id; } @@ -784,6 +788,7 @@ class SlackBridge { } importFromHistory(family, options) { + logger.class.debug('Importing messages history'); let response = HTTP.get('https://slack.com/api/' + family + '.history', { params: _.extend({ token: this.apiToken }, options) }); if (response && response.data && _.isArray(response.data.messages) && response.data.messages.length > 0) { let latest = 0; @@ -793,28 +798,106 @@ class SlackBridge { latest = message.ts; } message.channel = options.channel; - this.saveMessage(message); + this.saveMessage(message, true); } return { has_more: response.data.has_more, ts: latest }; } } + copyChannelInfo(rid, channelMap) { + logger.class.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid); + let response = HTTP.get('https://slack.com/api/' + channelMap.family + '.info', { params: { token: this.apiToken, channel: channelMap.id } }); + if (response && response.data) { + let data = channelMap.family === 'channels' ? response.data.channel : response.data.group; + if (data && _.isArray(data.members) && data.members.length > 0) { + for (let member of data.members) { + let user = this.findUser(member) || this.addUser(member); + if (user) { + logger.class.debug('Adding user to room', user.username, rid); + RocketChat.addUserToRoom(rid, user, null, true); + } + } + } + + let topic = ''; + let topic_last_set = 0; + let topic_creator = null; + if (data && data.topic && data.topic.value) { + topic = data.topic.value; + topic_last_set = data.topic.last_set; + topic_creator = data.topic.creator; + } + + if (data && data.purpose && data.purpose.value) { + if (topic_last_set) { + if (topic_last_set < data.purpose.last_set) { + topic = data.purpose.topic; + topic_creator = data.purpose.creator; + } + } else { + topic = data.purpose.topic; + topic_creator = data.purpose.creator; + } + } + + if (topic) { + let creator = this.findUser(topic_creator) || this.addUser(topic_creator); + logger.class.debug('Setting room topic', rid, topic, creator.username); + RocketChat.saveRoomTopic(rid, topic, creator); + } + } + } + + copyPins(rid, channelMap) { + let response = HTTP.get('https://slack.com/api/pins.list', { params: { token: this.apiToken, channel: channelMap.id } }); + if (response && response.data && _.isArray(response.data.items) && response.data.items.length > 0) { + for (let pin of response.data.items) { + if (pin.message) { + let user = this.findUser(pin.message.user); + let msgObj = { + rid: rid, + t: 'message_pinned', + msg: '', + u: { + _id: user._id, + username: user.username + }, + attachments: [{ + 'text' : this.convertSlackMessageToRocketChat(pin.message.text), + 'author_name' : user.username, + 'author_icon' : getAvatarUrlFromUsername(user.username), + 'ts' : new Date(parseInt(pin.message.ts.split('.')[0]) * 1000) + }] + }; + + RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${pin.channel}-${pin.message.ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000)); + } + } + } + } + importMessages(rid, callback) { logger.class.info('importMessages: ', rid); let rocketchat_room = RocketChat.models.Rooms.findOneById(rid); if (rocketchat_room) { if (this.channelMap[rid]) { + this.copyChannelInfo(rid, this.channelMap[rid]); + logger.class.debug('Importing messages from Slack to Rocket.Chat', this.channelMap[rid], rid); let results = this.importFromHistory(this.channelMap[rid].family, { channel: this.channelMap[rid].id, oldest: 1 }); while (results && results.has_more) { results = this.importFromHistory(this.channelMap[rid].family, { channel: this.channelMap[rid].id, oldest: results.ts }); } + + logger.class.debug('Pinning Slack channel messages to Rocket.Chat', this.channelMap[rid], rid); + this.copyPins(rid, this.channelMap[rid]); + return callback(); } else { let slack_room = this.findSlackChannel(rocketchat_room.name); if (slack_room) { this.channelMap[rid] = { id: slack_room.id, family: slack_room.id.charAt(0) === 'C' ? 'channels' : 'groups' }; - this.importMessages(rid, callback); + return this.importMessages(rid, callback); } else { logger.class.error('Could not find Slack room with specified name', rocketchat_room.name); return callback(new Meteor.Error('error-slack-room-not-found', 'Could not find Slack room with specified name')); diff --git a/packages/rocketchat-slackbridge/slashcommand/slackbridge_import.server.js b/packages/rocketchat-slackbridge/slashcommand/slackbridge_import.server.js index ddc8daa0fbaccd28d4945ec2ec9a27edf90b9899..aa50f7051d17d88153b08cea76cb2540ef76004f 100644 --- a/packages/rocketchat-slackbridge/slashcommand/slackbridge_import.server.js +++ b/packages/rocketchat-slackbridge/slashcommand/slackbridge_import.server.js @@ -1,3 +1,4 @@ +/* globals msgStream */ function SlackBridgeImport(command, params, item) { var channel, room, user; if (command !== 'slackbridge-import' || !Match.test(params, String)) { @@ -6,40 +7,57 @@ function SlackBridgeImport(command, params, item) { room = RocketChat.models.Rooms.findOneById(item.rid); channel = room.name; user = Meteor.users.findOne(Meteor.userId()); - RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { + + msgStream.emit(item.rid, { _id: Random.id(), rid: item.rid, + u: { username: 'rocket.cat' }, ts: new Date(), - msg: TAPi18n.__('SlackBridge_is_importing_your_messages_at_s_', { + msg: TAPi18n.__('SlackBridge_start', { postProcess: 'sprintf', - sprintf: [channel] + sprintf: [user.username, channel] }, user.language) }); - RocketChat.SlackBridge.importMessages(item.rid, err => { - if (err) { - RocketChat.Notifications.notifyUser(user._id, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), - msg: TAPi18n.__('SlackBridge_got_an_error_while_importing_your_messages_at_s__s_', { - postProcess: 'sprintf', - sprintf: [channel, err.message] - }, user.language) - }); - } else { - RocketChat.Notifications.notifyUser(user._id, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), - msg: TAPi18n.__('SlackBridge_has_finished_importing_your_messages_at_s_', { - postProcess: 'sprintf', - sprintf: [channel] - }, user.language) - }); - } - }); - + try { + RocketChat.SlackBridge.importMessages(item.rid, error => { + if (error) { + msgStream.emit(item.rid, { + _id: Random.id(), + rid: item.rid, + u: { username: 'rocket.cat' }, + ts: new Date(), + msg: TAPi18n.__('SlackBridge_error', { + postProcess: 'sprintf', + sprintf: [channel, error.message] + }, user.language) + }); + } else { + msgStream.emit(item.rid, { + _id: Random.id(), + rid: item.rid, + u: { username: 'rocket.cat' }, + ts: new Date(), + msg: TAPi18n.__('SlackBridge_finish', { + postProcess: 'sprintf', + sprintf: [channel] + }, user.language) + }); + } + }); + } catch (error) { + msgStream.emit(item.rid, { + _id: Random.id(), + rid: item.rid, + u: { username: 'rocket.cat' }, + ts: new Date(), + msg: TAPi18n.__('SlackBridge_error', { + postProcess: 'sprintf', + sprintf: [channel, error.message] + }, user.language) + }); + throw error; + } return SlackBridgeImport; } diff --git a/packages/rocketchat-smarsh-connector/lib/rocketchat.js b/packages/rocketchat-smarsh-connector/lib/rocketchat.js new file mode 100644 index 0000000000000000000000000000000000000000..c609f1f3cac59b7b8b843386eed441d62ec70854 --- /dev/null +++ b/packages/rocketchat-smarsh-connector/lib/rocketchat.js @@ -0,0 +1 @@ +RocketChat.smarsh = {}; diff --git a/packages/rocketchat-smarsh-connector/package.js b/packages/rocketchat-smarsh-connector/package.js new file mode 100644 index 0000000000000000000000000000000000000000..2d2ff37b68c82e2c9b9a3813c74083914eda666f --- /dev/null +++ b/packages/rocketchat-smarsh-connector/package.js @@ -0,0 +1,27 @@ +Package.describe({ + name: 'rocketchat:smarsh-connector', + version: '0.0.1', + summary: 'Smarsh Connector', + git: '' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.0'); + + api.use([ + 'ecmascript', + 'rocketchat:lib', + 'underscore', + 'mrt:moment', + 'mrt:moment-timezone' + ]); + + api.addFiles('lib/rocketchat.js', [ 'client', 'server' ]); + api.addFiles([ + 'server/settings.js', + 'server/models/SmarshHistory.js', + 'server/functions/sendEmail.js', + 'server/functions/generateEml.js', + 'server/startup.js' + ], 'server'); +}); diff --git a/packages/rocketchat-smarsh-connector/server/functions/generateEml.js b/packages/rocketchat-smarsh-connector/server/functions/generateEml.js new file mode 100644 index 0000000000000000000000000000000000000000..567fa735f4ad2b45c5022ae0a99b2196899632ce --- /dev/null +++ b/packages/rocketchat-smarsh-connector/server/functions/generateEml.js @@ -0,0 +1,113 @@ +const start = '<table style="width: 100%; border: 1px solid; border-collapse: collapse; table-layout: fixed; margin-top: 10px; font-size: 12px; word-break: break-word;"><tbody>'; +const end = '</tbody></table>'; +const opentr = '<tr style="border: 1px solid;">'; +const closetr = '</tr>'; +const open20td = '<td style="border: 1px solid; text-align: center; width: 20%;">'; +const open60td = '<td style="border: 1px solid; text-align: left; width: 60%; padding: 0 5px;">'; +const closetd = '</td>'; + +function _getLink(attachment) { + const url = attachment.title_link.replace(/ /g, '%20'); + + if (Meteor.settings.public.sandstorm || url.match(/^(https?:)?\/\//i)) { + return url; + } else { + return Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + url; + } +} + +RocketChat.smarsh.generateEml = () => { + Meteor.defer(() => { + RocketChat.models.Rooms.find().forEach((room) => { + const smarshHistory = RocketChat.smarsh.History.findOne({ _id: room._id }); + const query = { rid: room._id }; + + if (smarshHistory) { + query.ts = { $gt: smarshHistory.lastRan }; + } + + const date = new Date(); + const rows = []; + const data = { + users: [], + msgs: 0, + files: [], + time: smarshHistory ? moment(date).diff(moment(smarshHistory.lastRan), 'minutes') : moment(date).diff(moment(room.ts), 'minutes'), + room: room.name ? `#${room.name}` : `Direct Message Between: ${room.usernames.join(' & ')}` + }; + + RocketChat.models.Messages.find(query).forEach((message) => { + rows.push(opentr); + + //The timestamp + rows.push(open20td); + rows.push(moment(message.ts).tz('America/Los_Angeles').format('YYYY-MM-DD HH-mm-ss z')); + rows.push(closetd); + + //The sender + rows.push(open20td); + const sender = RocketChat.models.Users.findOne({ _id: message.u._id }); + if (data.users.indexOf(sender._id) === -1) { + data.users.push(sender._id); + } + + //Get the user's email, can be nothing if it is an unconfigured bot account (like rocket.cat) + if (sender.emails && sender.emails[0] && sender.emails[0].address) { + rows.push(`${sender.name} <${sender.emails[0].address}>`); + } else { + rows.push(`${sender.name} <${RocketChat.settings.get('Smarsh_MissingEmail_Email')}>`); + } + rows.push(closetd); + + //The message + rows.push(open60td); + data.msgs++; + if (message.t) { + const messageType = RocketChat.MessageTypes.getType(message); + if (messageType) { + rows.push(TAPi18n.__(messageType.message, messageType.data ? messageType.data(message) : '', 'en')); + } else { + rows.push(`${message.msg} (${message.t})`); + } + } else if (message.file) { + data.files.push(message.file._id); + rows.push(`${message.attachments[0].title} (${_getLink(message.attachments[0])})`); + } else if (message.attachments) { + const attaches = []; + _.each(message.attachments, function _loopThroughMessageAttachments(a) { + if (a.image_url) { + attaches.push(a.image_url); + } + //TODO: Verify other type of attachments which need to be handled that aren't file uploads and image urls + // } else { + // console.log(a); + // } + }); + + rows.push(`${message.msg} (${attaches.join(', ')})`); + } else { + rows.push(message.msg); + } + rows.push(closetd); + + rows.push(closetr); + }); + + if (rows.length !== 0) { + const result = start + rows.join('') + end; + + RocketChat.smarsh.History.upsert({ _id: room._id }, { + _id: room._id, + lastRan: date, + lastResult: result + }); + + RocketChat.smarsh.sendEmail({ + body: result, + subject: `Rocket.Chat, ${data.users.length} Users, ${data.msgs} Messages, ${data.files.length} Files, ${data.time} Minutes, in ${data.room}`, + files: data.files + }); + } + }); + }); +}; diff --git a/packages/rocketchat-smarsh-connector/server/functions/sendEmail.js b/packages/rocketchat-smarsh-connector/server/functions/sendEmail.js new file mode 100644 index 0000000000000000000000000000000000000000..d2a8c5fd4c70f0e29d977e9a55b75d20251231b7 --- /dev/null +++ b/packages/rocketchat-smarsh-connector/server/functions/sendEmail.js @@ -0,0 +1,32 @@ +/* globals UploadFS */ +//Expects the following details: +// { +// body: '<table>', +// subject: 'Rocket.Chat, 17 Users, 24 Messages, 1 File, 799504 Minutes, in #random', +// files: ['i3nc9l3mn'] +// } + +RocketChat.smarsh.sendEmail = (data) => { + const attachments = []; + + if (data.files.length > 0) { + _.each(data.files, (fileId) => { + const file = RocketChat.models.Uploads.findOneById(fileId); + if (file.store === 'rocketchat_uploads' || file.store === 'fileSystem') { + const rs = UploadFS.getStore(file.store).getReadStream(fileId, file); + attachments.push({ + filename: file.name, + streamSource: rs + }); + } + }); + } + + Email.send({ + to: RocketChat.settings.get('Smarsh_Email'), + from: RocketChat.settings.get('From_Email'), + subject: data.subject, + html: data.body, + attachments: attachments + }); +}; diff --git a/packages/rocketchat-smarsh-connector/server/models/SmarshHistory.js b/packages/rocketchat-smarsh-connector/server/models/SmarshHistory.js new file mode 100644 index 0000000000000000000000000000000000000000..8b28da7b963474d56ee1c2125a76aa3a59b7bd7d --- /dev/null +++ b/packages/rocketchat-smarsh-connector/server/models/SmarshHistory.js @@ -0,0 +1,6 @@ +RocketChat.smarsh.History = new class extends RocketChat.models._Base { + constructor() { + super(); + super._initModel('smarsh_history'); + } +}; diff --git a/packages/rocketchat-smarsh-connector/server/settings.js b/packages/rocketchat-smarsh-connector/server/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..239e8a8fb60c67cd4a4e11efbe7bf840de068ed3 --- /dev/null +++ b/packages/rocketchat-smarsh-connector/server/settings.js @@ -0,0 +1,46 @@ +RocketChat.settings.addGroup('Smarsh', function addSettings() { + this.add('Smarsh_Enabled', false, { + type: 'boolean', + i18nLabel: 'Smarsh_Enabled', + enableQuery: { + _id: 'From_Email', + value: { + $exists: 1, + $ne: '' + } + } + }); + this.add('Smarsh_Email', '', { + type: 'string', + i18nLabel: 'Smarsh_Email', + placeholder: 'email@domain.com' + }); + this.add('Smarsh_MissingEmail_Email', 'no-email@example.com', { + type: 'string', + i18nLabel: 'Smarsh_MissingEmail_Email', + placeholder: 'no-email@example.com' + }); + this.add('Smarsh_Interval', 'every_30_minutes', { + type: 'select', + values: [{ + key: 'every_30_seconds', + i18nLabel: 'every_30_seconds' + }, { + key: 'every_30_minutes', + i18nLabel: 'every_30_minutes' + }, { + key: 'every_1_hours', + i18nLabel: 'every_hour' + }, { + key: 'every_6_hours', + i18nLabel: 'every_six_hours' + }], + enableQuery: { + _id: 'From_Email', + value: { + $exists: 1, + $ne: '' + } + } + }); +}); diff --git a/packages/rocketchat-smarsh-connector/server/startup.js b/packages/rocketchat-smarsh-connector/server/startup.js new file mode 100644 index 0000000000000000000000000000000000000000..a1d515a690d50ab7ecbcf151250d3b40221ddb29 --- /dev/null +++ b/packages/rocketchat-smarsh-connector/server/startup.js @@ -0,0 +1,27 @@ +/* globals SyncedCron */ +const smarshJobName = 'Smarsh EML Connector'; + +const _addSmarshSyncedCronJob = _.debounce(Meteor.bindEnvironment(function __addSmarshSyncedCronJobDebounced() { + if (SyncedCron.nextScheduledAtDate(smarshJobName)) { + SyncedCron.remove(smarshJobName); + } + + if (RocketChat.settings.get('Smarsh_Enabled') && RocketChat.settings.get('Smarsh_Email') !== '' && RocketChat.settings.get('From_Email') !== '') { + SyncedCron.add({ + name: smarshJobName, + schedule: (parser) => parser.text(RocketChat.settings.get('Smarsh_Interval').replace(/_/g, ' ')), + job: RocketChat.smarsh.generateEml + }); + } +}), 500); + +Meteor.startup(() => { + Meteor.defer(() => { + _addSmarshSyncedCronJob(); + + RocketChat.settings.get('Smarsh_Interval', _addSmarshSyncedCronJob); + RocketChat.settings.get('Smarsh_Enabled', _addSmarshSyncedCronJob); + RocketChat.settings.get('Smarsh_Email', _addSmarshSyncedCronJob); + RocketChat.settings.get('From_Email', _addSmarshSyncedCronJob); + }); +}); diff --git a/packages/rocketchat-theme/assets/stylesheets/animation.css b/packages/rocketchat-theme/assets/stylesheets/animation.css index 2a15ee23eae154d170128ffbe9d8b06cf9b4bc48..02a5251e73c73d70c23c1a8d82df6fcbd5155df1 100644 --- a/packages/rocketchat-theme/assets/stylesheets/animation.css +++ b/packages/rocketchat-theme/assets/stylesheets/animation.css @@ -8,6 +8,15 @@ animation: spin 2s infinite linear; display: inline-block; } + +.animate-pulse { + -moz-animation: spin 1s infinite steps(8); + -o-animation: spin 1s infinite steps(8); + -webkit-animation: spin 1s infinite steps(8); + animation: spin 1s infinite steps(8); + display: inline-block; +} + @-moz-keyframes spin { 0% { -moz-transform: rotate(0deg); @@ -82,4 +91,4 @@ -webkit-transform: rotate(359deg); transform: rotate(359deg); } -} \ No newline at end of file +} diff --git a/packages/rocketchat-theme/assets/stylesheets/base.less b/packages/rocketchat-theme/assets/stylesheets/base.less index d7c3da5ce48d3be405525223f698be88e7ca057e..97b690bef415e359f761b611561725508181e455 100644 --- a/packages/rocketchat-theme/assets/stylesheets/base.less +++ b/packages/rocketchat-theme/assets/stylesheets/base.less @@ -491,7 +491,9 @@ input[type='text'], input[type='number'], input[type='email'], input[type='url'], -input[type='password'] { +input[type='password'], +input[type='time'] + { -webkit-appearance: none; -moz-appearance: none; appearance: none; @@ -534,6 +536,7 @@ form.inline { input[type='email'], input[type='url'], input[type='password'], + input[type='time'] select { width: auto; } @@ -577,18 +580,9 @@ textarea[disabled] { font-size: 14px; padding: 8px 8px; } - > i { - visibility: hidden; - &:after { - content: " "; - visibility: visible; - background-image: url('images/logo/loading.gif'); - background-position: center; - background-repeat: no-repeat; - display: block; - height: 40px; - margin-bottom: 12px; - } + .loading { + position: relative; + min-height: 60px; } } @@ -622,19 +616,16 @@ label.required:after { .toggle-favorite {} .loading { - background-image: url('images/loading.gif'); - background-repeat: no-repeat; - background-position: 50%; - width: 100%; - height: 100%; - position: fixed; top: 0; + right: 0; + bottom: 0; left: 0; - &.inline { - position: relative; - min-height: 40px; - background-size: 24px 24px; - } + display: flex; + align-items: center; + position: absolute; + justify-content: center; + font-size: 2em; + color: #ccc; } .btn-loading { @@ -659,12 +650,11 @@ label.required:after { font-weight: 500; font-size: 13px; text-align: center; - margin: 4px; text-transform: uppercase; word-spacing: 0; - box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.125); line-height: 16px; position: relative; + border-radius: 4px; span { position: relative; z-index: 2; @@ -678,7 +668,6 @@ label.required:after { height: 100%; opacity: 0; z-index: 1; - .transition(opacity .1s ease-out); } &:hover { text-decoration: none; @@ -704,19 +693,29 @@ label.required:after { font-weight: 600; } } - &.facebook {} - &.twitter {} - &.google {} - &.github {} - &.gitlab {} - &.trello {} - &.meteor-developer {} &.button-block { display: block; + margin-bottom: 4px; width: 100%; } } +.buttons-group { + display: -webkit-flex; + display: -moz-flex; + display: flex; + margin-bottom: 4px; + .button { + margin-left: 4px; + } + .button:first-child { + margin-left: 0px; + -webkit-flex-grow: 1; + -moz-flex-grow: 1; + flex-grow: 1; + } +} + .sec-header { margin: 16px 0; text-align: center; @@ -1025,7 +1024,6 @@ label.required:after { padding: 15px 12px; line-height: 1; text-decoration: none; - border-bottom: 1px solid; &:nth-child(even) {} &:hover { text-decoration: none; @@ -1574,7 +1572,7 @@ label.required:after { max-width: 800px; margin: 40px auto; padding: 20px; - border-radius: 5px; + border-radius: 4px; box-shadow: 1px 1px 4px rgba(0, 0, 0, .3); .cms-page-close { margin-bottom: 10px; @@ -1698,10 +1696,10 @@ label.required:after { } .section { border: 1px solid #ddd; - border-left: none; + border-radius: 4px; background-color: #fff; padding: 20px; - margin-bottom: 20px; + margin: 20px; &.section-collapsed { .section-content { display: none; @@ -1842,7 +1840,7 @@ label.required:after { .section-content { border: 1px solid; padding: 20px; - border-radius: 5px; + border-radius: 4px; .section-helper { padding: 20px 20px 40px; pre { @@ -2075,6 +2073,7 @@ label.required:after { height: 100%; top: 0; left: 0; + border-right: 1px solid; .room-topic { font-size: 14px; opacity: 0.4; @@ -2107,12 +2106,16 @@ label.required:after { min-height: @footer-min-height; } .message-popup-results { + .loading { + display: none; + } &.notready { .message-popup-items { - background-image: url(images/logo/loading.gif); - background-repeat: no-repeat; - background-position: 50% 50%; + position: relative; height: 100px; + .loading { + display: flex; + } } .popup-item { display: none; @@ -2691,6 +2694,14 @@ label.required:after { } button { display: block; + color: #008ce3; + border: 0px; + min-height: 26px; + text-decoration: none; + + &:hover { + color: #006db0; + } } } } @@ -2802,20 +2813,14 @@ body:not(.is-cordova) { height: 100%; top: 0; right: 0; - background: #FCFCFC; - border-left: 1px solid #eaeaea; z-index: 130; .tab-button { position: relative; cursor: pointer; - padding: 10px 0; - background: #FCFCFC; - border-bottom: 1px solid #eaeaea; text-align: center; - &:hover { - background: #EAEAEA; + button { + height: 38px; } - .counter { position: absolute; background: #999; @@ -2830,16 +2835,11 @@ body:not(.is-cordova) { top: 4px; text-align: center; } - &.active { - background-color: #F4F4F4; - margin-left: -1px; - border-right: 3px solid #ff0000; - + border-right: 3px solid; button { - margin-left: 4px; + margin-left: 3px; } - .counter { margin-right: -3px; } @@ -2905,7 +2905,6 @@ body:not(.is-cordova) { // FLEX-TAB and FLEX-TAB views .flex-tab { - border-left: 1px solid; overflow-x: visible; position: fixed; z-index: 110; @@ -3117,6 +3116,11 @@ body:not(.is-cordova) { color: #006db0; text-decoration: underline; } + p { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } } i { @@ -3153,31 +3157,10 @@ body:not(.is-cordova) { } .thumb { width: 100%; - height: 200px; - - .transition(height .4s ease); - - cursor: zoom-in; - cursor: -moz-zoom-in; - cursor: -webkit-zoom-in; - - &.bigger { - height: 350px; - cursor: zoom-out; - cursor: -moz-zoom-out; - cursor: -webkit-zoom-out; - } - - .avatar { - border-radius: 0; - - .avatar-image { - border-radius: 0; - } - } + height: 350px; + padding: 20px; } nav { - margin-left: -4px; padding: 0 20px; .back { float: right; @@ -3692,8 +3675,8 @@ body:not(.is-cordova) { max-width: 520px; padding: 20px; margin: 20px auto; - box-shadow: 0 0 6px 10px rgba(0, 0, 0, 0.1); - border-radius: 2px; + box-shadow: 0 1px 1px 0 rgba(0,0,0,0.2),0 2px 10px 0 rgba(0,0,0,0.16); + border-radius: 4px; position: relative; z-index: 1; header { @@ -3718,29 +3701,13 @@ body:not(.is-cordova) { img { width: 200px; } - a { - margin: 4px 0; - display: inline-block; - &:active {} - &:hover {} - } .options { display: none; width: 100%; font-size: 10px; } - .submit { - margin: 12px 0; - } - .remember { - float: left; - } - .remember input { - margin-right: 4px; - } - .forgot { - float: right; - line-height: 20px; + .submit, .register, .forgot-password, .back-to-login { + margin-top: 12px; } .input-text { margin: 0 0 14px 0; @@ -3763,6 +3730,7 @@ body:not(.is-cordova) { box-shadow: 0 0 0; border-width: 0; position: relative; + margin-top: 14px; padding: 4px 8px; font-size: 18px; border-bottom: 1px solid; @@ -3783,19 +3751,29 @@ body:not(.is-cordova) { } .select-arrow { position: absolute; - top: 10px; + bottom: 11px; right: 0px; color: #a9a9a9; } - input:-webkit-autofill {} - input:-webkit-autofill { - -webkit-box-shadow: 0 0 0px 1000px #f4f4f4 inset; + label { + position: absolute; + top: 20px; + left: 8px; + display: block; + font-size: 18px; + text-align: left; + color: #a9a9a9; + transition: all 0.3s; + z-index: 100; + } + &.focus label { + top: 0; + font-size: 12px; } - .input-error { text-align: left; color: #b40202; - padding-left: 4px; + padding-left: 8px; font-weight: bold; font-size: 14px; } @@ -3803,26 +3781,10 @@ body:not(.is-cordova) { } .social-login { - text-align: center; - position: relative; - z-index: 1; - display: -webkit-flex; - display: flex; - flex-wrap: wrap; - -webkit-flex-wrap: wrap; margin-bottom: 20px; - h3 { - &:extend(.rocket-h3); - margin-top: 0; - margin-bottom: 12px; - } .button { - border-radius: 2px; - min-height: 40px; - line-height: 20px; + line-height: 22px; font-size: 18px; - margin: 2px; - padding: 0; -webkit-flex-grow: 1; flex-grow: 1; } @@ -4373,17 +4335,6 @@ body:not(.is-cordova) { } } -.group-call-buttons { - display: -webkit-flex; - display: -moz-flex; - display: flex; - button:first-child { - -webkit-flex-grow: 1; - -moz-flex-grow: 1; - flex-grow: 1; - } -} - .alert-icon { font-size: 80px; display: block; @@ -4522,72 +4473,6 @@ body:not(.is-cordova) { margin-top: 1em; } -.page-loading { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, .5); - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; - .spinner { - margin: 10px auto; - width: 50px; - height: 40px; - text-align: center; - font-size: 10px; - } - .spinner > div { - background-color: #fff; - height: 100%; - width: 6px; - display: inline-block; - -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out; - animation: sk-stretchdelay 1.2s infinite ease-in-out; - } - .spinner .rect2 { - -webkit-animation-delay: -1.1s; - animation-delay: -1.1s; - } - .spinner .rect3 { - -webkit-animation-delay: -1.0s; - animation-delay: -1.0s; - } - .spinner .rect4 { - -webkit-animation-delay: -0.9s; - animation-delay: -0.9s; - } - .spinner .rect5 { - -webkit-animation-delay: -0.8s; - animation-delay: -0.8s; - } - @-webkit-keyframes sk-stretchdelay { - 0%, - 40%, - 100% { - -webkit-transform: scaleY(0.4) - } - 20% { - -webkit-transform: scaleY(1.0) - } - } - @keyframes sk-stretchdelay { - 0%, - 40%, - 100% { - transform: scaleY(0.4); - -webkit-transform: scaleY(0.4); - } - 20% { - transform: scaleY(1.0); - -webkit-transform: scaleY(1.0); - } - } -} - .code-error-box { .title { background: red; diff --git a/packages/rocketchat-theme/assets/stylesheets/utils/_colors.import.less b/packages/rocketchat-theme/assets/stylesheets/utils/_colors.import.less index 2f61a2d9c08fb3586cef7d0112f09280941849c6..b22a76c5c9b5a9e4b6baa29a60b3517bc4ca0b98 100755 --- a/packages/rocketchat-theme/assets/stylesheets/utils/_colors.import.less +++ b/packages/rocketchat-theme/assets/stylesheets/utils/_colors.import.less @@ -393,7 +393,6 @@ a.github-fork { } button, a { color: fade( @quaternary-font-color, 50%); - border-bottom-color: darken(@primary-background-color, 2%); &:hover { background-color: darken(@primary-background-color, 2%); color: fade( @quaternary-font-color, 75%); @@ -689,6 +688,7 @@ a.github-fork { // change to page-messages .messages-container { + border-right-color: @tertiary-background-color; .edit-room-title { color: @secondary-font-color; &:hover { @@ -832,7 +832,6 @@ a.github-fork { // FLEX-TAB and FLEX-TAB views .flex-tab { background-color: @secondary-background-color; - border-left-color: @tertiary-background-color; .control { background-color: @secondary-background-color; &:before { @@ -1128,15 +1127,6 @@ a.github-fork { color: #b40202; } } - a { - color: @primary-background-color; - &:active { - color: @primary-background-color; - } - &:hover { - color: darken(@primary-background-color, 10%); - } - } .input-text { input, select { background-color: transparent; @@ -1160,10 +1150,9 @@ a.github-fork { } } input:-webkit-autofill { - color: @content-background-color !important; - } - input:-webkit-autofill { - background-color: transparent !important; + color: @primary-font-color !important; + background-color: #FAFAFA !important; + -webkit-box-shadow: 0 0 0px 1000px #FAFAFA inset; } } } @@ -1241,10 +1230,18 @@ a.github-fork { } .flex-tab-bar { + background: #FCFCFC; .tab-button { + &:hover { + background: #EAEAEA; + } &.red { color: red; } + &.active { + background-color: @secondary-background-color; + border-color: #ff0000; + } &.attention { animation-duration: 1000ms; animation-name: blink; diff --git a/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.coffee b/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.coffee index e48436f960bd462964bd9ca91e45dcfd062494df..063582c8e77bb4734de2b94896e86c72f6dae57f 100644 --- a/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.coffee +++ b/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.coffee @@ -32,6 +32,16 @@ Template.adminRoomInfo.helpers canDeleteRoom: -> roomType = ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t return roomType? and RocketChat.authz.hasAtLeastOnePermission("delete-#{roomType}") + readOnly: -> + room = ChatRoom.findOne(@rid, { fields: { ro: 1 }}) + return room?.ro + readOnlyDescription: -> + room = ChatRoom.findOne(@rid, { fields: { ro: 1 }}) + readOnly = room?.ro + if readOnly is true + return t('True') + else + return t('False') Template.adminRoomInfo.events 'click .delete': -> @@ -146,4 +156,8 @@ Template.adminRoomInfo.onCreated -> return handleError(err) if err toastr.success TAPi18n.__ 'Room_unarchived' RocketChat.callbacks.run 'unarchiveRoom', ChatRoom.findOne(rid) + when 'readOnly' + Meteor.call 'saveRoomSettings', rid, 'readOnly', @$('input[name=readOnly]:checked').val() is 'true', (err, result) -> + return handleError err if err + toastr.success TAPi18n.__ 'Read_only_changed_successfully' @editing.set() diff --git a/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.html b/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.html index 2240950882f6968b929160b89eaa474cb34dd1df..03ea571241394f43ff0d7cc642a7658749349cab 100644 --- a/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.html +++ b/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.html @@ -69,7 +69,7 @@ <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> <button type="button" class="button primary save">{{_ "Save"}}</button> {{else}} - <span>{{#if readOnlyDescription}}{{_ "True"}}{{else}}{{_ "False"}}{{/if}}{{#if canEdit}} <i class="icon-pencil" data-edit="readOnly"></i>{{/if}}</span> + <span>{{readOnlyDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="readOnly"></i>{{/if}}</span> {{/if}} </div> </li> diff --git a/packages/rocketchat-ui-admin/publications/adminRooms.js b/packages/rocketchat-ui-admin/publications/adminRooms.js index 1cc49f6ba371ce8c91cb41e0c884580562ffad14..950b70213f57dd586eeabcb7ec758ae3c7e70e21 100644 --- a/packages/rocketchat-ui-admin/publications/adminRooms.js +++ b/packages/rocketchat-ui-admin/publications/adminRooms.js @@ -17,6 +17,7 @@ Meteor.publish('adminRooms', function(filter, types, limit) { u: 1, usernames: 1, muted: 1, + ro: 1, default: 1, topic: 1, msgs: 1, diff --git a/packages/rocketchat-ui-login/login/form.coffee b/packages/rocketchat-ui-login/login/form.coffee index fb6912ea14363d515eb7ac243fbac0ac21db6564..ab1061a66b307ad0a3c608d843db25777ee6e387 100644 --- a/packages/rocketchat-ui-login/login/form.coffee +++ b/packages/rocketchat-ui-login/login/form.coffee @@ -14,7 +14,7 @@ Template.loginForm.helpers btnLoginSave: -> switch Template.instance().state.get() when 'register' - return t('Submit') + return t('Register') when 'login' return t('Login') when 'email-verification' @@ -147,6 +147,13 @@ Template.loginForm.events OnePassword.findLoginForUrl(succesCallback, errorCallback, Meteor.absoluteUrl()) + 'focus .input-text input': (event) -> + $(event.currentTarget).parents('.input-text').addClass('focus') + + 'blur .input-text input': (event) -> + if event.currentTarget.value is '' + $(event.currentTarget).parents('.input-text').removeClass('focus') + Template.loginForm.onCreated -> instance = @ diff --git a/packages/rocketchat-ui-login/login/form.html b/packages/rocketchat-ui-login/login/form.html index c1bc71318718a8bb81123e075697a2d79dc9f84f..eea75f0a01419b67681fb6869eb1ced1bad5a6ca 100644 --- a/packages/rocketchat-ui-login/login/form.html +++ b/packages/rocketchat-ui-login/login/form.html @@ -24,40 +24,45 @@ <div class="fields"> {{#if state 'login'}} <div class='input-text active'> - <input type="text" name='emailOrUsername' placeholder='{{emailOrUsernamePlaceholder}}' autocapitalize="off" autocorrect="off" /> + <label for="emailOrUsername">{{emailOrUsernamePlaceholder}}</label> + <input type="text" name='emailOrUsername' id="emailOrUsername" autocapitalize="off" autocorrect="off" /> {{#if hasOnePassword}} <div class="one-passsword"></div> {{/if}} <div class="input-error"></div> </div> <div class='input-text active'> - <input type="password" name='pass' placeholder='{{passwordPlaceholder}}' /> + <label for="pass">{{passwordPlaceholder}}</label> + <input type="password" name='pass' id="pass" /> <div class="input-error"></div> </div> {{/if}} {{#if state 'register'}} <div class='input-text active'> - <input type="text" name='name' placeholder='{{namePlaceholder}}' dir="auto" /> + <label for="name">{{namePlaceholder}}</label> + <input type="text" name='name' id="name" dir="auto" /> <div class="input-error"></div> </div> <div class='input-text active'> - <input type="email" name='email' placeholder='{{_ "Email"}}' /> + <label for="email">{{_ "Email"}}</label> + <input type="email" name='email' id="email" /> <div class="input-error"></div> </div> {{#each customFields}} {{#if $eq field.type 'select'}} - <div class='input-text active'> + <div class='input-text active focus'> + <label for="{{fieldName}}">{{_ fieldName}}</label> <div class="select-arrow"> <i class="icon-down-open"></i> </div> <select name="{{fieldName}}" data-customfield="true"> {{#each field.options}} {{#if $eq . ../field.defaultValue}} - <option value="{{.}}" selected>{{_ ../fieldName}}: {{_ .}}</option> + <option value="{{.}}" selected>{{_ .}}</option> {{else}} - <option value="{{.}}">{{_ ../fieldName}}: {{_ .}}</option> + <option value="{{.}}">{{_ .}}</option> {{/if}} {{/each}} </select> @@ -67,20 +72,23 @@ {{#if $eq field.type 'text'}} <div class='input-text active'> - <input type="text" name="{{fieldName}}" data-customfield="true" placeholder="{{_ fieldName}}" value="{{field.defaultValue}}" maxlength="{{field.maxLength}}" /> + <label for="{{fieldName}}">{{_ fieldName}}</label> + <input type="text" name="{{fieldName}}" id="{{fieldName}}" data-customfield="true" value="{{field.defaultValue}}" maxlength="{{field.maxLength}}" /> <div class="input-error"></div> </div> {{/if}} {{/each}} <div class='input-text active'> - <input type="password" name='pass' placeholder='{{passwordPlaceholder}}' /> + <label for="pass">{{passwordPlaceholder}}</label> + <input type="password" name='pass' id="pass" /> <div class="input-error"></div> </div> {{#if requirePasswordConfirmation}} <div class='input-text active'> - <input type="password" name='confirm-pass' placeholder='{{_ "Confirm_password"}}' /> + <label for="confirm-pass">{{_ "Confirm_password"}}</label> + <input type="password" name='confirm-pass' id="confirm-pass" /> <div class="input-error"></div> </div> {{/if}} @@ -88,7 +96,8 @@ {{#if state 'forgot-password' 'email-verification'}} <div class='input-text active'> - <input type="email" name='email' placeholder='{{_ "Email"}}' /> + <label for="email">{{_ "Email"}}</label> + <input type="email" name='email' id="email" /> </div> {{/if}} </div> @@ -99,8 +108,8 @@ {{#if state 'login'}} {{#if registrationAllowed}} - <div class="register"> - <button type="button">{{_ 'Register'}}</button> + <div> + <button type="button" class="register">{{_ 'Register'}}</button> </div> {{else}} {{#if linkReplacementText}} @@ -109,8 +118,8 @@ {{/if}} {{#if passwordResetAllowed}} - <div class="forgot-password"> - <button type="button">{{_ 'Forgot_password'}}</button> + <div> + <button type="button" class="forgot-password">{{_ 'Forgot_password'}}</button> </div> {{/if}} {{/if}} @@ -118,8 +127,8 @@ {{/if}} {{#unless state 'login'}} - <div class="back-to-login"> - <button type="button">{{_ 'Back_to_login'}}</button> + <div> + <button type="button" class="back-to-login">{{_ 'Back_to_login'}}</button> </div> {{/unless}} </form> diff --git a/packages/rocketchat-ui-login/login/services.html b/packages/rocketchat-ui-login/login/services.html index c71ee6728e5d3e0cfd8323865881ba6989f3922c..7eb0c3dfebbc747a2a6b2a1b6f0d1c3f42c5a67c 100644 --- a/packages/rocketchat-ui-login/login/services.html +++ b/packages/rocketchat-ui-login/login/services.html @@ -1,6 +1,6 @@ <template name='loginServices'> {{#if loginService.length}} - <div class="social-login"> + <div class="social-login buttons-group"> {{#each loginService}} <button type="button" class="button external-login {{service.service}}" title="{{displayName}}" style="{{#if service.buttonColor}}background-color:{{service.buttonColor}};{{/if}}{{#if service.buttonLabelColor}}color:{{service.buttonLabelColor}};{{/if}}"><i class="icon-{{icon}} service-icon"></i><i class="icon-spin animate-spin loading-icon hidden"></i><span>{{service.buttonLabelText}}</span></button> {{/each}} diff --git a/packages/rocketchat-ui-login/reset-password/resetPassword.html b/packages/rocketchat-ui-login/reset-password/resetPassword.html index f88c29023b967ab9a5104bd86445882de50d3e01..8592692eb7532b06ada3286046af2fbce03be70e 100644 --- a/packages/rocketchat-ui-login/reset-password/resetPassword.html +++ b/packages/rocketchat-ui-login/reset-password/resetPassword.html @@ -13,7 +13,8 @@ {{/if}} </header> <div class="input-text active"> - <input type="password" name="newPassword" placeholder="{{_ "Type_your_new_password"}}" dir="auto" /> + <label for="newPassword">{{_ "Type_your_new_password"}}</label> + <input type="password" name="newPassword" id="newPassword" dir="auto" /> </div> <div class="submit"> <button data-loading-text="{{_ "Please_wait"}}..." class="button primary resetpass"><span>{{_ "Reset"}}</span></button> diff --git a/packages/rocketchat-ui-login/reset-password/resetPassword.js b/packages/rocketchat-ui-login/reset-password/resetPassword.js index 3294916a2c8e301338319f758bda9835c1667156..b3222a2340f9ae05baab85489487ab3afe02829e 100644 --- a/packages/rocketchat-ui-login/reset-password/resetPassword.js +++ b/packages/rocketchat-ui-login/reset-password/resetPassword.js @@ -14,6 +14,14 @@ Template.resetPassword.helpers({ }); Template.resetPassword.events({ + 'focus .input-text input': function(event) { + $(event.currentTarget).parents('.input-text').addClass('focus'); + }, + 'blur .input-text input': function(event) { + if (event.currentTarget.value === '') { + $(event.currentTarget).parents('.input-text').removeClass('focus'); + } + }, 'submit #login-card': function(event, instance) { event.preventDefault(); diff --git a/packages/rocketchat-ui-login/username/username.coffee b/packages/rocketchat-ui-login/username/username.coffee index 2cf00abd556576fe956e436b04bf22d2b2ffb9f9..3517e9fafcadeb14708dc12f4a8d09999a6b02e0 100644 --- a/packages/rocketchat-ui-login/username/username.coffee +++ b/packages/rocketchat-ui-login/username/username.coffee @@ -14,6 +14,13 @@ Template.username.helpers return Template.instance().username.get() Template.username.events + 'focus .input-text input': (event) -> + $(event.currentTarget).parents('.input-text').addClass('focus') + + 'blur .input-text input': (event) -> + if event.currentTarget.value is '' + $(event.currentTarget).parents('.input-text').removeClass('focus') + 'submit #login-card': (event, instance) -> event.preventDefault() diff --git a/packages/rocketchat-ui-login/username/username.html b/packages/rocketchat-ui-login/username/username.html index 5c5af5dc37c1c7e632123f020c860772ab4eebe8..4f12d49f0d30636fc0807c594f1c0937c02e63de 100644 --- a/packages/rocketchat-ui-login/username/username.html +++ b/packages/rocketchat-ui-login/username/username.html @@ -25,7 +25,8 @@ <div class='input-text active'> {{#if username.ready}} <span> - <input type="text" name='username' value="{{username.username}}" placeholder='{{_ "Username"}}' dir="auto"/> + <label for="username">{{_ "Username"}}</label> + <input type="text" name='username' id="username" value="{{username.username}}" dir="auto"/> </span> <i></i> {{else}} diff --git a/packages/rocketchat-ui-master/master/loading.html b/packages/rocketchat-ui-master/master/loading.html index 91f1bf7aee0c47cb816c5b42e73adb16955bce5e..6ca8ffd53eff54a0c67edcbba56c8c53b068915f 100644 --- a/packages/rocketchat-ui-master/master/loading.html +++ b/packages/rocketchat-ui-master/master/loading.html @@ -1,24 +1,5 @@ <template name="loading"> - <svg class="rocket-loader" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="200px" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve"> - <g> - <path class="outer" fill="#CC3333" d="M188.146,99.881c0-8.852-2.647-17.341-7.873-25.232c-4.691-7.083-11.263-13.354-19.533-18.637 - c-15.967-10.199-36.953-15.817-59.089-15.817c-7.395,0-14.682,0.625-21.751,1.863c-4.387-4.105-9.521-7.798-14.953-10.72 - c-29.024-14.066-53.094-0.331-53.094-0.331s22.379,18.383,18.739,34.499c-10.012,9.931-15.438,21.906-15.438,34.375 - c0,0.04,0.002,0.08,0.003,0.119c-0.001,0.04-0.003,0.08-0.003,0.119c0,12.469,5.426,24.443,15.438,34.375 - c3.64,16.114-18.739,34.498-18.739,34.498s24.069,13.735,53.094-0.33c5.433-2.922,10.566-6.615,14.953-10.721 - c7.069,1.239,14.356,1.863,21.751,1.863c22.136,0,43.122-5.617,59.089-15.816c8.271-5.282,14.842-11.554,19.533-18.637 - c5.226-7.892,7.873-16.38,7.873-25.232c0-0.04-0.002-0.08-0.002-0.119S188.146,99.92,188.146,99.881z"/> - <path class="inner" fill="#FFFFFF" d="M101.686,51.726c41.843,0,75.765,21.667,75.765,48.395c0,26.728-33.922,48.396-75.765,48.396 - c-9.317,0-18.239-1.076-26.483-3.042c-8.378,10.08-26.809,24.092-44.713,19.562c5.823-6.255,14.452-16.825,12.604-34.233 - c-10.731-8.351-17.173-19.037-17.173-30.683C25.921,73.393,59.842,51.726,101.686,51.726"/> - <g> - <circle fill="#CC3333" cx="136.68" cy="100.121" r="10.063"/> - <circle fill="#CC3333" cx="101.685" cy="100.121" r="10.064"/> - <circle fill="#CC3333" cx="66.691" cy="100.121" r="10.064"/> - </g> - <path class="inner" fill="#CCCCCC" d="M101.686,142.149c-9.317,0-18.239-0.933-26.483-2.636c-7.397,7.713-22.634,18.081-38.425,17.699 - c-2.079,3.153-4.34,5.732-6.288,7.824c17.904,4.53,36.335-9.481,44.713-19.562c8.244,1.966,17.166,3.042,26.483,3.042 - c41.507,0,75.214-21.324,75.752-47.755C176.899,123.67,143.192,142.149,101.686,142.149z"/> - </g> - </svg> + <div class="loading"> + <i class="icon-spinner animate-pulse"></i> + </div> </template> diff --git a/packages/rocketchat-ui-master/master/main.html b/packages/rocketchat-ui-master/master/main.html index a5f089eb6bdac7441199b850316c3a4470991387..357f60a39079c9f4a63114fcd88c4dd533e74fb9 100644 --- a/packages/rocketchat-ui-master/master/main.html +++ b/packages/rocketchat-ui-master/master/main.html @@ -86,6 +86,6 @@ <script>{{{CustomScriptLoggedIn}}}</script> {{/unless}} {{else}} - {{> pageLoading}} + {{> loading}} {{/if}} </template> diff --git a/packages/rocketchat-ui-master/master/pageLoading.html b/packages/rocketchat-ui-master/master/pageLoading.html deleted file mode 100644 index 58baf29401aabbd88689a56691b4ad92586709f5..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-master/master/pageLoading.html +++ /dev/null @@ -1,11 +0,0 @@ -<template name="pageLoading"> - <div class="page-loading"> - <div class="spinner"> - <div class="rect1"></div> - <div class="rect2"></div> - <div class="rect3"></div> - <div class="rect4"></div> - <div class="rect5"></div> - </div> - </div> -</template> diff --git a/packages/rocketchat-ui-master/package.js b/packages/rocketchat-ui-master/package.js index 587555fc5dafbf27929c1bfb916330f07934dff5..ce4e67300acc9b52e86b091360965b501c85a709 100644 --- a/packages/rocketchat-ui-master/package.js +++ b/packages/rocketchat-ui-master/package.js @@ -26,7 +26,6 @@ Package.onUse(function(api) { api.addFiles('master/main.html', 'client'); api.addFiles('master/loading.html', 'client'); - api.addFiles('master/pageLoading.html', 'client'); api.addFiles('master/error.html', 'client'); api.addFiles('master/logoLayout.html', 'client'); diff --git a/packages/rocketchat-ui-master/server/inject.js b/packages/rocketchat-ui-master/server/inject.js index 4f076798f9f7f9980f8c62bfff6b3e305a6b27b9..8ef623f7998a4ad8f6ad857f8c90e32973132a8a 100644 --- a/packages/rocketchat-ui-master/server/inject.js +++ b/packages/rocketchat-ui-master/server/inject.js @@ -2,12 +2,8 @@ Inject.rawBody('page-loading', ` <div id="initial-page-loading" class="page-loading"> - <div class="spinner"> - <div class="rect1"></div> - <div class="rect2"></div> - <div class="rect3"></div> - <div class="rect4"></div> - <div class="rect5"></div> + <div class="loading"> + <i class="icon-spinner animate-pulse"></i> </div> </div>` ); diff --git a/packages/rocketchat-ui-message/message/messageBox.coffee b/packages/rocketchat-ui-message/message/messageBox.coffee index 816114c87219ee4997e8f16e01c2ec1116dcb254..0206c73e38511d1295d9be1a0adff1ad05a1994a 100644 --- a/packages/rocketchat-ui-message/message/messageBox.coffee +++ b/packages/rocketchat-ui-message/message/messageBox.coffee @@ -26,6 +26,8 @@ Template.messageBox.helpers return RocketChat.settings.get('Message_ShowFormattingTips') and (RocketChat.Markdown or RocketChat.MarkdownCode or katexSyntax()) canJoin: -> return RocketChat.roomTypes.verifyShowJoinLink @_id + joinCodeRequired: -> + return Session.get('roomData' + this._id)?.joinCodeRequired subscribed: -> return RocketChat.roomTypes.verifyCanSendMessage @_id allowedToSend: -> @@ -86,7 +88,15 @@ Template.messageBox.events 'click .join': (event) -> event.stopPropagation() event.preventDefault() - Meteor.call 'joinRoom', @_id + Meteor.call 'joinRoom', @_id, Template.instance().$('[name=joinCode]').val(), (err) -> + if err? + toastr.error t(err.reason) + + if RocketChat.authz.hasAllPermission('preview-c-room') is false and RoomHistoryManager.getRoom(@_id).loaded is 0 + RoomManager.getOpenedRoomByRid(@_id).streamActive = false + RoomManager.getOpenedRoomByRid(@_id).ready = false + RoomHistoryManager.getRoom(@_id).loaded = undefined + RoomManager.computation.invalidate() 'focus .input-message': (event) -> KonchatNotification.removeRoomNotification @_id @@ -105,7 +115,12 @@ Template.messageBox.events chatMessages[@_id].keyup(@_id, event, instance) instance.isMessageFieldEmpty.set(chatMessages[@_id].isEmpty()) - 'paste .input-message': (e) -> + 'paste .input-message': (e, instance) -> + Meteor.setTimeout -> + input = instance.find('.input-message') + input.updateAutogrow?() + , 50 + if not e.originalEvent.clipboardData? return diff --git a/packages/rocketchat-ui-message/message/messageBox.html b/packages/rocketchat-ui-message/message/messageBox.html index ba633eb6dfe8846cdf4ddf57e34512878891a4b3..0312fc164538215ea6c8659e712bd8a06d9e6e5f 100644 --- a/packages/rocketchat-ui-message/message/messageBox.html +++ b/packages/rocketchat-ui-message/message/messageBox.html @@ -110,6 +110,9 @@ {{#if canJoin}} <div> {{{_ "you_are_in_preview_mode_of" room_name=roomName}}} + {{#if joinCodeRequired}} + <input type="text" name="joinCode" placeholder="{{_ 'Code'}}" style="width: 100px"> + {{/if}} <button class="button join"><span><i class="icon-login"></i> {{_ "join"}}</span></button> </div> {{/if}} diff --git a/packages/rocketchat-ui-message/message/popup/messagePopup.html b/packages/rocketchat-ui-message/message/popup/messagePopup.html index 98d57a2d19e75d40fa0591350ce8c668d9ca099a..de4d729b63ab4c901d83b0c238e779dbff587e5a 100644 --- a/packages/rocketchat-ui-message/message/popup/messagePopup.html +++ b/packages/rocketchat-ui-message/message/popup/messagePopup.html @@ -6,6 +6,7 @@ {{title}} </div> <div class="message-popup-items"> + {{> loading}} {{#each data}} <div class="popup-item" data-id="{{_id}}"> {{> Template.dynamic template=../template}} diff --git a/packages/rocketchat-ui/lib/RoomHistoryManager.coffee b/packages/rocketchat-ui/lib/RoomHistoryManager.coffee index 70089fc4113b91166fa09449bd1312732c5eabc4..7ee6c25f28eae6fea0921ba6504147f8b3ba5375 100644 --- a/packages/rocketchat-ui/lib/RoomHistoryManager.coffee +++ b/packages/rocketchat-ui/lib/RoomHistoryManager.coffee @@ -11,7 +11,7 @@ isLoading: new ReactiveVar false unreadNotLoaded: new ReactiveVar 0 firstUnread: new ReactiveVar - loaded: 0 + loaded: undefined return histories[rid] @@ -63,7 +63,9 @@ RoomManager.updateMentionsMarksOfRoom typeName room.isLoading.set false - room.loaded += result?.messages?.length + room.loaded ?= 0 + if result?.messages?.length? + room.loaded += result.messages.length if result?.messages?.length < limit room.hasMore.set false @@ -102,7 +104,9 @@ RoomManager.updateMentionsMarksOfRoom typeName room.isLoading.set false - room.loaded += result.messages.length + room.loaded ?= 0 + if result.messages.length? + room.loaded += result.messages.length if result.messages.length < limit room.hasMoreNext.set false @@ -170,7 +174,9 @@ setTimeout -> msgElement.removeClass('highlight') , 500 - room.loaded += result.messages.length + room.loaded ?= 0 + if result.messages.length? + room.loaded += result.messages.length room.hasMore.set result.moreBefore room.hasMoreNext.set result.moreAfter @@ -187,7 +193,7 @@ getMoreIfIsEmpty = (rid) -> room = getRoom rid - if room.loaded is 0 + if room.loaded is undefined getMore rid diff --git a/packages/rocketchat-ui/lib/RoomManager.coffee b/packages/rocketchat-ui/lib/RoomManager.coffee index a29280f5d854873502826912c8057d09a9abec02..a1c45b28a14b61b7a5b64b7d5a6dbad46a447898 100644 --- a/packages/rocketchat-ui/lib/RoomManager.coffee +++ b/packages/rocketchat-ui/lib/RoomManager.coffee @@ -42,7 +42,7 @@ Tracker.autorun -> if Meteor.userId() RocketChat.Notifications.onUser 'message', (msg) -> msg.u = - username: 'rocketbot' + username: 'rocket.cat' msg.private = true ChatMessage.upsert { _id: msg._id }, msg @@ -245,6 +245,7 @@ Tracker.autorun -> onlineUsers: onlineUsers updateMentionsMarksOfRoom: updateMentionsMarksOfRoom getOpenedRoomByRid: getOpenedRoomByRid + computation: computation RocketChat.callbacks.add 'afterLogoutCleanUp', -> diff --git a/packages/rocketchat-ui/lib/chatMessages.coffee b/packages/rocketchat-ui/lib/chatMessages.coffee index 4847e6c102d4100d0c1ff329ab14ae97659915bc..c61851ff0c2b68433689b86241274aba884a766c 100644 --- a/packages/rocketchat-ui/lib/chatMessages.coffee +++ b/packages/rocketchat-ui/lib/chatMessages.coffee @@ -169,6 +169,7 @@ class @ChatMessages KonchatNotification.removeRoomNotification(rid) input.value = '' + input.updateAutogrow?() this.hasValue.set false this.stopTyping(rid) @@ -379,7 +380,8 @@ class @ChatMessages RoomHistoryManager.clear rid valueChanged: (rid, event) -> - this.determineInputDirection() + if this.input.value.length is 1 + this.determineInputDirection() determineInputDirection: () -> this.input.dir = if this.isMessageRtl(this.input.value) then 'rtl' else 'ltr' diff --git a/packages/rocketchat-ui/lib/cordova/user-state.coffee b/packages/rocketchat-ui/lib/cordova/user-state.coffee deleted file mode 100644 index f7b867a139d3b40dbc064b1abb1b2e4bd7bfb5f5..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui/lib/cordova/user-state.coffee +++ /dev/null @@ -1,8 +0,0 @@ -if Meteor.isCordova - document.addEventListener 'pause', -> - UserPresence.setAway() - readMessage.disable() - - document.addEventListener 'resume', -> - UserPresence.setOnline() - readMessage.enable() \ No newline at end of file diff --git a/packages/rocketchat-ui/lib/cordova/user-state.js b/packages/rocketchat-ui/lib/cordova/user-state.js new file mode 100644 index 0000000000000000000000000000000000000000..40f4deb58b65c3b3fa827606272376c577b5d042 --- /dev/null +++ b/packages/rocketchat-ui/lib/cordova/user-state.js @@ -0,0 +1,25 @@ +/* globals UserPresence, readMessage */ + +var timer = undefined; +if (Meteor.isCordova) { + document.addEventListener('pause', () => { + UserPresence.setAway(); + readMessage.disable(); + + //Only disconnect after one minute of being in the background + timer = setTimeout(() => { + Meteor.disconnect(); + timer = undefined; + }, 60000); + }, true); + + document.addEventListener('resume', () => { + if (!_.isUndefined(timer)) { + clearTimeout(timer); + } + + Meteor.reconnect(); + UserPresence.setOnline(); + readMessage.enable(); + }, true); +} diff --git a/packages/rocketchat-ui/package.js b/packages/rocketchat-ui/package.js index b9d2abe4e47a61e074a775fa5a4d66902e665e86..d0d60681384110b80076d370f43a27381376cf12 100644 --- a/packages/rocketchat-ui/package.js +++ b/packages/rocketchat-ui/package.js @@ -63,7 +63,7 @@ Package.onUse(function(api) { api.addFiles('lib/cordova/keyboard-fix.coffee', 'client'); api.addFiles('lib/cordova/push.coffee', 'client'); api.addFiles('lib/cordova/urls.coffee', 'client'); - api.addFiles('lib/cordova/user-state.coffee', 'client'); + api.addFiles('lib/cordova/user-state.js', 'client'); // LIB RECORDERJS api.addFiles('lib/recorderjs/audioRecorder.coffee', 'client'); diff --git a/packages/rocketchat-ui/views/app/room.coffee b/packages/rocketchat-ui/views/app/room.coffee index af2f6b6f8f52d22451f7f580d72a0bd4af578230..066e449c444cd7c1dfef3a04483e870f8f731e83 100644 --- a/packages/rocketchat-ui/views/app/room.coffee +++ b/packages/rocketchat-ui/views/app/room.coffee @@ -127,6 +127,16 @@ Template.room.helpers userCanDrop: -> return userCanDrop @_id + canPreview: -> + room = Session.get('roomData' + this._id) + if room.t isnt 'c' + return true + + if RocketChat.authz.hasAllPermission('preview-c-room') + return true + + return RocketChat.models.Subscriptions.findOne({rid: this._id})? + isSocialSharingOpen = false touchMoved = false diff --git a/packages/rocketchat-ui/views/app/room.html b/packages/rocketchat-ui/views/app/room.html index 424fa4af70169bfc38ffffabd4dbab6ef7a234df..22e571663e795e3b9c23470b3fa29ad9fb7975f1 100644 --- a/packages/rocketchat-ui/views/app/room.html +++ b/packages/rocketchat-ui/views/app/room.html @@ -69,20 +69,29 @@ <div class="jump-recent {{#unless hasMoreNext}}not{{/unless}}"> <button>{{_ "Jump_to_recent_messages"}} <i class="icon-level-down"></i></button> </div> + {{#unless canPreview}} + <div class="content room-not-found"> + <div> + {{_ "You_must_join_to_view_messages_in_this_channel"}} + </div> + </div> + {{/unless}} <div class="wrapper {{#if hasMoreNext}}has-more-next{{/if}} {{hideUsername}} {{hideAvatar}}"> <ul aria-live="polite"> - {{#if hasMore}} - <li class="load-more"> - {{#if isLoading}} - <div class="load-more-loading">{{_ "Loading_more_from_history"}}...</div> - {{else}} - <button>{{_ "Has_more"}}...</button> - {{/if}} - </li> - {{else}} - <li class="start"> - {{_ "Start_of_conversation"}} - </li> + {{#if canPreview}} + {{#if hasMore}} + <li class="load-more"> + {{#if isLoading}} + <div class="load-more-loading">{{_ "Loading_more_from_history"}}...</div> + {{else}} + <button>{{_ "Has_more"}}...</button> + {{/if}} + </li> + {{else}} + <li class="start"> + {{_ "Start_of_conversation"}} + </li> + {{/if}} {{/if}} {{#each messagesHistory}} {{#nrr nrrargs 'message' .}}{{/nrr}} diff --git a/packages/rocketchat-ui/views/app/videoCall/videoButtons.html b/packages/rocketchat-ui/views/app/videoCall/videoButtons.html index d8f442d60ff704ea11351d6485286b55d59f520e..2e3e07653e24c149db341e39de6505a69fa65c3b 100644 --- a/packages/rocketchat-ui/views/app/videoCall/videoButtons.html +++ b/packages/rocketchat-ui/views/app/videoCall/videoButtons.html @@ -1,5 +1,5 @@ <template name="videoButtons"> - <div class="group-call-buttons"> + <div class="buttons-group"> {{#if videoAvaliable}} {{#unless videoActive}} {{#if callInProgress}} diff --git a/packages/rocketchat-ui/views/app/videoCall/videoCall.html b/packages/rocketchat-ui/views/app/videoCall/videoCall.html index 4f43b38bccfe96bb871e7cff161f7cf3f8dbb3f8..7c818b314beed182c6aea4183a8c87f185cffa95 100644 --- a/packages/rocketchat-ui/views/app/videoCall/videoCall.html +++ b/packages/rocketchat-ui/views/app/videoCall/videoCall.html @@ -30,7 +30,7 @@ </div> {{/each}} </div> - <div class="group-call-buttons"> + <div class="buttons-group"> {{#if videoActive}} <button class="stop-call button red"><i class="icon-stop"></i>{{_ "Stop"}}</button> {{#if audioEnabled}} diff --git a/packages/rocketchat-videobridge/client/messageType.js b/packages/rocketchat-videobridge/lib/messageType.js similarity index 100% rename from packages/rocketchat-videobridge/client/messageType.js rename to packages/rocketchat-videobridge/lib/messageType.js diff --git a/packages/rocketchat-videobridge/package.js b/packages/rocketchat-videobridge/package.js index bcd5835356dce355f62ca0983d11e2f0c5583359..47c0aaa4dbb2c322941e44ff1c143b8ac4ca7978 100644 --- a/packages/rocketchat-videobridge/package.js +++ b/packages/rocketchat-videobridge/package.js @@ -24,7 +24,9 @@ Package.onUse(function(api) { api.addFiles('client/views/videoFlexTab.js', 'client'); api.addFiles('client/tabBar.js', 'client'); api.addFiles('client/actionLink.js', 'client'); - api.addFiles('client/messageType.js', 'client'); + + //Need to register the messageType with both the server and client + api.addFiles('lib/messageType.js', ['client', 'server']); api.addFiles('server/settings.js', 'server'); api.addFiles('server/models/Rooms.js', 'server'); diff --git a/server/lib/cordova.coffee b/server/lib/cordova.coffee index 09401875ae9c4c83b928a10a2a41ff77a9198817..1a0aab0e35cd5b60b62c9b40ffc9c0173e75455b 100644 --- a/server/lib/cordova.coffee +++ b/server/lib/cordova.coffee @@ -46,6 +46,7 @@ Meteor.methods configurePush = -> if RocketChat.settings.get 'Push_debug' + Push.debug = true console.log 'Push: configuring...' if RocketChat.settings.get('Push_enable') is true diff --git a/server/methods/createChannel.coffee b/server/methods/createChannel.coffee deleted file mode 100644 index 82ad5bfb2bbbadeeaa0133278177af243a3f77df..0000000000000000000000000000000000000000 --- a/server/methods/createChannel.coffee +++ /dev/null @@ -1,73 +0,0 @@ -Meteor.methods - createChannel: (name, members, readOnly) -> - if not Meteor.userId() - throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'createChannel' } - - try - nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' - catch - nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' - - if not nameValidation.test name - throw new Meteor.Error 'error-invalid-name', "Invalid name", { method: 'createChannel' } - - if RocketChat.authz.hasPermission(Meteor.userId(), 'create-c') isnt true - throw new Meteor.Error 'error-not-allowed', "Not allowed", { method: 'createChannel' } - - now = new Date() - user = Meteor.user() - - members.push user.username if user.username not in members - - # avoid duplicate names - if RocketChat.models.Rooms.findOneByName name - if RocketChat.models.Rooms.findOneByName(name).archived - throw new Meteor.Error 'error-archived-duplicate-name', "There's an archived channel with name " + name, { method: 'createChannel', room_name: name } - else - throw new Meteor.Error 'error-duplicate-channel-name', "A channel with name '" + name + "' exists", { method: 'createChannel', room_name: name } - - # name = s.slugify name - - RocketChat.callbacks.run 'beforeCreateChannel', user, - t: 'c' - name: name - ts: now - ro: readOnly is true - sysMes: readOnly isnt true - usernames: members - u: - _id: user._id - username: user.username - - # create new room - room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames 'c', name, user, members, - ts: now - ro: readOnly is true - sysMes: readOnly isnt true - - for username in members - member = RocketChat.models.Users.findOneByUsername username - if not member? - continue - - # make all room members muted by default, unless they have the post-read-only permission - if readOnly is true and RocketChat.authz.hasPermission(member._id, 'post-read-only') is false - RocketChat.models.Rooms.muteUsernameByRoomId room._id, username - - extra = {} - - if username is user.username - extra.ls = now - extra.open = true - - RocketChat.models.Subscriptions.createWithRoomAndUser room, member, extra - - # set creator as channel moderator. permission limited to channel by scoping to rid - RocketChat.authz.addUserRoles(Meteor.userId(), ['owner'], room._id) - - Meteor.defer -> - RocketChat.callbacks.run 'afterCreateChannel', user, room - - return { - rid: room._id - } diff --git a/server/methods/createPrivateGroup.coffee b/server/methods/createPrivateGroup.coffee deleted file mode 100644 index 83339f90a32e48bd56705b7199ca81a060c902c5..0000000000000000000000000000000000000000 --- a/server/methods/createPrivateGroup.coffee +++ /dev/null @@ -1,57 +0,0 @@ -Meteor.methods - createPrivateGroup: (name, members, readOnly) -> - if not Meteor.userId() - throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'createPrivateGroup' } - - unless RocketChat.authz.hasPermission(Meteor.userId(), 'create-p') - throw new Meteor.Error 'error-not-allowed', "Not allowed", { method: 'createPrivateGroup' } - - try - nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' - catch - nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' - - if not nameValidation.test name - throw new Meteor.Error 'error-invalid-name', "Invalid name", { method: 'createPrivateGroup' } - - now = new Date() - - me = Meteor.user() - - members.push me.username - - # name = s.slugify name - - # avoid duplicate names - if RocketChat.models.Rooms.findOneByName name - if RocketChat.models.Rooms.findOneByName(name).archived - throw new Meteor.Error 'error-archived-duplicate-name', "There's an archived channel with name " + name, { method: 'createPrivateGroup', room_name: name } - else - throw new Meteor.Error 'error-duplicate-channel-name', "A channel with name '" + name + "' exists", { method: 'createPrivateGroup', room_name: name } - - # create new room - room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames 'p', name, me, members, - ts: now - ro: readOnly is true - sysMes: readOnly isnt true - - for username in members - member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }}) - if not member? - continue - - extra = {} - - if username is me.username - extra.ls = now - else - extra.alert = true - - RocketChat.models.Subscriptions.createWithRoomAndUser room, member, extra - - # set creator as group moderator. permission limited to group by scoping to rid - RocketChat.authz.addUserRoles(Meteor.userId(), ['owner'], room._id) - - return { - rid: room._id - } diff --git a/server/methods/joinRoom.coffee b/server/methods/joinRoom.coffee deleted file mode 100644 index fdda7e783068411e805079fd3baf60738c308270..0000000000000000000000000000000000000000 --- a/server/methods/joinRoom.coffee +++ /dev/null @@ -1,43 +0,0 @@ -Meteor.methods - joinRoom: (rid) -> - if not Meteor.userId() - throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'joinRoom' } - - room = RocketChat.models.Rooms.findOneById rid - - if not room? - throw new Meteor.Error 'error-invalid-room', 'Invalid room', { method: 'joinRoom' } - - if room.t isnt 'c' or RocketChat.authz.hasPermission(Meteor.userId(), 'view-c-room') isnt true - throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'joinRoom' } - - now = new Date() - - # Check if user is already in room - subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId rid, Meteor.userId() - if subscription? - return - - user = RocketChat.models.Users.findOneById Meteor.userId() - - RocketChat.callbacks.run 'beforeJoinRoom', user, room - - # Automatically mute users in read only rooms - - muted = room.ro and not RocketChat.authz.hasPermission(Meteor.userId(), 'post-read-only') - - RocketChat.models.Rooms.addUsernameById rid, user.username, muted - - RocketChat.models.Subscriptions.createWithRoomAndUser room, user, - ts: now - open: true - alert: true - unread: 1 - - RocketChat.models.Messages.createUserJoinWithRoomIdAndUser rid, user, - ts: now - - Meteor.defer -> - RocketChat.callbacks.run 'afterJoinRoom', user, room - - return true diff --git a/server/methods/loadHistory.coffee b/server/methods/loadHistory.coffee index 3b08ef8296434cacda943031d1fe644e822a25b5..bf92f320d395972405fffeb4b0c251d0abb0b050 100644 --- a/server/methods/loadHistory.coffee +++ b/server/methods/loadHistory.coffee @@ -4,7 +4,11 @@ Meteor.methods throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'loadHistory' } fromId = Meteor.userId() - unless Meteor.call 'canAccessRoom', rid, fromId + room = Meteor.call 'canAccessRoom', rid, fromId + unless room + return false + + if room.t is 'c' and not RocketChat.authz.hasPermission(fromId, 'preview-c-room') and room.usernames.indexOf(room.username) is -1 return false options = diff --git a/server/methods/sendForgotPasswordEmail.coffee b/server/methods/sendForgotPasswordEmail.coffee index 32adedb8fba99eefcc7bfdb294efbb354f24e9e2..549ded5c54f25053155d74414e58cd36bba4a8b2 100644 --- a/server/methods/sendForgotPasswordEmail.coffee +++ b/server/methods/sendForgotPasswordEmail.coffee @@ -1,8 +1,15 @@ Meteor.methods sendForgotPasswordEmail: (email) -> - user = RocketChat.models.Users.findOneByEmailAddress s.trim(email.toLowerCase()) + + email = s.trim(email) + user = RocketChat.models.Users.findOneByEmailAddress(email) + regex = new RegExp("^" + s.escapeRegExp(email) + "$", 'i') + + email = _.find _.pluck(user.emails || [], 'address'), (userEmail) -> + return regex.test(userEmail) if user? - Accounts.sendResetPasswordEmail(user._id, s.trim(email)) + Accounts.sendResetPasswordEmail(user._id, email) return true + return false diff --git a/server/methods/setAvatarFromService.coffee b/server/methods/setAvatarFromService.coffee index ad516e8107f949887e801b6a39ac0e53ac04f102..dedcddea7ad4b278285b33fb6e48c048ade464b9 100644 --- a/server/methods/setAvatarFromService.coffee +++ b/server/methods/setAvatarFromService.coffee @@ -8,53 +8,7 @@ Meteor.methods user = Meteor.user() - if service is 'initials' - RocketChat.models.Users.setAvatarOrigin user._id, service - return - - if service is 'url' - result = null - - try - result = HTTP.get dataURI, npmRequestOptions: {encoding: 'binary'} - catch e - console.log "Error while handling the setting of the avatar from a url (#{dataURI}) for #{user.username}:", e - throw new Meteor.Error('error-avatar-url-handling', 'Error while handling avatar setting from a URL ('+ dataURI +') for ' + user.username, { method: 'setAvatarFromService', url: dataURI, username: user.username }); - - if result.statusCode isnt 200 - console.log "Not a valid response, #{result.statusCode}, from the avatar url: #{dataURI}" - throw new Meteor.Error('error-avatar-invalid-url', 'Invalid avatar URL: ' + dataURI, { method: 'setAvatarFromService', url: dataURI }) - - if not /image\/.+/.test result.headers['content-type'] - console.log "Not a valid content-type from the provided url, #{result.headers['content-type']}, from the avatar url: #{dataURI}" - throw new Meteor.Error('error-avatar-invalid-url', 'Invalid avatar URL: ' + dataURI, { method: 'setAvatarFromService', url: dataURI }) - - ars = RocketChatFile.bufferToStream new Buffer(result.content, 'binary') - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{user.username}.jpg") - aws = RocketChatFileAvatarInstance.createWriteStream encodeURIComponent("#{user.username}.jpg"), result.headers['content-type'] - aws.on 'end', Meteor.bindEnvironment -> - Meteor.setTimeout -> - console.log "Set #{user.username}'s avatar from the url: #{dataURI}" - RocketChat.models.Users.setAvatarOrigin user._id, service - RocketChat.Notifications.notifyAll 'updateAvatar', { username: user.username } - , 500 - - ars.pipe(aws) - return - - {image, contentType} = RocketChatFile.dataURIParse dataURI - - rs = RocketChatFile.bufferToStream new Buffer(image, 'base64') - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{user.username}.jpg") - ws = RocketChatFileAvatarInstance.createWriteStream encodeURIComponent("#{user.username}.jpg"), contentType - ws.on 'end', Meteor.bindEnvironment -> - Meteor.setTimeout -> - RocketChat.models.Users.setAvatarOrigin user._id, service - RocketChat.Notifications.notifyAll 'updateAvatar', {username: user.username} - , 500 - - rs.pipe(ws) - return + return RocketChat.setUserAvatar(user, dataURI, contentType, service); DDPRateLimiter.addRule type: 'method' diff --git a/server/methods/updateUserUtcOffset.coffee b/server/methods/userSetUtcOffset.coffee similarity index 74% rename from server/methods/updateUserUtcOffset.coffee rename to server/methods/userSetUtcOffset.coffee index 9712ad9b5e7c9fbff0116a28c193b549e54ebefd..fb8a7dd662f9113595c4992b92a8516bd3c18586 100644 --- a/server/methods/updateUserUtcOffset.coffee +++ b/server/methods/userSetUtcOffset.coffee @@ -1,5 +1,5 @@ Meteor.methods - updateUserUtcOffset: (utcOffset) -> + userSetUtcOffset: (utcOffset) -> if not @userId? return @@ -9,6 +9,6 @@ Meteor.methods DDPRateLimiter.addRule type: 'method' - name: 'updateUserUtcOffset' + name: 'userSetUtcOffset' userId: -> return true , 1, 60000 diff --git a/server/startup/cron.coffee b/server/startup/cron.coffee index c935e53f0c8ae1a713e902b2ec0e3071a002c8d5..a58810a4264b5f8ed844cd35a9df01fcd4cbb926 100644 --- a/server/startup/cron.coffee +++ b/server/startup/cron.coffee @@ -10,8 +10,8 @@ generateStatistics = -> statistics = RocketChat.statistics.save() statistics.host = Meteor.absoluteUrl() if RocketChat.settings.get 'Statistics_reporting' - try - HTTP.post 'https://rocket.chat/stats', + try + HTTP.post 'https://collector.rocket.chat/', data: statistics catch e logger.warn('Failed to send usage report') diff --git a/server/startup/migrations/v058.js b/server/startup/migrations/v058.js new file mode 100644 index 0000000000000000000000000000000000000000..329900d3d5fef437db608a9e9e3cad3e4b86078b --- /dev/null +++ b/server/startup/migrations/v058.js @@ -0,0 +1,11 @@ +RocketChat.Migrations.add({ + version: 58, + up: function() { + RocketChat.models.Settings.update({ _id: 'Push_gateway', value: 'https://rocket.chat' }, { + $set: { + value: 'https://gateway.rocket.chat', + packageValue: 'https://gateway.rocket.chat' + } + }); + } +}); diff --git a/server/startup/roomPublishes.coffee b/server/startup/roomPublishes.coffee index 4d73c6c11d4863b9c14313dbd1f02c304860a45e..c1c1c7397210f41154bc4c706a21bcd5c58c4fdb 100644 --- a/server/startup/roomPublishes.coffee +++ b/server/startup/roomPublishes.coffee @@ -14,6 +14,10 @@ Meteor.startup -> jitsiTimeout: 1 description: 1 sysMes: 1 + joinCodeRequired: 1 + + if RocketChat.authz.hasPermission(this.userId, 'view-join-code') + options.fields.joinCode = 1 if RocketChat.authz.hasPermission(this.userId, 'view-c-room') return RocketChat.models.Rooms.findByTypeAndName 'c', identifier, options @@ -21,6 +25,7 @@ Meteor.startup -> roomId = RocketChat.models.Subscriptions.findByTypeNameAndUserId('c', identifier, this.userId).fetch() if roomId.length > 0 return RocketChat.models.Rooms.findById(roomId[0]?.rid, options) + return this.ready() RocketChat.roomTypes.setPublish 'p', (identifier) -> diff --git a/server/stream/messages.coffee b/server/stream/messages.coffee index ac005c558fae0ba66956cbeda35b0176a23713a6..45cd8cf9688d64aa98af2cbfbd1545fca01cf65b 100644 --- a/server/stream/messages.coffee +++ b/server/stream/messages.coffee @@ -4,7 +4,12 @@ msgStream.allowWrite('none') msgStream.allowRead (eventName) -> try - return false if not Meteor.call 'canAccessRoom', eventName, this.userId + room = Meteor.call 'canAccessRoom', eventName, this.userId + if not room + return false + + if room.t is 'c' and not RocketChat.authz.hasPermission(this.userId, 'preview-c-room') and room.usernames.indexOf(room.username) is -1 + return false return true catch e @@ -36,6 +41,6 @@ Meteor.startup -> records = RocketChat.models.Messages.getChangedRecords type, args[0], fields for record in records - if record._hidden isnt true + if record._hidden isnt true and not record.imported? msgStream.emit '__my_messages__', record, {} msgStream.emit record.rid, record diff --git a/server/stream/streamBroadcast.coffee b/server/stream/streamBroadcast.coffee index cfe6fca62238557ed181a00694b1c95c8e227161..82ca7b51d52c5af927528b99734897b74502d1a9 100644 --- a/server/stream/streamBroadcast.coffee +++ b/server/stream/streamBroadcast.coffee @@ -4,22 +4,40 @@ logger = new Logger 'StreamBroadcast', auth: 'Auth' stream: 'Stream' -authorizeConnection = (instance) -> +_authorizeConnection = (instance) -> logger.auth.info "Authorizing with #{instance}" + connections[instance].call 'broadcastAuth', connections[instance].instanceRecord._id, InstanceStatus.id(), (err, ok) -> + if err? + return logger.auth.error "broadcastAuth error #{instance} #{connections[instance].instanceRecord._id} #{InstanceStatus.id()}", err + connections[instance].broadcastAuth = ok logger.auth.info "broadcastAuth with #{instance}", ok +authorizeConnection = (instance) -> + if not InstanceStatus.getCollection().findOne({_id: InstanceStatus.id()})? + return Meteor.setTimeout -> + authorizeConnection(instance) + , 500 + + _authorizeConnection(instance) + @connections = {} @startStreamBroadcast = () -> + process.env.INSTANCE_IP ?= 'localhost' + logger.info 'startStreamBroadcast' InstanceStatus.getCollection().find({'extraInformation.port': {$exists: true}}, {sort: {_createdAt: -1}}).observe added: (record) -> - if record.extraInformation.port is process.env.PORT and (record.extraInformation.host is 'localhost' or record.extraInformation.host is process.env.INSTANCE_IP) + instance = "#{record.extraInformation.host}:#{record.extraInformation.port}" + + if record.extraInformation.port is process.env.PORT and record.extraInformation.host is process.env.INSTANCE_IP + logger.auth.info "prevent self connect", instance return - instance = record.extraInformation.host + ':' + record.extraInformation.port + if record.extraInformation.host is process.env.INSTANCE_IP + instance = "localhost:#{record.extraInformation.port}" if connections[instance]?.instanceRecord? if connections[instance].instanceRecord._createdAt < record._createdAt @@ -31,19 +49,22 @@ authorizeConnection = (instance) -> logger.connection.info 'connecting in', instance connections[instance] = DDP.connect(instance, {_dontPrintErrors: true}) connections[instance].instanceRecord = record; - authorizeConnection(instance); connections[instance].onReconnect = -> - authorizeConnection(instance); + authorizeConnection(instance) removed: (record) -> - instance = record.extraInformation.host + ':' + record.extraInformation.port + instance = "#{record.extraInformation.host}:#{record.extraInformation.port}" + + if record.extraInformation.host is process.env.INSTANCE_IP + instance = "localhost:#{record.extraInformation.port}" + if connections[instance]? and not InstanceStatus.getCollection().findOne({'extraInformation.host': record.extraInformation.host, 'extraInformation.port': record.extraInformation.port})? logger.connection.info 'disconnecting from', instance connections[instance].disconnect() delete connections[instance] broadcast = (streamName, eventName, args, userId) -> - fromInstance = (process.env.INSTANCE_IP or 'localhost') + ':' + process.env.PORT + fromInstance = process.env.INSTANCE_IP + ':' + process.env.PORT for instance, connection of connections do (instance, connection) -> if connection.status().connected is true