diff --git a/redmine_migration.rb b/redmine_migration.rb new file mode 100755 index 0000000..d2f17e1 --- /dev/null +++ b/redmine_migration.rb @@ -0,0 +1,200 @@ +#!/usr/bin/env ruby + +require 'net/http' +require 'thor' +require 'json' + +class RedmineMigration < Thor + desc "import LIST_ID", "import tickets from a ClickUp list" + def import(list_id, project_id, tracker_id) + tasks = ClickUp.tasks(list_id) + issues = [] + tasks.each do |task| + issue = Issue.new( + name: task['name'], + description: task['text_content'], + status: task['status']['status'], + author: task['creator']['email'], + estimate: task['time_estimate'], # Check this + ) + print 'T' + comments = ClickUp.comments(task['id']) + + comments.each do |comment| + c = Comment.new( + text: comment['comment_text'], + author: comment['user']['email'], + ) + + issue.comments << c + end + print 'C' + issues << issue + end + + issues.each do |issue| + issue_response = Redmine.create_issue( + issue:, + project_id:, + tracker_id:, + ) + puts issue.inspect + puts issue_response.inspect + issue.id = issue_response['issue']['id'] + issue.comments.each do |comment| + next unless comment.class == Comment # I don’t know why I need this 😭 + + puts 'goo' + puts issue.inspect + puts issue_response.inspect + Redmine.add_comment( + issue_id: issue.id, + text: comment.text, + author: comment.author, + ) + end if issue.comments.any? + end + end +end + +class Issue + attr_accessor :name, :description, :status, :author, :comments, :estimate, :id + + def initialize(name:, description:, status:, author:, estimate:) + @name = name + @description = description + @status = status + @author = author + @comments = [], + @estimate = estimate + end +end + +class Comment + attr_accessor :text, :author + + def initialize(text:, author:) + @text = text + @author = author + end +end + +class Redmine + def self.create_issue(issue:, project_id:, tracker_id:) + api_call( + :post, + "issues.json", + params: { + issue: { + project_id: project_id, + tracker_id: tracker_id, + subject: issue.name, + description: issue.description, + estimated_hours: issue.estimate, + } + }, + as_user: issue.author + ) + end + + def self.add_comment(issue_id:, text:, author: nil) + api_call( + :put, + "issues/#{issue_id}.json", + params: { + issue: { + notes: text, + } + }, + as_user: author + ) + end + + def self.api_call(method, path, params: {}, as_user: nil) + url = URI.parse("https://redmine.foxsoft.co.uk/#{path}") + http = Net::HTTP.new(url.host, url.port) + http.use_ssl = true + + headers = { + 'Content-Type' => 'application/json', + 'X-Redmine-API-Key' => api_token, + } + headers['X-Redmine-Switch-User'] = as_user if as_user + case method + when :post + request = Net::HTTP::Post.new( + url, + headers + ) + when :put + request = Net::HTTP::Put.new( + url, + headers + ) + end + request.body = params.to_json + + response = http.request(request) + + return api_call(method, path, params: params) if response.code.to_i == 412 + JSON.parse(response.read_body) unless response.is_a?(Net::HTTPNoContent) + end + + def self.api_token + ENV['REDMINE_API_KEY'] + end +end + +class ClickUp + def self.available_workspaces + api_call("team")["teams"] + end + + def self.folders(workspace_id) + api_call("space/#{workspace_id}/folder", params: { archived: false })['folders'] + end + + def self.spaces + api_call("team/#{team_id}/space", + params: {archived: false})['spaces'] + end + + def self.tasks(list_id) + api_call("list/#{list_id}/task")['tasks'] + end + + def self.comments(task_id) + api_call("task/#{task_id}/comment")['comments'] + end + + def self.lists(folder_id) + api_call("folder/#{folder_id}/list")['lists'] + end + + def self.api_call(url, params: {}) + url = URI("https://api.clickup.com/api/v2/#{url}") + url.query = URI.encode_www_form(params) + http = Net::HTTP.new(url.host, url.port) + http.use_ssl = true + + request = Net::HTTP::Get.new(url) + request['Authorization'] = api_token + + response = http.request(request) + json = JSON.parse(response.read_body) + if json.has_key?('err') + puts "ClickUp API returned an error:" + puts "\t#{json['ECODE']}" + puts "\t#{json['err']}" + exit + end + + json + end + + def self.api_token + ENV['CLICKUP_API_KEY'] + end +end + +RedmineMigration.start