From 3d554788ed8a0db427749d5339536d1a5fe0925a Mon Sep 17 00:00:00 2001 From: Thanos Kyritsis Date: Wed, 30 Dec 2015 10:39:03 +0200 Subject: [PATCH] initial import --- Gemfile | 3 + .../settings/_mattermost_settings.html.erb | 37 +++ lib/redmine_mattermost/listener.rb | 225 ++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 Gemfile create mode 100644 app/views/settings/_mattermost_settings.html.erb create mode 100644 lib/redmine_mattermost/listener.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..9fcac66 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem "httpclient" diff --git a/app/views/settings/_mattermost_settings.html.erb b/app/views/settings/_mattermost_settings.html.erb new file mode 100644 index 0000000..ed5f31e --- /dev/null +++ b/app/views/settings/_mattermost_settings.html.erb @@ -0,0 +1,37 @@ +

+ + +

+ +

+ + +

+ +

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

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + /> +

diff --git a/lib/redmine_mattermost/listener.rb b/lib/redmine_mattermost/listener.rb new file mode 100644 index 0000000..a4164f2 --- /dev/null +++ b/lib/redmine_mattermost/listener.rb @@ -0,0 +1,225 @@ +require 'httpclient' + +class MattermostListener < Redmine::Hook::Listener + def controller_issues_new_after_save(context={}) + issue = context[:issue] + + channel = channel_for_project issue.project + url = url_for_project issue.project + + return unless channel and url + return if issue.is_private? + + 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, channel, attachment, url + end + + def controller_issues_edit_after_save(context={}) + issue = context[:issue] + journal = context[:journal] + + channel = channel_for_project issue.project + url = url_for_project issue.project + + return unless channel and url and Setting.plugin_redmine_mattermost[:post_updates] == '1' + return if issue.is_private? + + 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, channel, 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] + + channel = channel_for_project issue.project + url = url_for_project issue.project + + return unless channel and url and issue.save + return if issue.is_private? + + msg = "[#{escape issue.project}] #{escape journal.user.to_s} updated <#{object_url issue}|#{escape issue}>" + + repository = changeset.repository + + 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 + ) + + 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, channel, attachment, url + end + + def speak(msg, channel, attachment=nil, url=nil) + url = Setting.plugin_redmine_mattermost[:mattermost_url] if not 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[:channel] = channel if channel + + 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 + + begin + client = HTTPClient.new + client.ssl_config.cert_store.set_default_paths + client.ssl_config.ssl_version = "SSLv23" + client.post_async url, {:payload => params.to_json} + rescue + # Bury exception if connection error + end + end + +private + def escape(msg) + msg.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">") + end + + def object_url(obj) + Rails.application.routes.url_for(obj.event_url({:host => Setting.host_name, :protocol => Setting.protocol})) + end + + def url_for_project(proj) + return nil if proj.blank? + + cf = ProjectCustomField.find_by_name("Mattermost URL") + + return [ + (proj.custom_value_for(cf).value rescue nil), + (url_for_project proj.parent), + Setting.plugin_redmine_mattermost[:mattermost_url], + ].find{|v| v.present?} + end + + def channel_for_project(proj) + return nil if proj.blank? + + cf = ProjectCustomField.find_by_name("Mattermost Channel") + + val = [ + (proj.custom_value_for(cf).value rescue nil), + (channel_for_project proj.parent), + Setting.plugin_redmine_mattermost[:channel], + ].find{|v| v.present?} + + # Channel name '-' is reserved for NOT notifying + return nil if val.to_s == '-' + val + end + + def detail_to_field(detail) + if detail.property == "cf" + key = CustomField.find(detail.prop_key).name rescue nil + title = key + 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 + + value = "-" if value.empty? + + result = { :title => title, :value => value } + result[:short] = true if short + result + end + + def mentions text + names = extract_usernames text + names.present? ? "\nTo: " + names.join(', ') : nil + end + + def extract_usernames text = '' + # 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