From cb271d35226ffd784acb4b7b616707b710d5af51 Mon Sep 17 00:00:00 2001 From: Egon Zemmer Date: Thu, 2 Mar 2017 14:24:37 +0100 Subject: [PATCH] Initial import. Bump to version 0.6.1. --- Gemfile | 2 +- README.md | 42 +-- .../settings/_mattermost_settings.html.erb | 56 --- .../settings/_rocketchat_settings.html.erb | 71 ++++ init.rb | 52 +-- lib/redmine_mattermost/listener.rb | 320 ------------------ .../issue_patch.rb | 12 +- lib/redmine_rocketchat/listener.rb | 320 ++++++++++++++++++ 8 files changed, 447 insertions(+), 428 deletions(-) delete mode 100644 app/views/settings/_mattermost_settings.html.erb create mode 100644 app/views/settings/_rocketchat_settings.html.erb delete mode 100644 lib/redmine_mattermost/listener.rb rename lib/{redmine_mattermost => redmine_rocketchat}/issue_patch.rb (80%) create mode 100644 lib/redmine_rocketchat/listener.rb diff --git a/Gemfile b/Gemfile index 9fcac66..c0e611b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem "httpclient" +gem 'httpclient' diff --git a/README.md b/README.md index c999c96..d631f77 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ -# Mattermost chat plugin for Redmine +# Rocket.Chat plugin for Redmine -This plugin posts updates to issues in your Redmine installation to a Mattermost +This plugin posts updates to issues in your Redmine installation to a Rocket.Chat channel. Redmine Supported versions: 2.0.x - 3.3.x. ## Screenshot -![screenshot](https://raw.githubusercontent.com/keritaf/redmine_mattermost/assets/screenshot.png) +![screenshot](https://raw.githubusercontent.com/phlegx/redmine_rocketchat/assets/screenshot.png) ## Installation -From your Redmine plugins directory, clone this repository as `redmine_mattermost` (note +From your Redmine plugins directory, clone this repository as `redmine_rocketchat` (note the underscore!): - git clone https://github.com/keritaf/redmine_mattermost.git redmine_mattermost + git clone https://github.com/phlegx/redmine_rocketchat.git redmine_rocketchat You will also need the `httpclient` dependency, which can be installed by running @@ -23,8 +23,8 @@ You will also need the `httpclient` dependency, which can be installed by runnin from the plugin directory. Restart Redmine, and you should see the plugin show up in the Plugins page. -Under the configuration options, set the Mattermost API URL to the URL for an -Incoming WebHook integration in your Mattermost account and also set the Mattermost +Under the configuration options, set the Rocket.Chat API URL to the URL for an +Incoming WebHook integration in your Rocket.Chat account and also set the Rocket.Chat Channel to the channel's handle (be careful, this is not the channel's display name visible to users, you can find each channel's handle by navigating inside the channel and clicking the down-arrow and selecting view info). See also the next two sections @@ -34,35 +34,35 @@ for advanced and custom routing options. You can also route messages to different channels on a per-project basis. To do this, create a project custom field (Administration > Custom fields > Project) -named `Mattermost Channel`. If no custom channel is defined for a project, the parent +named `Rocket.Chat Channel`. If no custom channel is defined for a project, the parent project will be checked (or the default will be used). To prevent all notifications from being sent for a project, set the custom channel to `-`. -For more information, see http://www.redmine.org/projects/redmine/wiki/Plugins (see also next section for an easy configuration demonstration). +For more information, see http://www.redmine.org/projects/redmine/wiki/Plugins (see also next section for an easy configuration demonstration). ## Screenshot Guided Configuration -Step 1: Create an Incoming Webhook in Mattermost (Account Settings > Integrations > Incoming Webhooks). +Step 1: Create an Incoming Webhook in Rocket.Chat (Administration > Integrations > Incoming WebHook). -![step1](https://raw.githubusercontent.com/keritaf/redmine_mattermost/assets/step1.png) +![step1](https://raw.githubusercontent.com/phlegx/redmine_rocketchat/assets/step1.png) -Step 2: Install this Redmine plugin for Mattermost. +Step 2: Install this Redmine plugin for Rocket.Chat. -![step2](https://raw.githubusercontent.com/keritaf/redmine_mattermost/assets/step2.png) +![step2](https://raw.githubusercontent.com/phlegx/redmine_rocketchat/assets/step2.png) -Step 3: Configure this Redmine plugin for Mattermost. For per-project customized routing, leave the `Mattermost Channel` field empty and follow the next steps, otherwise all Redmine projects will post to the same Mattermost channel. Be careful when filling the channel field, you need to input the channel's handle, not the display name visible to users. You can find each channel's handle by going inside the channel and click the down-arrow and selecting view info. +Step 3: Configure this Redmine plugin for Rocket.Chat. For per-project customized routing, leave the `Rocket.Chat Channel` field empty and follow the next steps, otherwise all Redmine projects will post to the same Rocket.Chat channel. Be careful when filling the channel field, you need to input the channel's handle, not the display name visible to users. You can find each channel's handle by going inside the channel and click the down-arrow and selecting view info. -![step3](https://raw.githubusercontent.com/keritaf/redmine_mattermost/assets/step3.png) +![step3](https://raw.githubusercontent.com/phlegx/redmine_rocketchat/assets/step3.png) -Step 4: For per-project customized routing, first create the project custom field (Administration > Custom fields > Project). +Step 4: For per-project customized routing, first create the project custom field (Administration > Custom fields > New custom field > Projects). -![step4a](https://raw.githubusercontent.com/keritaf/redmine_mattermost/assets/step4a.png) -![step4b](https://raw.githubusercontent.com/keritaf/redmine_mattermost/assets/step4b.png) +![step4a](https://raw.githubusercontent.com/phlegx/redmine_rocketchat/assets/assets/step4a.png) +![step4b](https://raw.githubusercontent.com/phlegx/redmine_rocketchat/assets/assets/step4b.png) -Step 5: For per-project customized routing, configure the Mattermost channel handle inside your Redmine project. +Step 5: For per-project customized routing, configure the Rocket.Chat channel handle inside your Redmine project. -![step5](https://raw.githubusercontent.com/keritaf/redmine_mattermost/assets/step5.png) +![step5](https://raw.githubusercontent.com/phlegx/redmine_rocketchat/assets/assets/step5.png) ## Credits -The source code is forked from https://github.com/altsol/redmine_mattermost. Special thanks to the original author and contributors for making this awesome hook for Redmine. This fork is just refactored to use Mattermost-namespaced configuration options in order to use both hooks (Mattermost and Slack) in a Redmine installation. +The source code is forked from https://github.com/altsol/redmine_mattermost. Special thanks to the original author and contributors for making this awesome hook for Redmine. This fork is just refactored to use Rocket.Chat-namespaced configuration options in order to use all hooks (Rocket.Chat, Mattermost and Slack) in a Redmine installation. diff --git a/app/views/settings/_mattermost_settings.html.erb b/app/views/settings/_mattermost_settings.html.erb deleted file mode 100644 index 8f1d06f..0000000 --- a/app/views/settings/_mattermost_settings.html.erb +++ /dev/null @@ -1,56 +0,0 @@ -

- - -

- -

- Generate an "Incoming WebHook" URL from the Integrations configuration page on Mattermost. This URL can be changed on a per-project basis by creating a project custom field named "Mattermost URL" (without quotes). -

- -

- - -

- -

- The channel can be changed on a per-project basis by creating a - project custom field named "Mattermost Channel" (without quotes). -

- -

- - -

- -

- - -

- -

- - -

- -

- - /> -

- -

- - /> -

- -

- - /> -

- -

- - /> -

diff --git a/app/views/settings/_rocketchat_settings.html.erb b/app/views/settings/_rocketchat_settings.html.erb new file mode 100644 index 0000000..caaadf3 --- /dev/null +++ b/app/views/settings/_rocketchat_settings.html.erb @@ -0,0 +1,71 @@ +

+ + +

+ +

+ Generate an "Incoming WebHook" URL from the Integrations configuration page on Rocket.Chat. + This URL can be changed on a per-project basis by creating a project custom field + named "Rocket.Chat URL" (without quotes). +

+ +

+ + +

+ +

+ The channel can be changed on a per-project basis by creating a + project custom field + named "Rocket.Chat Channel" (without quotes). +

+ +

+ + +

+ +

+ + +

+ +

+ + /> +

+ +

+ + /> +

+ +

+ + /> +

+ +

+ + /> +

+ +

+ + /> +

+ +

+ + /> +

+ +

+ + /> +

+ +

+ + /> +

diff --git a/init.rb b/init.rb index e65108f..81e7dd1 100644 --- a/init.rb +++ b/init.rb @@ -1,34 +1,38 @@ require 'redmine' -require_dependency 'redmine_mattermost/listener' +require_dependency 'redmine_rocketchat/listener' -Redmine::Plugin.register :redmine_mattermost do - name 'Redmine Mattermost' - author 'AltSol' - url 'https://github.com/altsol/redmine_mattermost' - author_url 'http://altsol.gr' - description 'Mattermost chat integration' - version '0.6' +Redmine::Plugin.register :redmine_rocketchat do + name 'Redmine Rocket.Chat' + author 'Egon Zemmer' + url 'https://github.com/phlegx/redmine_rocketchat' + author_url 'https://phlegx.com' + description 'Rocket.Chat integration' + version '0.6.1' - requires_redmine :version_or_higher => '2.0.0' + requires_redmine :version_or_higher => '2.0.0' - settings \ - :default => { - 'callback_url' => 'http://example.com/callback/', - 'channel' => nil, - 'icon' => 'https://raw.githubusercontent.com/altsol/redmine_mattermost/assets/icon.png', - 'username' => 'redmine', - 'display_watchers' => 'no', - 'post_updates' => '1', + settings \ + :default => { + 'callback_url' => 'https://rocket.chat/hooks/my_rocket_chat_token', + 'channel' => 'redmine', + 'icon' => 'https://raw.githubusercontent.com/phlegx/redmine_rocketchat/assets/icon.png', + 'username' => 'redmine.bot', + 'display_watchers' => '0', + 'auto_mentions' => '1', + 'post_updates' => '1', + 'new_include_description' => '1', + 'updated_include_description' => '1', 'post_private_issues' => '1', - 'post_private_notes' => '1' - }, - :partial => 'settings/mattermost_settings' + 'post_private_notes' => '1', + 'post_wiki_updates' => '0' + }, + :partial => 'settings/rocketchat_settings' end ActionDispatch::Callbacks.to_prepare do - require_dependency 'issue' - unless Issue.included_modules.include? RedmineMattermost::IssuePatch - Issue.send(:include, RedmineMattermost::IssuePatch) - end + require_dependency 'issue' + unless Issue.included_modules.include? RedmineRocketchat::IssuePatch + Issue.send(:include, RedmineRocketchat::IssuePatch) + end end diff --git a/lib/redmine_mattermost/listener.rb b/lib/redmine_mattermost/listener.rb deleted file mode 100644 index d85f52f..0000000 --- a/lib/redmine_mattermost/listener.rb +++ /dev/null @@ -1,320 +0,0 @@ -require 'httpclient' - -class MattermostListener < Redmine::Hook::Listener - def redmine_mattermost_issues_new_after_save(context={}) - issue = context[:issue] - - channels = channels_for_project issue.project - url = url_for_project issue.project - post_private_issues = post_private_issues_for_project(issue.project) - - return unless channels.present? and url - return if issue.is_private? and post_private_issues != '1' - - msg = "[#{escape issue.project}] #{escape issue.author} created <#{object_url issue}|#{escape issue}>#{mentions issue.description}" - - attachment = {} - attachment[:text] = escape issue.description if issue.description - attachment[:fields] = [{ - :title => I18n.t(:field_status), - :value => escape(issue.status.to_s), - :short => true - }, { - :title => I18n.t(:field_priority), - :value => escape(issue.priority.to_s), - :short => true - }, { - :title => I18n.t(:field_assigned_to), - :value => escape(issue.assigned_to.to_s), - :short => true - }] - - attachment[:fields] << { - :title => I18n.t(:field_watcher), - :value => escape(issue.watcher_users.join(', ')), - :short => true - } if Setting.plugin_redmine_mattermost[:display_watchers] == 'yes' - - speak msg, channels, attachment, url - end - - def redmine_mattermost_issues_edit_after_save(context={}) - issue = context[:issue] - journal = context[:journal] - - channels = channels_for_project issue.project - url = url_for_project issue.project - post_private_issues = post_private_issues_for_project(issue.project) - post_private_notes = post_private_notes_for_project(issue.project) - - return unless channels.present? and url and Setting.plugin_redmine_mattermost[:post_updates] == '1' - return if issue.is_private? and post_private_issues != '1' - return if journal.private_notes? and post_private_notes != '1' - - msg = "[#{escape issue.project}] #{escape journal.user.to_s} updated <#{object_url issue}|#{escape issue}>#{mentions journal.notes}" - - attachment = {} - attachment[:text] = escape journal.notes if journal.notes - attachment[:fields] = journal.details.map { |d| detail_to_field d } - - speak msg, channels, attachment, url - end - - def model_changeset_scan_commit_for_issue_ids_pre_issue_update(context={}) - issue = context[:issue] - journal = issue.current_journal - changeset = context[:changeset] - - channels = channels_for_project issue.project - url = url_for_project issue.project - post_private_issues = post_private_issues_for_project(issue.project) - - return unless channels.present? and url and issue.save - return if issue.is_private? and post_private_issues != '1' - - msg = "[#{escape issue.project}] #{escape journal.user.to_s} updated <#{object_url issue}|#{escape issue}>" - - repository = changeset.repository - - if Setting.host_name.to_s =~ /\A(https?\:\/\/)?(.+?)(\:(\d+))?(\/.+)?\z/i - host, port, prefix = $2, $4, $5 - revision_url = Rails.application.routes.url_for( - :controller => 'repositories', - :action => 'revision', - :id => repository.project, - :repository_id => repository.identifier_param, - :rev => changeset.revision, - :host => host, - :protocol => Setting.protocol, - :port => port, - :script_name => prefix - ) - else - revision_url = Rails.application.routes.url_for( - :controller => 'repositories', - :action => 'revision', - :id => repository.project, - :repository_id => repository.identifier_param, - :rev => changeset.revision, - :host => Setting.host_name, - :protocol => Setting.protocol - ) - end - - attachment = {} - attachment[:text] = ll(Setting.default_language, :text_status_changed_by_changeset, "<#{revision_url}|#{escape changeset.comments}>") - attachment[:fields] = journal.details.map { |d| detail_to_field d } - - speak msg, channels, attachment, url - end - - def controller_wiki_edit_after_save(context = { }) - return unless Setting.plugin_redmine_mattermost[:post_wiki_updates] == '1' - - project = context[:project] - page = context[:page] - - user = page.content.author - project_url = "<#{object_url project}|#{escape project}>" - page_url = "<#{object_url page}|#{page.title}>" - comment = "[#{project_url}] #{page_url} updated by *#{user}*" - - channels = channels_for_project project - url = url_for_project project - - return unless channels.present? and url - - attachment = nil - unless page.content.comments.empty? - attachment = {} - attachment[:text] = "#{escape page.content.comments}" - end - - speak comment, channels, attachment, url - end - - def speak(msg, channels, attachment=nil, url=nil) - return if channels.blank? - - url = Setting.plugin_redmine_mattermost[:mattermost_url] unless url - username = Setting.plugin_redmine_mattermost[:username] - icon = Setting.plugin_redmine_mattermost[:icon] - - params = { - :text => msg, - :link_names => 1, - } - - params[:username] = username if username - - - params[:attachments] = [attachment] if attachment - - if icon and not icon.empty? - if icon.start_with? ':' - params[:icon_emoji] = icon - else - params[:icon_url] = icon - end - end - - channels.each do |channel| - params[:channel] = channel - - begin - client = HTTPClient.new - client.ssl_config.cert_store.set_default_paths - client.ssl_config.ssl_version = :auto - client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE - client.post_async url, {:payload => params.to_json} - rescue Exception => e - Rails.logger.warn("cannot connect to #{url}") - Rails.logger.warn(e) - end - end - end - -private - def escape(msg) - msg.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">") - end - - def object_url(obj) - if Setting.host_name.to_s =~ /\A(https?\:\/\/)?(.+?)(\:(\d+))?(\/.+)?\z/i - host, port, prefix = $2, $4, $5 - Rails.application.routes.url_for(obj.event_url({:host => host, :protocol => Setting.protocol, :port => port, :script_name => prefix})) - else - Rails.application.routes.url_for(obj.event_url({:host => Setting.host_name, :protocol => Setting.protocol})) - end - end - - def url_for_project(proj) - return nil if proj.blank? - - cf = ProjectCustomField.find_by_name("Mattermost URL") - - [ - (proj.custom_value_for(cf).value rescue nil), - (url_for_project proj.parent), - Setting.plugin_redmine_mattermost[:mattermost_url], - ].flatten.find(&:present?) - end - - def post_private_issues_for_project(proj) - return nil if proj.blank? - - cf = ProjectCustomField.find_by_name("Mattermost Post private issues") - [ - (proj.custom_value_for(cf).value rescue nil), - (post_private_issues_for_project proj.parent), - Setting.plugin_redmine_mattermost[:post_private_issues], - ].flatten.find(&:present?) - end - - def post_private_notes_for_project(proj) - return nil if proj.blank? - - cf = ProjectCustomField.find_by_name("Mattermost Post private notes") - [ - (proj.custom_value_for(cf).value rescue nil), - (post_private_notes_for_project proj.parent), - Setting.plugin_redmine_mattermost[:post_private_notes], - ].flatten.find(&:present?) - end - - def channels_for_project(proj) - return nil if proj.blank? - - cf = ProjectCustomField.find_by_name("Mattermost Channel") - val = [ - (proj.custom_value_for(cf).value rescue nil), - (channels_for_project proj.parent), - Setting.plugin_redmine_mattermost[:channel], - ].flatten.find(&:present?) - - # Channel name '-' or empty '' is reserved for NOT notifying - return [] if val.nil? or val.to_s == '' - return [] if val.to_s == '-' - return val.split(",") if val.is_a? String - val - end - - def detail_to_field(detail) - field_format = nil - - if detail.property == "cf" - key = CustomField.find(detail.prop_key).name rescue nil - title = key - field_format = CustomField.find(detail.prop_key).field_format rescue nil - elsif detail.property == "attachment" - key = "attachment" - title = I18n.t :label_attachment - else - key = detail.prop_key.to_s.sub("_id", "") - title = I18n.t "field_#{key}" - end - - short = true - value = escape detail.value.to_s - - case key - when "title", "subject", "description" - short = false - when "tracker" - tracker = Tracker.find(detail.value) rescue nil - value = escape tracker.to_s - when "project" - project = Project.find(detail.value) rescue nil - value = escape project.to_s - when "status" - status = IssueStatus.find(detail.value) rescue nil - value = escape status.to_s - when "priority" - priority = IssuePriority.find(detail.value) rescue nil - value = escape priority.to_s - when "category" - category = IssueCategory.find(detail.value) rescue nil - value = escape category.to_s - when "assigned_to" - user = User.find(detail.value) rescue nil - value = escape user.to_s - when "fixed_version" - version = Version.find(detail.value) rescue nil - value = escape version.to_s - when "attachment" - attachment = Attachment.find(detail.prop_key) rescue nil - value = "<#{object_url attachment}|#{escape attachment.filename}>" if attachment - when "parent" - issue = Issue.find(detail.value) rescue nil - value = "<#{object_url issue}|#{escape issue}>" if issue - end - - case field_format - when "version" - version = Version.find(detail.value) rescue nil - value = escape version.to_s - end - - value = "-" if value.empty? - - result = { :title => title, :value => value } - result[:short] = true if short - result - end - - def mentions text - return nil if text.nil? - names = extract_usernames text - names.present? ? "\nTo: " + names.join(', ') : nil - end - - def extract_usernames text = '' - if text.nil? - text = '' - end - - # mattermost usernames may only contain lowercase letters, numbers, - # dashes and underscores and must start with a letter or number. - text.scan(/@[a-z0-9][a-z0-9_\-]*/).uniq - end -end diff --git a/lib/redmine_mattermost/issue_patch.rb b/lib/redmine_rocketchat/issue_patch.rb similarity index 80% rename from lib/redmine_mattermost/issue_patch.rb rename to lib/redmine_rocketchat/issue_patch.rb index fbfdfec..b418611 100644 --- a/lib/redmine_mattermost/issue_patch.rb +++ b/lib/redmine_rocketchat/issue_patch.rb @@ -1,4 +1,4 @@ -module RedmineMattermost +module RedmineRocketchat module IssuePatch def self.included(base) # :nodoc: base.extend(ClassMethods) @@ -10,24 +10,24 @@ module RedmineMattermost after_save :save_from_issue end end - + module ClassMethods end - + module InstanceMethods def create_from_issue @create_already_fired = true - Redmine::Hook.call_hook(:redmine_mattermost_issues_new_after_save, { :issue => self}) + Redmine::Hook.call_hook(:redmine_rocketchat_issues_new_after_save, { :issue => self}) return true end def save_from_issue unless @create_already_fired - Redmine::Hook.call_hook(:redmine_mattermost_issues_edit_after_save, {:issue => self, :journal => self.current_journal}) unless self.current_journal.nil? + Redmine::Hook.call_hook(:redmine_rocketchat_issues_edit_after_save, {:issue => self, :journal => self.current_journal}) unless self.current_journal.nil? end return true end - end + end end end diff --git a/lib/redmine_rocketchat/listener.rb b/lib/redmine_rocketchat/listener.rb new file mode 100644 index 0000000..bcd882b --- /dev/null +++ b/lib/redmine_rocketchat/listener.rb @@ -0,0 +1,320 @@ +require 'httpclient' + +class RocketchatListener < Redmine::Hook::Listener + def redmine_rocketchat_issues_new_after_save(context={}) + issue = context[:issue] + + channels = channels_for_project issue.project + url = url_for_project issue.project + post_private_issues = post_private_issues_for_project(issue.project) + + return unless channels.present? and url + return if issue.is_private? and post_private_issues != '1' + + msg = "[#{escape issue.project}] #{escape issue.author} created <#{object_url issue}|#{escape issue}>#{mentions issue.description if Setting.plugin_redmine_rocketchat[:auto_mentions] == '1'}" + + attachment = {} + attachment[:text] = escape issue.description if issue.description and Setting.plugin_redmine_rocketchat[:new_include_description] == '1' + attachment[:fields] = [{ + :title => I18n.t(:field_status), + :value => escape(issue.status.to_s), + :short => true + }, { + :title => I18n.t(:field_priority), + :value => escape(issue.priority.to_s), + :short => true + }, { + :title => I18n.t(:field_assigned_to), + :value => escape(issue.assigned_to.to_s), + :short => true + }] + + attachment[:fields] << { + :title => I18n.t(:field_watcher), + :value => escape(issue.watcher_users.join(', ')), + :short => true + } if Setting.plugin_redmine_rocketchat[:display_watchers] == '1' + + speak msg, channels, attachment, url + end + + def redmine_rocketchat_issues_edit_after_save(context={}) + issue = context[:issue] + journal = context[:journal] + + channels = channels_for_project issue.project + url = url_for_project issue.project + post_private_issues = post_private_issues_for_project(issue.project) + post_private_notes = post_private_notes_for_project(issue.project) + + return unless channels.present? and url and Setting.plugin_redmine_rocketchat[:post_updates] == '1' + return if issue.is_private? and post_private_issues != '1' + return if journal.private_notes? and post_private_notes != '1' + + msg = "[#{escape issue.project}] #{escape journal.user.to_s} updated <#{object_url issue}|#{escape issue}>#{mentions journal.notes if Setting.plugin_redmine_rocketchat[:auto_mentions] == '1'}" + + attachment = {} + attachment[:text] = escape journal.notes if journal.notes and Setting.plugin_redmine_rocketchat[:updated_include_description] == '1' + attachment[:fields] = journal.details.map { |d| detail_to_field d } + + speak msg, channels, attachment, url + end + + def model_changeset_scan_commit_for_issue_ids_pre_issue_update(context={}) + issue = context[:issue] + journal = issue.current_journal + changeset = context[:changeset] + + channels = channels_for_project issue.project + url = url_for_project issue.project + post_private_issues = post_private_issues_for_project(issue.project) + + return unless channels.present? and url and issue.save + return if issue.is_private? and post_private_issues != '1' + + msg = "[#{escape issue.project}] #{escape journal.user.to_s} updated <#{object_url issue}|#{escape issue}>" + + repository = changeset.repository + + if Setting.host_name.to_s =~ /\A(https?\:\/\/)?(.+?)(\:(\d+))?(\/.+)?\z/i + host, port, prefix = $2, $4, $5 + revision_url = Rails.application.routes.url_for( + :controller => 'repositories', + :action => 'revision', + :id => repository.project, + :repository_id => repository.identifier_param, + :rev => changeset.revision, + :host => host, + :protocol => Setting.protocol, + :port => port, + :script_name => prefix + ) + else + revision_url = Rails.application.routes.url_for( + :controller => 'repositories', + :action => 'revision', + :id => repository.project, + :repository_id => repository.identifier_param, + :rev => changeset.revision, + :host => Setting.host_name, + :protocol => Setting.protocol + ) + end + + attachment = {} + attachment[:text] = ll(Setting.default_language, :text_status_changed_by_changeset, "<#{revision_url}|#{escape changeset.comments}>") + attachment[:fields] = journal.details.map { |d| detail_to_field d } + + speak msg, channels, attachment, url + end + + def controller_wiki_edit_after_save(context = { }) + return unless Setting.plugin_redmine_rocketchat[:post_wiki_updates] == '1' + + project = context[:project] + page = context[:page] + + user = page.content.author + project_url = "<#{object_url project}|#{escape project}>" + page_url = "<#{object_url page}|#{page.title}>" + comment = "[#{project_url}] #{page_url} updated by *#{user}*" + + channels = channels_for_project project + url = url_for_project project + + return unless channels.present? and url + + attachment = nil + unless page.content.comments.empty? + attachment = {} + attachment[:text] = "#{escape page.content.comments}" + end + + speak comment, channels, attachment, url + end + + def speak(msg, channels, attachment=nil, url=nil) + return if channels.blank? + + url = Setting.plugin_redmine_rocketchat[:rocketchat_url] unless url + username = Setting.plugin_redmine_rocketchat[:username] + icon = Setting.plugin_redmine_rocketchat[:icon] + + params = { + :text => msg, + :link_names => 1, + } + + params[:username] = username if username + + + params[:attachments] = [attachment] if attachment + + if icon and not icon.empty? + if icon.start_with? ':' + params[:icon_emoji] = icon + else + params[:icon_url] = icon + end + end + + channels.each do |channel| + params[:channel] = channel + + begin + client = HTTPClient.new + client.ssl_config.cert_store.set_default_paths + client.ssl_config.ssl_version = :auto + client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE + client.post_async url, {:payload => params.to_json} + rescue Exception => e + Rails.logger.warn("cannot connect to #{url}") + Rails.logger.warn(e) + end + end + end + +private + def escape(msg) + msg.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>') + end + + def object_url(obj) + if Setting.host_name.to_s =~ /\A(https?\:\/\/)?(.+?)(\:(\d+))?(\/.+)?\z/i + host, port, prefix = $2, $4, $5 + Rails.application.routes.url_for(obj.event_url({:host => host, :protocol => Setting.protocol, :port => port, :script_name => prefix})) + else + Rails.application.routes.url_for(obj.event_url({:host => Setting.host_name, :protocol => Setting.protocol})) + end + end + + def url_for_project(proj) + return nil if proj.blank? + + cf = ProjectCustomField.find_by_name('Rocket.Chat URL') + + [ + (proj.custom_value_for(cf).value rescue nil), + (url_for_project proj.parent), + Setting.plugin_redmine_rocketchat[:rocketchat_url], + ].flatten.find(&:present?) + end + + def post_private_issues_for_project(proj) + return nil if proj.blank? + + cf = ProjectCustomField.find_by_name('Rocket.Chat Post private issues') + [ + (proj.custom_value_for(cf).value rescue nil), + (post_private_issues_for_project proj.parent), + Setting.plugin_redmine_rocketchat[:post_private_issues], + ].flatten.find(&:present?) + end + + def post_private_notes_for_project(proj) + return nil if proj.blank? + + cf = ProjectCustomField.find_by_name('Rocket.Chat Post private notes') + [ + (proj.custom_value_for(cf).value rescue nil), + (post_private_notes_for_project proj.parent), + Setting.plugin_redmine_rocketchat[:post_private_notes], + ].flatten.find(&:present?) + end + + def channels_for_project(proj) + return nil if proj.blank? + + cf = ProjectCustomField.find_by_name('Rocket.Chat Channel') + val = [ + (proj.custom_value_for(cf).value rescue nil), + (channels_for_project proj.parent), + Setting.plugin_redmine_rocketchat[:channel], + ].flatten.find(&:present?) + + # Channel name '-' or empty '' is reserved for NOT notifying + return [] if val.nil? or val.to_s == '' + return [] if val.to_s == '-' + return val.split(",") if val.is_a? String + val + end + + def detail_to_field(detail) + field_format = nil + + if detail.property == "cf" + key = CustomField.find(detail.prop_key).name rescue nil + title = key + field_format = CustomField.find(detail.prop_key).field_format rescue nil + elsif detail.property == 'attachment' + key = 'attachment' + title = I18n.t :label_attachment + else + key = detail.prop_key.to_s.sub('_id', '') + title = I18n.t "field_#{key}" + end + + short = true + value = escape detail.value.to_s + + case key + when 'title', 'subject', 'description' + short = false + when 'tracker' + tracker = Tracker.find(detail.value) rescue nil + value = escape tracker.to_s + when 'project' + project = Project.find(detail.value) rescue nil + value = escape project.to_s + when 'status' + status = IssueStatus.find(detail.value) rescue nil + value = escape status.to_s + when 'priority' + priority = IssuePriority.find(detail.value) rescue nil + value = escape priority.to_s + when 'category' + category = IssueCategory.find(detail.value) rescue nil + value = escape category.to_s + when 'assigned_to' + user = User.find(detail.value) rescue nil + value = escape user.to_s + when 'fixed_version' + version = Version.find(detail.value) rescue nil + value = escape version.to_s + when 'attachment' + attachment = Attachment.find(detail.prop_key) rescue nil + value = "<#{object_url attachment}|#{escape attachment.filename}>" if attachment + when 'parent' + issue = Issue.find(detail.value) rescue nil + value = "<#{object_url issue}|#{escape issue}>" if issue + end + + case field_format + when 'version' + version = Version.find(detail.value) rescue nil + value = escape version.to_s + end + + value = '-' if value.empty? + + result = { :title => title, :value => value } + result[:short] = true if short + result + end + + def mentions text + return nil if text.nil? + names = extract_usernames text + names.present? ? '\nTo: ' + names.join(', ') : nil + end + + def extract_usernames text = '' + if text.nil? + text = '' + end + + # rocketchat usernames may only contain lowercase letters, numbers, + # dashes, dots and underscores and must start with a letter or number. + text.scan(/@[a-z0-9][a-z0-9_\-.]*/).uniq + end +end