From d3ff150dfb8072a13c09262c976e7b7b3a107b9e Mon Sep 17 00:00:00 2001 From: Felipe Batista Date: Tue, 21 May 2019 18:15:47 +0200 Subject: [PATCH] Rewrite the plugin from scratch. --- LICENSE | 201 ++++++++ README.md | 49 +- .../projects/settings/_redmine_zulip.html.erb | 21 + app/views/settings/_redmine_zulip.html.erb | 14 + config/locales/en.yml | 56 +++ config/locales/fr.yml | 54 +++ config/locales/pt-BR.yml | 54 +++ ...0130920172651_add_zulip_auth_to_project.rb | 0 ...70051_upgrade_zulip_settings_to_project.rb | 9 + init.rb | 16 + lib/redmine_zulip.rb | 16 + lib/redmine_zulip/api.rb | 47 ++ lib/redmine_zulip/issue_patch.rb | 429 ++++++++++++++++++ lib/redmine_zulip/project_patch.rb | 14 + lib/redmine_zulip/project_settings_tabs.rb | 13 + .../views/projects/_redmine_zulip.html.erb | 14 - .../views/settings/_redmine_zulip.html.erb | 24 - redmine_zulip/config/locales/en.yml | 10 - redmine_zulip/init.rb | 24 - redmine_zulip/lib/project_patch.rb | 11 - redmine_zulip/lib/version.rb | 3 - redmine_zulip/lib/zulip_hooks.rb | 132 ------ redmine_zulip/lib/zulip_view_hooks.rb | 3 - 23 files changed, 989 insertions(+), 225 deletions(-) create mode 100644 LICENSE create mode 100644 app/views/projects/settings/_redmine_zulip.html.erb create mode 100644 app/views/settings/_redmine_zulip.html.erb create mode 100644 config/locales/en.yml create mode 100644 config/locales/fr.yml create mode 100644 config/locales/pt-BR.yml rename {redmine_zulip/db => db}/migrate/20130920172651_add_zulip_auth_to_project.rb (100%) create mode 100644 db/migrate/20190408170051_upgrade_zulip_settings_to_project.rb create mode 100644 init.rb create mode 100644 lib/redmine_zulip.rb create mode 100644 lib/redmine_zulip/api.rb create mode 100644 lib/redmine_zulip/issue_patch.rb create mode 100644 lib/redmine_zulip/project_patch.rb create mode 100644 lib/redmine_zulip/project_settings_tabs.rb delete mode 100644 redmine_zulip/app/views/projects/_redmine_zulip.html.erb delete mode 100644 redmine_zulip/app/views/settings/_redmine_zulip.html.erb delete mode 100644 redmine_zulip/config/locales/en.yml delete mode 100644 redmine_zulip/init.rb delete mode 100644 redmine_zulip/lib/project_patch.rb delete mode 100644 redmine_zulip/lib/version.rb delete mode 100644 redmine_zulip/lib/zulip_hooks.rb delete mode 100644 redmine_zulip/lib/zulip_view_hooks.rb diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c4c3a68 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "{}" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright 2019 Felipe Batista + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index dbb4365..799fa5e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,47 @@ -Redmine Zulip plugin -==================== +# Redmine Zulip Plugin -Get notifications about new and updated issues directly in Zulip! +Get Zulip notifications for your Redmine issues! -Please visit https://zulipchat.com/integrations/#redmine for detailed installation instructions. +## Installing + +0. Please navigate to your Redmine instance's root directory by running: + +```sh +cd /path/to/redmine +``` + +1. Clone the **Redmine Zulip Plugin** into `plugins` directory + +```sh +git clone https://github.com/zulip/zulip-redmine-plugin.git plugins/redmine_zulip +``` + +2. **Restart** your Redmine instance + +3. Update the Redmine database by running: + +```sh +rake redmine:plugins:migrate +``` + +## Configuring plugin settings + +#### Global settings + +Log into your Redmine instance, click on **Administration** in the top-left corner, then click on **Plugins**. + +Find the **Redmine Zulip** plugin, and click **Configure**. You must now set the following: + +* Zulip URL (e.g `https://yourZulipDomain.zulipchat.com/`) +* Zulip Bot E-mail +* Zulip Bot API key + + +#### Project settings + +Go to your project's **Settings** page, and select the **Zulip** tab. Now, you may: + +* Specify the Zulip stream +* Enable/disable private notifications on tasks assignments +* Enable/disable notifications on issues update +* Enable/disable notifications on milestone progress diff --git a/app/views/projects/settings/_redmine_zulip.html.erb b/app/views/projects/settings/_redmine_zulip.html.erb new file mode 100644 index 0000000..881ce24 --- /dev/null +++ b/app/views/projects/settings/_redmine_zulip.html.erb @@ -0,0 +1,21 @@ +<%= form_for @project do |f| %> +
+

+ <%= content_tag(:label, l(:field_zulip_stream)) %> + <%= f.text_field :zulip_stream %> +

+

+ <%= content_tag(:label, l(:field_zulip_private_messages)) %> + <%= f.check_box :zulip_private_messages %> +

+

+ <%= content_tag(:label, l(:field_zulip_subject_issue)) %> + <%= f.check_box :zulip_subject_issue %> +

+

+ <%= content_tag(:label, l(:field_zulip_subject_version)) %> + <%= f.check_box :zulip_subject_version %> +

+
+ <%= f.submit "Save" %> +<% end %> diff --git a/app/views/settings/_redmine_zulip.html.erb b/app/views/settings/_redmine_zulip.html.erb new file mode 100644 index 0000000..e5c2ea7 --- /dev/null +++ b/app/views/settings/_redmine_zulip.html.erb @@ -0,0 +1,14 @@ +

+ <%= content_tag(:label, l(:zulip_settings_label_url)) %> + <%= text_field_tag 'settings[zulip_url]', @settings["zulip_url"] %> +

+ +

+ <%= content_tag(:label, l(:zulip_settings_label_email)) %> + <%= text_field_tag 'settings[zulip_email]', @settings["zulip_email"] %> +

+ +

+ <%= content_tag(:label, l(:zulip_settings_label_api_key)) %> + <%= text_field_tag 'settings[zulip_api_key]', @settings["zulip_api_key"] %> +

diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..5b4e70b --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,56 @@ +en: + label_redmine_zulip: Zulip + zulip_settings_header: Zulip Plugin Configuration + zulip_settings_label_url: Zulip URL + zulip_settings_label_email: Zulip Bot Email + zulip_settings_label_api_key: Zulip Bot API key + field_zulip_stream: Stream name + field_zulip_private_messages: Get notified on assignments + field_zulip_subject_issue: Get notified on issues update + field_zulip_subject_version: Get notified on milestone progress + zulip_notify_assignment: > + %{user} **assigned** issue [%{id}: %{subject}](%{url}) to **you**: + + + ~~~quote + + %{description} + + ~~~ + + + * **%{status_label}**: %{status} + + + zulip_notify_unassignment: > + %{user} **unassigned** issue [%{id}: %{subject}](%{url}) from **you**. + + zulip_notify_updated: > + %{user} **updated** issue [%{id}: %{subject}](%{url}). + + zulip_notify_destroyed: > + %{user} **deleted** issue **%{id}: %{subject}**. + + zulip_init_issue_subject: > + %{user} **created** issue [%{id}: %{subject}](%{url}): + + + ~~~quote + + %{description} + + ~~~ + + * **%{assigned_to_label}**: %{assigned_to} + + * **%{status_label}**: %{status} + + zulip_update_version_subject_added: > + %{user} **added** issue [%{id}: %{subject}](%{url}) to version **%{fixed_version}**. + + zulip_update_version_subject_removed: > + %{user} **removed** issue [%{id}: %{subject}](%{url}) from version **%{fixed_version}**. + + zulip_update_version_subject_status: > + %{user} **changed status** of issue [%{id}: %{subject}](%{url}) from + *~~%{previous_status}~~* to **%{current_status}**. diff --git a/config/locales/fr.yml b/config/locales/fr.yml new file mode 100644 index 0000000..5f2b1d0 --- /dev/null +++ b/config/locales/fr.yml @@ -0,0 +1,54 @@ +fr: + zulip_settings_header: Configuration du Zulip Plugin + zulip_settings_label_url: URL de l'instance Zulip + zulip_settings_label_email: E-mail du Robot Zulip + zulip_settings_label_api_key: API key du Robot Zulip + field_zulip_stream: Nom du canal + field_zulip_private_messages: Notifier les attributions de demandes + field_zulip_subject_issue: Notifier la mise à jour des demandes + field_zulip_subject_version: Notifier l'avancement des versions + zulip_notify_assignment: > + %{user} **vous a assigné** la demande [%{id}: %{subject}](%{url}) : + + + ~~~quote + + %{description} + + ~~~ + + + * **%{status_label}**: %{status} + + zulip_notify_unassignment: > + %{user} **vous a désattributé** la demande [%{id}: %{subject}](%{url}). + + zulip_notify_updated: > + %{user} **a mis à jour** la demande [%{id}: %{subject}](%{url}). + + zulip_notify_destroyed: > + %{user} **a supprimé** la demande **%{id}: %{subject}**. + + zulip_init_issue_subject: > + %{user} **a créé** la demande [%{id}: %{subject}](%{url}) : + + + ~~~quote + + %{description} + + ~~~ + + * **%{assigned_to_label}**: %{assigned_to} + + * **%{status_label}**: %{status} + + zulip_update_version_subject_added: > + %{user} **a ajouté** la demande [%{id}: %{subject}](%{url}) à la version **%{fixed_version}**. + + zulip_update_version_subject_removed: > + %{user} **a retiré** la demande [%{id}: %{subject}](%{url}) de la version **%{fixed_version}**. + + zulip_update_version_subject_status: > + %{user} **a mis a jour le statut** de la demande [%{id}: %{subject}](%{url}) de + *~~%{previous_status}~~* à **%{current_status}**. diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml new file mode 100644 index 0000000..74019dc --- /dev/null +++ b/config/locales/pt-BR.yml @@ -0,0 +1,54 @@ +pt-BR: + zulip_settings_header: Configuração do Zulip Plugin + zulip_settings_label_url: URL da instância Zulip + zulip_settings_label_email: E-mail do Bot no Zulip + zulip_settings_label_api_key: API key do Bot no Zulip + field_zulip_stream: Nome do fluxo + field_zulip_private_messages: Notificar atribuições + field_zulip_subject_issue: Notificar a atualização de tarefas + field_zulip_subject_version: Notifier o avanço das milestones + zulip_notify_assignment: > + %{user} **atribuiu** a tarefa [%{id}: %{subject}](%{url}) para **você**: + + + ~~~quote + + %{description} + + ~~~ + + + * **%{status_label}**: %{status} + + zulip_notify_unassignment: > + %{user} **desatribuiu** a tarefa [%{id}: %{subject}](%{url}) de **você**. + + zulip_notify_updated: > + %{user} **modificou** a tarefa [%{id}: %{subject}](%{url}). + + zulip_notify_destroyed: > + %{user} **excluiu** a tarefa **%{id}: %{subject}**. + + zulip_init_issue_subject: > + %{user} **criou** a tarefa [%{id}: %{subject}](%{url}): + + + ~~~quote + + %{description} + + ~~~ + + * **%{assigned_to_label}**: %{assigned_to} + + * **%{status_label}**: %{status} + + zulip_update_version_subject_added: > + %{user} **adicionou** a tarefa [%{id}: %{subject}](%{url}) à versão **%{fixed_version}**. + + zulip_update_version_subject_removed: > + %{user} **removeu** a tarefa [%{id}: %{subject}](%{url}) da versão **%{fixed_version}**. + + zulip_update_version_subject_status: > + %{user} **modificou o status** da tarefa [%{id}: %{subject}](%{url}) de + *~~%{previous_status}~~* para **%{current_status}**. diff --git a/redmine_zulip/db/migrate/20130920172651_add_zulip_auth_to_project.rb b/db/migrate/20130920172651_add_zulip_auth_to_project.rb similarity index 100% rename from redmine_zulip/db/migrate/20130920172651_add_zulip_auth_to_project.rb rename to db/migrate/20130920172651_add_zulip_auth_to_project.rb diff --git a/db/migrate/20190408170051_upgrade_zulip_settings_to_project.rb b/db/migrate/20190408170051_upgrade_zulip_settings_to_project.rb new file mode 100644 index 0000000..0baf9e6 --- /dev/null +++ b/db/migrate/20190408170051_upgrade_zulip_settings_to_project.rb @@ -0,0 +1,9 @@ +class UpgradeZulipSettingsToProject < ActiveRecord::Migration + def change + remove_column :projects, :zulip_email, :string + remove_column :projects, :zulip_api_key, :string + add_column :projects, :zulip_private_messages, :boolean, null: false, default: false + add_column :projects, :zulip_subject_issue, :boolean, null: false, default: false + add_column :projects, :zulip_subject_version, :boolean, null: false, default: false + end +end diff --git a/init.rb b/init.rb new file mode 100644 index 0000000..f575eeb --- /dev/null +++ b/init.rb @@ -0,0 +1,16 @@ +require "redmine_zulip" + +Redmine::Plugin.register :redmine_zulip do + name 'Zulip' + author 'Zulip, Inc.' + description 'Sends notifications to Zulip.' + version RedmineZulip::VERSION + url 'https://github.com/zulip/zulip-redmine-plugin' + author_url 'https://www.zulip.org/' + + settings partial: "settings/redmine_zulip", default: { + "zulip_url" => "", + "zulip_email" => "", + "zulip_api_key" => "" + } +end diff --git a/lib/redmine_zulip.rb b/lib/redmine_zulip.rb new file mode 100644 index 0000000..5d80b36 --- /dev/null +++ b/lib/redmine_zulip.rb @@ -0,0 +1,16 @@ +require "redmine_zulip/issue_patch" + +module RedmineZulip + VERSION = "2.0" + + def self.api + RedmineZulip::Api.new + end +end + + +Rails.configuration.to_prepare do + Issue.send(:include, RedmineZulip::IssuePatch) + Project.send(:include, RedmineZulip::ProjectPatch) + ProjectsController.send(:helper, RedmineZulip::ProjectSettingsTabs) +end diff --git a/lib/redmine_zulip/api.rb b/lib/redmine_zulip/api.rb new file mode 100644 index 0000000..bcd48b0 --- /dev/null +++ b/lib/redmine_zulip/api.rb @@ -0,0 +1,47 @@ +module RedmineZulip + class Api + attr_reader :url, :email, :key + + def initialize + @url = "#{Setting.plugin_redmine_zulip["zulip_url"]}/api/v1" + @key = Setting.plugin_redmine_zulip["zulip_api_key"] + @email = Setting.plugin_redmine_zulip["zulip_email"] + end + + def configured? + url.present? && email.present? && key.present? + end + + def messages + RedmineZulip::Api::Messages.new(self) + end + + private + + class Messages + attr_reader :api + + def initialize(api) + @api = api + end + + def send(type:, to:, content:, subject: nil) + form_data = { + "type" => type, + "to" => to, + "content" => content + } + form_data["subject"] = subject if subject.present? + uri = URI("#{api.url}/messages") + req = Net::HTTP::Post.new(uri) + req.basic_auth(api.email, api.key) + req["User-Agent"] = "ZulipRedminePlugin/#{RedmineZulip::VERSION}" + req.set_form_data(form_data) + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(req) + end + res.code == "200" + end + end + end +end diff --git a/lib/redmine_zulip/issue_patch.rb b/lib/redmine_zulip/issue_patch.rb new file mode 100644 index 0000000..eb43d60 --- /dev/null +++ b/lib/redmine_zulip/issue_patch.rb @@ -0,0 +1,429 @@ +module RedmineZulip + module IssuePatch + extend ActiveSupport::Concern + + TWEET_SIZE = 140 + + included do + after_commit :notify_assignment, if: proc { |issue| + RedmineZulip.api.configured? && + issue.project.zulip_private_messages && + issue.assigned_to_id.present? && + issue.assigned_to_id != User.current.id && + issue.previous_changes.keys.include?("assigned_to_id") + } + + after_commit :notify_unassignment, if: proc { |issue| + RedmineZulip.api.configured? && + issue.project.zulip_private_messages && + issue.previous_changes.keys.include?("assigned_to_id") && + issue.previous_changes["assigned_to_id"].first.present? && + issue.previous_changes["assigned_to_id"].first != User.current.id + } + + after_commit :notify_assigned_to_issue_updated, if: proc { |issue| + RedmineZulip.api.configured? && + !issue.destroyed? && + issue.project.zulip_private_messages && + !issue.previous_changes.include?("id") && + !issue.previous_changes.include?("assigned_to_id") && + issue.assigned_to_id.present? && + issue.assigned_to_id != User.current.id + } + + after_commit :notify_assigned_to_issue_destroyed, if: proc { |issue| + RedmineZulip.api.configured? && + issue.destroyed? && + issue.project.zulip_private_messages && + issue.assigned_to_id.present? && + issue.assigned_to_id != User.current.id + } + + after_commit :init_issue_subject, if: proc { |issue| + RedmineZulip.api.configured? && + issue.fixed_version_id.present? && + issue.project.zulip_subject_issue && + issue.project.zulip_stream.present? && + issue.previous_changes.include?("id") + } + + after_commit :update_issue_subject, if: proc { |issue| + RedmineZulip.api.configured? && + !issue.destroyed? && + issue.fixed_version_id.present? && + issue.project.zulip_subject_issue && + issue.project.zulip_stream.present? && + !issue.previous_changes.include?("id") + } + + after_commit :update_issue_subject_destroyed, if: proc { |issue| + RedmineZulip.api.configured? && + issue.destroyed? && + issue.fixed_version_id.present? && + issue.project.zulip_subject_issue && + issue.project.zulip_stream.present? + } + + after_commit :update_version_subject_added, if: proc { |issue| + RedmineZulip.api.configured? && + issue.project.zulip_subject_version && + issue.project.zulip_stream.present? && + issue.previous_changes.include?("fixed_version_id") && + issue.previous_changes["fixed_version_id"].last.present? + } + + after_commit :update_version_subject_removed, if: proc { |issue| + RedmineZulip.api.configured? && + issue.project.zulip_subject_version && + issue.project.zulip_stream.present? && + issue.previous_changes.include?("fixed_version_id") && + issue.previous_changes["fixed_version_id"].first.present? + } + + after_commit :update_version_subject_status, if: proc { |issue| + RedmineZulip.api.configured? + issue.project.zulip_subject_version && + issue.project.zulip_stream.present? && + issue.previous_changes.include?("status_id") && + issue.fixed_version_id.present? && + !issue.previous_changes.include?("fixed_version_id") + } + + after_commit :update_version_subject_destroyed, if: proc { |issue| + RedmineZulip.api.configured? && + issue.destroyed? && + issue.project.zulip_subject_version && + issue.project.zulip_stream.present? && + issue.fixed_version_id.present? + } + end + + private + + def notify_assignment + locale = assigned_to.language + message = I18n.t("zulip_notify_assignment", { + locale: locale, + user: User.current.name, + id: id, + url: url, + status: status.name, + subject: subject_without_punctuation, + description: description_truncated, + status_label: Issue.human_attribute_name(:status, locale: locale) + }) + if fixed_version.present? + version_label = Issue.human_attribute_name(:fixed_version, locale: locale) + message << "* **#{version_label}**: #{fixed_version.name}\n" + end + if estimated_hours.present? + estimated_hours_label = Issue.human_attribute_name(:estimated_hours, locale: locale) + message << "* **#{estimated_hours_label}**: #{estimated_hours}\n" + end + RedmineZulip.api.messages.send( + type: "private", + content: message, + to: assigned_to.mail + ) + end + + def notify_unassignment + previous_assigned_to = User.find( + previous_changes["assigned_to_id"].first + ) + message = I18n.t("zulip_notify_unassignment", { + user: User.current.name, + id: id, + url: url, + subject: subject_without_punctuation + }) + RedmineZulip.api.messages.send( + type: "private", + content: message, + to: previous_assigned_to.mail + ) + end + + def init_issue_subject + locale = Setting.default_language + message = I18n.t("zulip_init_issue_subject", { + locale: locale, + user: User.current.name, + id: id, + url: url, + subject: subject_without_punctuation, + description: description_truncated, + assigned_to_label: Issue.human_attribute_name(:assigned_to, locale: locale), + assigned_to: assigned_to || "-", + status_label: Issue.human_attribute_name(:status, locale: locale), + status: status&.name + }) + if fixed_version.present? + version_label = Issue.human_attribute_name(:fixed_version, locale: locale) + message << "* **#{version_label}**: #{fixed_version.name}\n" + end + if estimated_hours.present? + estimated_hours_label = Issue.human_attribute_name(:estimated_hours, locale: locale) + message << "* **#{estimated_hours_label}**: #{estimated_hours}\n" + end + RedmineZulip.api.messages.send( + type: "stream", + content: message, + to: project.zulip_stream, + subject: subject_without_punctuation + ) + end + + def notify_assigned_to_issue_updated + locale = assigned_to.language + message = I18n.t("zulip_notify_updated", { + user: User.current.name, + id: id, + url: url, + subject: subject_without_punctuation, + locale: locale + }) + if previous_changes.include?("description") + message << "~~~ quote\n" + message << description_truncated + message << "\n~~~\n" + end + if notes.present? + message << "\n**#{Issue.human_attribute_name(:notes, locale: locale)}**\n" + message << "~~~ quote\n" + message << notes + message << "\n~~~\n" + end + if previous_changes.include?("status_id") + message << "\n* **#{Issue.human_attribute_name(:status, locale: locale)}**: " + previous_status_id = previous_changes["status_id"].first + if previous_status_id.present? + previous_status = IssueStatus.find(previous_status_id) + message << "*~~#{previous_status}~~* " if previous_status.present? + end + if status.present? + message << status.name + end + end + if previous_changes.include?("fixed_version_id") + message << "\n* **#{Issue.human_attribute_name(:fixed_version, locale: locale)}**: " + previous_fixed_version_id = previous_changes["fixed_version_id"].first + if previous_fixed_version_id.present? + previous_fixed_version = Version.find(previous_fixed_version_id) + message << "*~~#{previous_fixed_version}~~* " if previous_fixed_version.present? + end + if fixed_version.present? + message << fixed_version.name + end + end + if previous_changes.include?("estimated_hours") + message << "\n* **#{Issue.human_attribute_name(:estimated_hours, locale: locale)}**: " + previous_estimated_hours = previous_changes["estimated_hours"].first + if previous_estimated_hours.present? + message << "*~~#{previous_estimated_hours}~~* " + end + if estimated_hours.present? + message << "#{estimated_hours}" + end + end + RedmineZulip.api.messages.send( + type: "private", + content: message, + to: assigned_to.mail + ) + end + + def notify_assigned_to_issue_destroyed + locale = assigned_to.language + message = I18n.t("zulip_notify_destroyed", { + user: User.current.name, + id: id, + subject: subject_without_punctuation, + locale: locale + }) + RedmineZulip.api.messages.send( + type: "private", + content: message, + to: assigned_to.mail + ) + end + + def update_issue_subject + locale = Setting.default_language + message = I18n.t("zulip_notify_updated", { + locale: locale, + user: User.current.name, + id: id, + url: url, + subject: subject_without_punctuation, + }) + if previous_changes.include?("description") + message << "~~~ quote\n" + message << description_truncated + message << "\n~~~\n" + end + if notes.present? + message << "\n* **#{Issue.human_attribute_name(:notes, locale: locale)}**\n" + message << "~~~ quote\n" + message << notes + message << "\n~~~\n" + end + if previous_changes.include?("assigned_to_id") + message << "\n* **#{Issue.human_attribute_name(:assigned_to, locale: locale)}**: " + previous_assigned_to_id = previous_changes["assigned_to_id"].first + if previous_assigned_to_id.present? + previous_assigned_to = User.find(previous_assigned_to_id) + message << "*~~#{previous_assigned_to.name}~~* " if previous_assigned_to.present? + end + if assigned_to.present? + message << assigned_to.name + end + end + if previous_changes.include?("status_id") + message << "\n* **#{Issue.human_attribute_name(:status, locale: locale)}**: " + previous_status_id = previous_changes["status_id"].first + if previous_status_id.present? + previous_status = IssueStatus.find(previous_status_id) + message << "*~~#{previous_status}~~* " if previous_status.present? + end + if status.present? + message << status.name + end + end + if previous_changes.include?("fixed_version_id") + message << "\n* **#{Issue.human_attribute_name(:fixed_version, locale: locale)}**: " + previous_fixed_version_id = previous_changes["fixed_version_id"].first + if previous_fixed_version_id.present? + previous_fixed_version = Version.find(previous_fixed_version_id) + message << "*~~#{previous_fixed_version}~~* " if previous_fixed_version.present? + end + if fixed_version.present? + message << fixed_version.name + end + end + if previous_changes.include?("estimated_hours") + message << "\n* **#{Issue.human_attribute_name(:estimated_hours, locale: locale)}**: " + previous_estimated_hours = previous_changes["estimated_hours"].first + if previous_estimated_hours.present? + message << "*~~#{previous_estimated_hours}~~* " + end + if estimated_hours.present? + message << "#{estimated_hours}" + end + end + RedmineZulip.api.messages.send( + type: "stream", + content: message, + to: project.zulip_stream, + subject: subject_without_punctuation + ) + end + + def update_issue_subject_destroyed + message = I18n.t("zulip_notify_destroyed", { + locale: Setting.default_language, + user: User.current.name, + id: id, + subject: subject_without_punctuation + }) + RedmineZulip.api.messages.send( + type: "stream", + content: message, + to: project.zulip_stream, + subject: subject_without_punctuation + ) + end + + def update_version_subject_added + message = I18n.t("zulip_update_version_subject_added", { + locale: Setting.default_language, + user: User.current.name, + id: id, + url: url, + subject: subject_without_punctuation, + fixed_version: fixed_version + }) + RedmineZulip.api.messages.send( + type: "stream", + content: message, + to: project.zulip_stream, + subject: "Version #{fixed_version.name}" + ) + end + + def update_version_subject_removed + previous_fixed_version = Version.find( + previous_changes["fixed_version_id"].first + ) + message = I18n.t("zulip_update_version_subject_removed", { + locale: Setting.default_language, + user: User.current.name, + id: id, + url: url, + subject: subject_without_punctuation, + fixed_version: previous_fixed_version + }) + RedmineZulip.api.messages.send( + type: "stream", + content: message, + to: project.zulip_stream, + subject: "Version #{previous_fixed_version.name}" + ) + end + + def update_version_subject_status + previous_status_id = previous_changes["status_id"].first + message = I18n.t("zulip_update_version_subject_status", { + locale: Setting.default_language, + user: User.current.name, + id: id, + url: url, + subject: subject_without_punctuation, + previous_status: IssueStatus.find(previous_status_id), + current_status: status + }) + RedmineZulip.api.messages.send( + type: "stream", + content: message, + to: project.zulip_stream, + subject: "Version #{fixed_version.name}" + ) + end + + def update_version_subject_destroyed + message = I18n.t("zulip_notify_destroyed", { + locale: Setting.default_language, + user: User.current.name, + id: id, + subject: subject_without_punctuation + }) + RedmineZulip.api.messages.send( + type: "stream", + content: message, + to: project.zulip_stream, + subject: "Version #{fixed_version.name}" + ) + end + + private + + def url + "#{Setting[:protocol]}://#{Setting[:host_name]}/issues/#{id}" + end + + def subject_without_punctuation + subject.end_with?(".") ? subject[0..-1] : subject + end + + def description_truncated + truncated = description + if truncated.include?("\n") + truncated = "#{truncated.split("\n")[0]}..." + end + if truncated.size > TWEET_SIZE + truncated = "#{truncated[0..(TWEET_SIZE - 1)]}..." + end + truncated + end + end +end diff --git a/lib/redmine_zulip/project_patch.rb b/lib/redmine_zulip/project_patch.rb new file mode 100644 index 0000000..1ab8d8f --- /dev/null +++ b/lib/redmine_zulip/project_patch.rb @@ -0,0 +1,14 @@ +module RedmineZulip + module ProjectPatch + extend ActiveSupport::Concern + + included do + safe_attributes( + "zulip_stream", + "zulip_subject_issue", + "zulip_subject_version", + "zulip_private_messages" + ) + end + end +end diff --git a/lib/redmine_zulip/project_settings_tabs.rb b/lib/redmine_zulip/project_settings_tabs.rb new file mode 100644 index 0000000..8797782 --- /dev/null +++ b/lib/redmine_zulip/project_settings_tabs.rb @@ -0,0 +1,13 @@ +module RedmineZulip + module ProjectSettingsTabs + def project_settings_tabs + super.tap do |tabs| + tabs.push({ + name: 'redmine_zulip', + partial: 'projects/settings/redmine_zulip', + label: :label_redmine_zulip + }) + end + end + end +end diff --git a/redmine_zulip/app/views/projects/_redmine_zulip.html.erb b/redmine_zulip/app/views/projects/_redmine_zulip.html.erb deleted file mode 100644 index 37ffcb1..0000000 --- a/redmine_zulip/app/views/projects/_redmine_zulip.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -

- <%= form.text_field :zulip_email %> - This should be a bot address for most uses. -

- -

- <%= form.text_field :zulip_api_key %> - The API key for the bot. -

- -

- <%= form.text_field :zulip_stream %> - Which Zulip stream will receive updates. The message topic will be the project name. -

diff --git a/redmine_zulip/app/views/settings/_redmine_zulip.html.erb b/redmine_zulip/app/views/settings/_redmine_zulip.html.erb deleted file mode 100644 index eeef0fa..0000000 --- a/redmine_zulip/app/views/settings/_redmine_zulip.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -

- <%= content_tag(:label, l(:zulip_settings_label_url)) %> - <%= text_field_tag 'settings[zulip_url]', @settings["zulip_url"] %> -

- -

- <%= content_tag(:label, l(:zulip_settings_label_email)) %> - <%= text_field_tag 'settings[zulip_email]', @settings["zulip_email"] %> -

- -

- <%= content_tag(:label, l(:zulip_settings_label_api_key)) %> - <%= text_field_tag 'settings[zulip_api_key]', @settings["zulip_api_key"] %> -

- -

- <%= content_tag(:label, l(:zulip_settings_label_stream)) %> - <%= text_field_tag 'settings[zulip_stream]', @settings["zulip_stream"] %> -

- -

- <%= content_tag(:label, l(:zulip_settings_label_projects)) %> - <%= select_tag 'settings[projects][]', project_tree_options_for_select(Project.active, :selected => Project.find_by_id(@settings["projects"])), :multiple => true, :size => Project.active.size %> -

diff --git a/redmine_zulip/config/locales/en.yml b/redmine_zulip/config/locales/en.yml deleted file mode 100644 index 0c04b9f..0000000 --- a/redmine_zulip/config/locales/en.yml +++ /dev/null @@ -1,10 +0,0 @@ -en: - zulip_settings_header: Zulip Plugin Configuration - zulip_settings_label_stream: Zulip stream - zulip_settings_label_url: Zulip URL - zulip_settings_label_email: Zulip Email - zulip_settings_label_api_key: Zulip API key - zulip_settings_label_projects: Projects - field_zulip_email: Zulip Email - field_zulip_api_key: Zulip API key - field_zulip_stream: Zulip stream diff --git a/redmine_zulip/init.rb b/redmine_zulip/init.rb deleted file mode 100644 index 68bd8f9..0000000 --- a/redmine_zulip/init.rb +++ /dev/null @@ -1,24 +0,0 @@ -require_relative "lib/version" - -Redmine::Plugin.register :redmine_zulip do - name 'Zulip' - author 'Zulip, Inc.' - description 'Sends notifications to Zulip.' - version RedmineZulip::VERSION - url 'https://github.com/zulip/zulip-redmine-plugin' - author_url 'https://www.zulip.org/' - - Rails.configuration.to_prepare do - require_dependency 'zulip_hooks' - require_dependency 'zulip_view_hooks' - require_dependency 'project_patch' - Project.send(:include, RedmineZulip::Patches::ProjectPatch) - end - - settings :partial => 'settings/redmine_zulip', - :default => { - :zulip_email => "", - :zulip_api_key => "", - :zulip_stream => "", - :zulip_url => ""} -end diff --git a/redmine_zulip/lib/project_patch.rb b/redmine_zulip/lib/project_patch.rb deleted file mode 100644 index 15d630a..0000000 --- a/redmine_zulip/lib/project_patch.rb +++ /dev/null @@ -1,11 +0,0 @@ -module RedmineZulip - module Patches - module ProjectPatch - def self.included(base) - base.class_eval do - safe_attributes 'zulip_email', 'zulip_api_key', 'zulip_stream' - end - end - end - end -end diff --git a/redmine_zulip/lib/version.rb b/redmine_zulip/lib/version.rb deleted file mode 100644 index a3dcbbe..0000000 --- a/redmine_zulip/lib/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module RedmineZulip - VERSION = "1.0" -end diff --git a/redmine_zulip/lib/zulip_hooks.rb b/redmine_zulip/lib/zulip_hooks.rb deleted file mode 100644 index 2769f7b..0000000 --- a/redmine_zulip/lib/zulip_hooks.rb +++ /dev/null @@ -1,132 +0,0 @@ -# encoding: utf-8 - -require 'json' - -class NotificationHook < Redmine::Hook::Listener - - # We generate Zulips for creating and updating issues. - - def controller_issues_new_after_save(context = {}) - issue = context[:issue] - project = issue.project - - if !configured(project) - # Fail silently: the rest of the app needs to continue working. - return true - end - - content = %Q{%s opened [issue %d: %s](%s) in %s - -~~~ quote -%s -~~~ - -**Priority**: %s -**Status**: %s -**Assigned to**: %s} % [User.current.name, issue.id, issue.subject, url(issue), - project.name, issue.description, issue.priority.to_s, - issue.status.to_s, issue.assigned_to.to_s] - - send_zulip_message(content, project) - end - - def controller_issues_edit_after_save(context = {}) - issue = context[:issue] - project = issue.project - - if !configured(project) - # Fail silently: the rest of the app needs to continue working. - return true - end - - content = %Q{%s updated [issue %d: %s](%s) in %s - -~~~ quote -%s -~~~} % [User.current.name, issue.id, issue.subject, url(issue), - project.name, issue.notes] - - send_zulip_message(content, project) - end - - private - - def configured(project) - # The plugin can be configured as a system setting or per-project. - - if project.zulip_email.present? && - project.zulip_api_key.present? && - project.zulip_stream.present? && - Setting.plugin_redmine_zulip["projects"] && - Setting.plugin_redmine_zulip["zulip_url"].present? - # We have full per-project settings. - return true - end - if Setting.plugin_redmine_zulip["projects"] && - Setting.plugin_redmine_zulip["zulip_email"].present? && - Setting.plugin_redmine_zulip["zulip_api_key"].present? && - Setting.plugin_redmine_zulip["zulip_stream"].present? && - Setting.plugin_redmine_zulip["zulip_url"].present? - # We have full global settings. - return true - end - - Rails.logger.info "Missing config, can't sent to Zulip!" - false - end - - def zulip_email(project) - if project.zulip_email.present? - return project.zulip_email - end - Setting.plugin_redmine_zulip["zulip_email"] - end - - def zulip_api_key(project) - if project.zulip_api_key.present? - return project.zulip_api_key - end - Setting.plugin_redmine_zulip["zulip_api_key"] - end - - def zulip_stream(project) - if project.zulip_stream.present? - return project.zulip_stream - end - Setting.plugin_redmine_zulip["zulip_stream"] - end - - def zulip_url() - Setting.plugin_redmine_zulip["zulip_url"] - end - - def url(issue) - "#{Setting[:protocol]}://#{Setting[:host_name]}/issues/#{issue.id}" - end - - def send_zulip_message(content, project) - data = {"to" => zulip_stream(project), - "type" => "stream", - "subject" => project.name, - "content" => content} - - Rails.logger.info "Forwarding to Zulip: #{data['content']}" - - uri = URI("#{zulip_url}/v1/messages") - - req = Net::HTTP::Post.new(uri) - req.basic_auth(zulip_email(project), zulip_api_key(project)) - req["User-Agent"] = "ZulipRedmine/#{RedmineZulip::VERSION}" - req.set_form_data(data) - - res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| - http.request(req) - end - - if res.code == "200" - Rails.logger.info "Zulip message sent!" - else - Rails.logger.error "Error while POSTing to Zulip: #{res.body}" - end - end -end diff --git a/redmine_zulip/lib/zulip_view_hooks.rb b/redmine_zulip/lib/zulip_view_hooks.rb deleted file mode 100644 index b7d9df5..0000000 --- a/redmine_zulip/lib/zulip_view_hooks.rb +++ /dev/null @@ -1,3 +0,0 @@ -class NotificationViewHook < Redmine::Hook::ViewListener - render_on(:view_projects_form, :partial => 'projects/redmine_zulip', :layout => false) -end