Fixes #3698 - Import for kayako during the setup.

This commit is contained in:
Dominik Klein 2021-10-20 11:43:35 +02:00 committed by Martin Gruner
parent d4f997f0fd
commit dd30b18285
145 changed files with 10489 additions and 6 deletions

View file

@ -17,7 +17,7 @@ module RuboCop
PATTERN
def_node_matcher :has_reset?, <<-PATTERN
$(send _ {:describe :context :it} (_ ...) (hash ... (pair (sym :db_strategy) (sym {:reset :reset_all}))))
$(send _ {:describe :context :it :shared_examples} (_ ...) (hash ... (pair (sym :db_strategy) (sym {:reset :reset_all}))))
PATTERN
MSG = 'Add a `db_strategy: :reset` to your context/decribe when you are creating object manager attributes!'.freeze

View file

@ -8,7 +8,6 @@ class ImportFreshdesk extends App.ControllerWizardFullScreen
'#freshdesk-subdomain-addon': 'freshdeskSubdomainAddon'
'.freshdesk-subdomain-error': 'linkErrorMessage'
'.freshdesk-api-token-error': 'apiTokenErrorMessage'
'#freshdesk-email': 'freshdeskEmail'
'#freshdesk-api-token': 'freshdeskApiToken'
'.js-ticket-count-info': 'ticketCountInfo'
updateMigrationDisplayLoop: 0

View file

@ -0,0 +1,194 @@
class ImportKayako extends App.ControllerWizardFullScreen
className: 'getstarted fit'
elements:
'.input-feedback': 'urlStatus'
'[data-target=kayako-credentials]': 'nextEnterCredentials'
'[data-target=kayako-start-migration]': 'nextStartMigration'
'#kayako-subdomain': 'kayakoSubdomain'
'#kayako-subdomain-addon': 'kayakoSubdomainAddon'
'.kayako-subdomain-error': 'linkErrorMessage'
'.kayako-password-error': 'apiTokenErrorMessage'
'#kayako-email': 'kayakoEmail'
'#kayako-password': 'kayakoPassword'
'.js-ticket-count-info': 'ticketCountInfo'
updateMigrationDisplayLoop: 0
events:
'click .js-kayako-credentials': 'showCredentials'
'click .js-migration-start': 'startMigration'
'keyup #kayako-subdomain': 'updateUrl'
'keyup #kayako-password': 'updateCredentials'
constructor: ->
super
# set title
@title 'Import'
@kayakoDomain = '.kayako.com'
# redirect to login if admin user already exists
if @Config.get('system_init_done')
@navigate '#login'
return
@fetch()
fetch: ->
# get data
@ajax(
id: 'getting_started'
type: 'GET'
url: "#{@apiPath}/getting_started"
processData: true
success: (data, status, xhr) =>
# check if import is active
if data.import_mode == true && data.import_backend != 'kayako'
@navigate "#import/#{data.import_backend}", { emptyEl: true }
return
# render page
@render()
if data.import_mode == true
@showImportState()
@updateMigration()
)
render: ->
@replaceWith App.view('import/kayako')(
kayakoDomain: @kayakoDomain
)
updateUrl: (e) =>
@urlStatus.attr('data-state', 'loading')
@kayakoSubdomainAddon.attr('style', 'padding-right: 42px')
@linkErrorMessage.text('')
# get data
callback = =>
@ajax(
id: 'import_kayako_url'
type: 'POST'
url: "#{@apiPath}/import/kayako/url_check"
data: JSON.stringify(url: "https://#{@kayakoSubdomain.val()}#{@kayakoDomain}")
processData: true
success: (data, status, xhr) =>
# validate form
if data.result is 'ok'
@urlStatus.attr('data-state', 'success')
@linkErrorMessage.text('')
@nextEnterCredentials.removeClass('hide')
else
@urlStatus.attr('data-state', 'error')
@linkErrorMessage.text( data.message_human || data.message)
@nextEnterCredentials.addClass('hide')
)
@delay( callback, 700, 'import_kayako_url' )
updateCredentials: (e) =>
@urlStatus.attr('data-state', 'loading')
@apiTokenErrorMessage.text('')
# get data
callback = =>
@ajax(
id: 'import_kayako_api_token'
type: 'POST'
url: "#{@apiPath}/import/kayako/credentials_check"
data: JSON.stringify(username: @kayakoEmail.val(), password: @kayakoPassword.val())
processData: true
success: (data, status, xhr) =>
# validate form
if data.result is 'ok'
@urlStatus.attr('data-state', 'success')
@apiTokenErrorMessage.text('')
@nextStartMigration.removeClass('hide')
else
@urlStatus.attr('data-state', 'error')
@apiTokenErrorMessage.text(data.message_human || data.message)
@nextStartMigration.addClass('hide')
)
@delay(callback, 700, 'import_kayako_api_token')
showCredentials: (e) =>
e.preventDefault()
@urlStatus.attr('data-state', '')
@$('[data-slide=kayako-subdomain]').toggleClass('hide')
@$('[data-slide=kayako-credentials]').toggleClass('hide')
showImportState: =>
@$('[data-slide=kayako-subdomain]').addClass('hide')
@$('[data-slide=kayako-credentials]').addClass('hide')
@$('[data-slide=kayako-import]').removeClass('hide')
startMigration: (e) =>
e.preventDefault()
@showImportState()
@ajax(
id: 'import_start'
type: 'POST'
url: "#{@apiPath}/import/kayako/import_start"
processData: true
success: (data, status, xhr) =>
# validate form
if data.result is 'ok'
@delay(@updateMigration, 3000)
)
updateMigration: =>
@updateMigrationDisplayLoop += 1
@showImportState()
@ajax(
id: 'import_status'
type: 'GET'
url: "#{@apiPath}/import/kayako/import_status"
processData: true
success: (data, status, xhr) =>
if _.isEmpty(data.result) && @updateMigrationDisplayLoop > 16
@$('.js-error').removeClass('hide')
@$('.js-error').html(App.i18n.translateContent('Background process did not start or has not finished! Please contact your support.'))
return
if !_.isEmpty(data.result['error'])
@$('.js-error').removeClass('hide')
@$('.js-error').html(App.i18n.translateContent(data.result['error']))
else
@$('.js-error').addClass('hide')
if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error'])
window.location.reload()
return
if !_.isEmpty(data.result)
for model, stats of data.result
if stats.sum > stats.total
stats.sum = stats.total
element = @$('.js-' + model.toLowerCase() )
element.find('.js-done').text(stats.sum)
element.find('.js-total').text(stats.total)
element.find('progress').attr('max', stats.total )
element.find('progress').attr('value', stats.sum )
if stats.total <= stats.sum
element.addClass('is-done')
else
element.removeClass('is-done')
@delay(@updateMigration, 5000)
)
App.Config.set('import/kayako', ImportKayako, 'Routes')
App.Config.set('kayako', {
title: 'Kayako'
name: 'Kayako'
class: 'js-kayako'
url: '#import/kayako'
}, 'ImportPlugins')

View file

@ -0,0 +1,113 @@
<div class="main flex vertical centered darkBackground">
<%- @Icon('full-logo', 'wizard-logo') %>
<div class="import wizard">
<div class="wizard-slide vertical" data-slide="kayako-subdomain">
<h2><%- @T('%s URL', 'Kayako') %></h2>
<div class="wizard-body flex vertical justified">
<p>
<%- @T('Enter the Subdomain of your %s system', 'Kayako') %>:
</p>
<div class="form-group">
<label for="kayako-subdomain"><%- @T('%s Subdomain', 'Kayako') %></label>
<div class="u-positionOrigin">
<div class="input-group">
<input type="text" id="kayako-subdomain" class="form-control" placeholder="example" name="kayako-subdomain" aria-describedby="kayako-subdomain-addon">
<span class="input-group-addon" id="kayako-subdomain-addon"><%- @kayakoDomain %></span>
</div>
<div class="input-feedback input-feedback--no-background centered">
<div class="small loading icon"></div>
<%- @Icon('diagonal-cross', 'icon-error') %>
<%- @Icon('checkmark') %>
</div>
</div>
<div class="error kayako-subdomain-error"></div>
</div>
</div>
<div class="wizard-controls horizontal center">
<a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
<div class="btn btn--primary align-right hide js-kayako-credentials" data-target="kayako-credentials"><%- @T('Enter credentials') %></div>
</div>
</div>
<div class="wizard-slide vertical hide" data-slide="kayako-credentials">
<h2><%- @T('%s credentials', 'Kayako') %></h2>
<div class="wizard-body flex vertical justified">
<p>
<%- @T('Enter your email address and password from your %s account which should be used for the import.', 'Kayako') %>
</p>
<p>
<%- @T('Attention: These will also your login password after the import is completed.') %>
</p>
<div class="form-group">
<label for="kayako-email"><%- @T('Email') %></label>
<div class="u-positionOrigin">
<input type="email" id="kayako-email" class="form-control" placeholder="admin@example.com" name="kayako-email">
</div>
<label for="kayako-password"><%- @T('Password') %></label>
<div class="u-positionOrigin">
<input type="password" id="kayako-password" class="form-control" name="kayako-password">
<div class="input-feedback centered">
<div class="small loading icon"></div>
<%- @Icon('diagonal-cross', 'icon-error') %>
<%- @Icon('checkmark') %>
</div>
</div>
<div class="error kayako-password-error"></div>
</div>
</div>
<div class="wizard-controls horizontal center">
<a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
<div class="btn btn--primary align-right hide js-migration-start" data-target="kayako-start-migration"><%- @T('Migrate %s Data', 'Kayako') %></div>
</div>
</div>
<div class="wizard-slide vertical hide" data-slide="kayako-import">
<h2><%- @T('%s Migration', 'Kayako') %></h2>
<div class="alert alert--danger hide js-error" role="alert"></div>
<div class="wizard-body flex vertical justified">
<table class="progressTable">
<tr class="js-groups">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Groups') %></span>
<td class="progressTable-progressCell">
<div class="horizontal center">
<div class="flex"><progress max="42" value="42"></progress></div>
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-organizations">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Organizations') %></span>
<td class="progressTable-progressCell">
<div class="horizontal center">
<div class="flex"><progress max="42" value="42"></progress></div>
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-users">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Users') %></span>
<td class="progressTable-progressCell">
<div class="horizontal center">
<div class="flex"><progress max="42" value="42"></progress></div>
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-tickets">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Tickets') %></span>
<td class="progressTable-progressCell">
<div class="horizontal center">
<div class="flex"><progress max="42" value="42"></progress></div>
<%- @Icon('checkmark') %>
</div>
</tr>
</table>
</div>
<div class="wizard-controls horizontal center">
<a href="#" class="btn btn--primary align-right hide js-finished"><%- @T('done') %></a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,146 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class ImportKayakoController < ApplicationController
def url_check
return if setup_done_response
url = params[:url]
# validate
if !valid_url_syntax?(url)
render json: {
result: 'invalid',
message: 'Invalid URL!',
}
return
end
endpoint = build_endpoint_url(url)
return if !valid_endpoint?(endpoint)
Setting.set('import_kayako_endpoint', endpoint)
render json: {
result: 'ok',
url: url,
}
end
def credentials_check
return if setup_done_response
if !params[:username] || !params[:password]
render json: {
result: 'invalid',
message_human: 'Incomplete credentials',
}
return
end
save_endpoint_settings(params[:username], params[:password])
return if !valid_connection?
render json: {
result: 'ok',
}
end
def import_start
return if setup_done_response
Setting.set('import_mode', true)
Setting.set('import_backend', 'kayako')
job = ImportJob.create(name: 'Import::Kayako')
AsyncImportJob.perform_later(job)
render json: {
result: 'ok',
}
end
def import_status
job = ImportJob.find_by(name: 'Import::Kayako')
if job.finished_at.present?
Setting.reload
end
model_show_render_item(job)
end
private
def setup_done
count = User.all.count()
done = true
if count <= 2
done = false
end
done
end
def setup_done_response
if !setup_done
return false
end
render json: {
setup_done: true,
}
true
end
def valid_url_syntax?(url)
return false if url.blank? || url !~ %r{^(http|https)://.+?$}
true
end
def valid_endpoint?(endpoint)
response = UserAgent.request("#{endpoint}/teams", verify_ssl: true)
if response.header.nil? || !response.header['x-api-version']
render json: {
result: 'invalid',
message: response.error.to_s,
message_human: 'Hostname not found!',
}
return false
end
true
end
def build_endpoint_url(url)
endpoint = "#{url}/api/v1"
endpoint.gsub(%r{([^:])//+}, '\\1/')
end
def valid_connection?
result = Sequencer.process('Import::Kayako::ConnectionTest')
if !result[:connected]
reset_endpoint_settings
render json: {
result: 'invalid',
message_human: 'Invalid credentials!',
}
return false
end
true
end
def save_endpoint_settings(username, possword)
Setting.set('import_kayako_endpoint_username', username)
Setting.set('import_kayako_endpoint_password', possword)
end
def reset_endpoint_settings
save_endpoint_settings(nil, nil)
end
end

View file

@ -0,0 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
Zammad::Application.routes.draw do
api_path = Rails.configuration.api_path
# import kayako
match api_path + '/import/kayako/url_check', to: 'import_kayako#url_check', via: :post
match api_path + '/import/kayako/credentials_check', to: 'import_kayako#credentials_check', via: :post
match api_path + '/import/kayako/import_start', to: 'import_kayako#import_start', via: :post
match api_path + '/import/kayako/import_status', to: 'import_kayako#import_status', via: :get
end

View file

@ -0,0 +1,15 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class AddTicketArticleTypeFacebookDirectMessage < ActiveRecord::Migration[6.0]
def change
# return if it's a new setup
return if !Setting.exists?(name: 'system_init_done')
Ticket::Article::Type.create_if_not_exists(
name: 'facebook direct-message',
communication: true,
updated_by_id: 1,
created_by_id: 1,
)
end
end

View file

@ -3282,6 +3282,61 @@ Setting.create_if_not_exists(
frontend: false
)
Setting.create_if_not_exists(
title: 'Import Endpoint',
name: 'import_kayako_endpoint',
area: 'Import::Kayako',
description: 'Defines Kayako endpoint to import users, ticket, states and articles.',
options: {
form: [
{
display: '',
null: false,
name: 'import_kayako_endpoint',
tag: 'input',
},
],
},
state: 'https://yours.kayako.com/api/v1',
frontend: false
)
Setting.create_if_not_exists(
title: 'Import User for requesting the Kayako API',
name: 'import_kayako_endpoint_username',
area: 'Import::Kayako',
description: 'Defines Kayako endpoint authentication user.',
options: {
form: [
{
display: '',
null: false,
name: 'import_kayako_endpoint_username',
tag: 'input',
},
],
},
state: '',
frontend: false
)
Setting.create_if_not_exists(
title: 'Import Password for requesting the Kayako API',
name: 'import_kayako_endpoint_password',
area: 'Import::Kayako',
description: 'Defines Kayako endpoint authentication password.',
options: {
form: [
{
display: '',
null: false,
name: 'import_kayako_endpoint_password',
tag: 'input',
},
],
},
state: '',
frontend: false
)
Setting.create_if_not_exists(
title: 'Import Backends',
name: 'import_backends',

View file

@ -12,3 +12,4 @@ Ticket::Article::Type.create_if_not_exists(id: 9, name: 'facebook feed comment',
Ticket::Article::Type.create_if_not_exists(id: 10, name: 'note', communication: false)
Ticket::Article::Type.create_if_not_exists(id: 11, name: 'web', communication: true)
Ticket::Article::Type.create_if_not_exists(id: 12, name: 'telegram personal-message', communication: true)
Ticket::Article::Type.create_if_not_exists(id: 13, name: 'facebook direct-message', communication: true)

15
lib/import/kayako.rb Normal file
View file

@ -0,0 +1,15 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
module Import
class Kayako < Import::Base
include Import::Mixin::Sequence
def start
process
end
def sequence_name
'Import::Kayako::Full'
end
end
end

View file

@ -0,0 +1,45 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class Case < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Ticket',
'Import::Kayako::Case::Skip::Deleted',
'Import::Kayako::Case::Skip::Suspended',
'Import::Kayako::Common::CreatedById',
'Import::Kayako::Common::ArticleSenderId',
'Import::Kayako::Case::UpdatedById',
'Import::Kayako::Case::CustomerId',
'Import::Kayako::Case::OwnerId',
'Import::Kayako::Case::GroupId',
'Import::Kayako::Case::OrganizationId',
'Import::Kayako::Case::PriorityId',
'Import::Kayako::Case::StateId',
'Import::Kayako::Case::Type',
'Import::Kayako::Common::ArticleSourceChannel',
'Import::Kayako::Case::Mapping',
'Import::Kayako::Mapping::Timestamps',
'Import::Kayako::Mapping::CustomFields',
'Import::Common::Model::FindBy::Id',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Common::Model::ResetPrimaryKeySequence',
'Import::Kayako::MapId',
'Import::Kayako::Case::Tags',
'Import::Kayako::Case::Posts',
'Import::Common::Model::Statistics::Diff::ModelKey',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
]
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class CaseField < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Ticket',
'Import::Kayako::ObjectAttribute::Skip',
'Import::Kayako::ObjectAttribute::SanitizedName',
'Import::Kayako::ObjectAttribute::Config',
'Import::Kayako::ObjectAttribute::Add',
'Import::Kayako::ObjectAttribute::MigrationExecute',
'Import::Kayako::ObjectAttribute::FieldMap',
]
end
end
end
end
end
end

View file

@ -0,0 +1,21 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class ConnectionTest < Sequencer::Sequence::Base
def self.expecting
[:connected]
end
def self.sequence
[
'Kayako::Connected',
]
end
end
end
end
end
end

View file

@ -0,0 +1,34 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class Full < Sequencer::Sequence::Base
def self.sequence
[
'Import::Common::ImportMode::Check',
'Import::Common::SystemInitDone::Check',
'Import::Common::ImportJob::DryRun',
'Import::Kayako::DefaultLanguage',
'Import::Kayako::IdMap',
'Import::Kayako::Teams',
'Import::Kayako::FieldMap',
'Import::Kayako::OrganizationFields',
'Import::Kayako::Organizations',
'Import::Kayako::UserFields',
'Import::Kayako::Users',
'Import::Kayako::CaseFields',
'Import::Kayako::Cases',
'Import::Kayako::TimeEntries',
'Import::Common::SystemInitDone::Set',
'Import::Kayako::ImportSettingsUnset',
'Import::Common::ImportMode::Unset',
]
end
end
end
end
end
end

View file

@ -0,0 +1,20 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class GenericField < Sequencer::Sequence::Base
def self.sequence
[
'Import::Kayako::Request',
'Import::Kayako::Resources',
'Import::Kayako::Perform',
]
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class GenericObject < Sequencer::Sequence::Base
def self.sequence
[
'Import::Kayako::Request',
'Import::Kayako::Resources',
'Import::Kayako::ModelClass',
'Import::Kayako::ObjectCount',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
'Import::Kayako::Perform',
]
end
end
end
end
end
end

View file

@ -0,0 +1,29 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class Organization < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Organization',
'Import::Kayako::Organization::Mapping',
'Import::Kayako::Mapping::CustomFields',
'Import::Common::Model::Attributes::AddByIds',
'Import::Common::Model::FindBy::Name',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Kayako::MapId',
'Import::Common::Model::Statistics::Diff::ModelKey',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
]
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class OrganizationField < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Organization',
'Import::Kayako::ObjectAttribute::Skip',
'Import::Kayako::ObjectAttribute::SanitizedName',
'Import::Kayako::ObjectAttribute::Config',
'Import::Kayako::ObjectAttribute::Add',
'Import::Kayako::ObjectAttribute::MigrationExecute',
'Import::Kayako::ObjectAttribute::FieldMap',
]
end
end
end
end
end
end

View file

@ -0,0 +1,30 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class Post < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Ticket::Article',
'Import::Kayako::Common::CreatedById',
'Import::Kayako::Common::ArticleSenderId',
'Import::Kayako::Common::ArticleSourceChannel',
'Import::Kayako::Post::Mapping',
'Import::Kayako::Post::InlineImages',
'Import::Kayako::Mapping::Timestamps',
'Import::Kayako::Post::UnsetInstance',
'Import::Common::Model::FindBy::MessageId',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Kayako::MapId',
'Import::Kayako::Post::Attachments',
]
end
end
end
end
end
end

View file

@ -0,0 +1,21 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class Posts < Sequencer::Sequence::Base
def self.sequence
[
'Import::Kayako::Request',
'Import::Kayako::Resources',
'Import::Kayako::ModelClass',
'Import::Kayako::Perform',
]
end
end
end
end
end
end

View file

@ -0,0 +1,28 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class Team < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Group',
'Import::Kayako::Team::Mapping',
'Import::Common::Model::Attributes::AddByIds',
'Import::Common::Model::FindBy::Name',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Kayako::MapId',
'Import::Common::Model::Statistics::Diff::ModelKey',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
]
end
end
end
end
end
end

View file

@ -0,0 +1,21 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class TimeEntries < Sequencer::Sequence::Base
def self.sequence
[
'Import::Kayako::Request',
'Import::Kayako::Resources',
'Import::Kayako::ModelClass',
'Import::Kayako::Perform',
]
end
end
end
end
end
end

View file

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class TimeEntry < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::Ticket::TimeAccounting',
'Import::Kayako::TimeEntry::Skip',
'Import::Kayako::Common::CreatedById',
'Import::Kayako::TimeEntry::Mapping',
'Import::Kayako::Mapping::Timestamps',
'Import::Common::Model::FindBy::TimeAccountingAttributes',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
]
end
end
end
end
end
end

View file

@ -0,0 +1,37 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class User < Sequencer::Sequence::Base
def self.sequence
[
'Import::Kayako::User::Identifier',
'Import::Kayako::User::Login',
'Import::Kayako::User::Initiator',
'Import::Kayako::User::Password',
'Import::Kayako::User::Roles',
'Import::Kayako::User::GroupIds',
'Import::Kayako::User::OrganizationId',
'Common::ModelClass::User',
'Import::Kayako::User::Mapping',
'Import::Kayako::Mapping::Timestamps',
'Import::Kayako::Mapping::CustomFields',
'Import::Common::Model::Attributes::AddByIds',
'Import::Common::Model::FindBy::UserAttributes',
'Import::Common::Model::Update',
'Import::Common::Model::Create',
'Import::Common::Model::Save',
'Import::Kayako::MapId',
'Import::Common::Model::Statistics::Diff::ModelKey',
'Import::Common::ImportJob::Statistics::Update',
'Import::Common::ImportJob::Statistics::Store',
]
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Sequence
module Import
module Kayako
class UserField < Sequencer::Sequence::Base
def self.sequence
[
'Common::ModelClass::User',
'Import::Kayako::ObjectAttribute::Skip',
'Import::Kayako::ObjectAttribute::SanitizedName',
'Import::Kayako::ObjectAttribute::Config',
'Import::Kayako::ObjectAttribute::Add',
'Import::Kayako::ObjectAttribute::MigrationExecute',
'Import::Kayako::ObjectAttribute::FieldMap',
]
end
end
end
end
end
end

View file

@ -0,0 +1,14 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Common
module ModelClass
module ObjectManager
class Attribute < Sequencer::Unit::Common::ModelClass::Base
end
end
end
end
end
end

View file

@ -14,10 +14,6 @@ class Sequencer
uses :resource
def object
'Conversation'
end
def sequence_name
'Sequencer::Sequence::Import::Freshdesk::Conversations'.freeze
end

View file

@ -0,0 +1,22 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class CustomerId < Sequencer::Unit::Common::Provider::Named
uses :resource, :id_map
private
def customer_id
id_map['User'].fetch(resource['requester']&.fetch('id'), 1)
end
end
end
end
end
end
end

View file

@ -0,0 +1,22 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class GroupId < Sequencer::Unit::Common::Provider::Named
uses :resource, :id_map
private
def group_id
id_map['Group'].fetch(resource['assigned_team']&.fetch('id'), 1)
end
end
end
end
end
end
end

View file

@ -0,0 +1,37 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class Mapping < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :resource, :customer_id, :owner_id, :group_id, :organization_id, :priority_id, :state_id,
:created_by_id, :updated_by_id, :type
def process
provide_mapped do
{
id: resource['id'],
number: resource['id'],
title: resource['subject'],
owner_id: owner_id,
group_id: group_id,
customer_id: customer_id,
organization_id: organization_id,
priority_id: priority_id,
state_id: state_id,
type: type,
updated_by_id: updated_by_id,
created_by_id: created_by_id,
}
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,28 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class OrganizationId < Sequencer::Unit::Common::Provider::Named
uses :resource, :id_map
private
def organization_id
return if organization.nil?
id_map['Organization'][organization['id']]
end
def organization
resource['requester']&.fetch('organization')
end
end
end
end
end
end
end

View file

@ -0,0 +1,22 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class OwnerId < Sequencer::Unit::Common::Provider::Named
uses :resource, :id_map
private
def owner_id
id_map['User'].fetch(resource['assigned_agent']&.fetch('id'), 1)
end
end
end
end
end
end
end

View file

@ -0,0 +1,35 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class Posts < Sequencer::Unit::Import::Kayako::SubSequence::SubObject
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
optional :action
skip_action :skipped, :failed
uses :resource
def object
'Post'
end
def sequence_name
'Sequencer::Sequence::Import::Kayako::Posts'.freeze
end
def request_params
super.merge(
ticket: resource,
)
end
end
end
end
end
end
end

View file

@ -0,0 +1,36 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class PriorityId < Sequencer::Unit::Common::Provider::Named
uses :resource
private
def priority_id
::Ticket::Priority.select(:id).find_by(name: local).id
end
def local
mapping.fetch(resource['priority']&.fetch('level'), mapping[nil])
end
def mapping
{
1 => '1 low',
nil => '2 normal',
2 => '2 normal',
3 => '3 high',
4 => '3 high',
}.freeze
end
end
end
end
end
end
end

View file

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
module Skip
class Deleted < Sequencer::Unit::Base
uses :resource
provides :action
def process
return if resource['state'] != 'TRASH'
logger.info { "Skipping. Kayako Case ID '#{resource['id']}' is in 'TRASH' state." }
state.provide(:action, :skipped)
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
module Skip
class Suspended < Sequencer::Unit::Base
uses :resource
provides :action
def process
return if resource['state'] != 'SUSPENDED'
logger.info { "Skipping. Kayako Case ID '#{resource['id']}' is in 'SUSPENDED' state." }
state.provide(:action, :skipped)
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,37 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class StateId < Sequencer::Unit::Common::Provider::Named
uses :resource
private
def state_id
::Ticket::State.select(:id).find_by(name: local).id
end
def local
mapping.fetch(resource['status']['type'], 'open')
end
def mapping
{
'NEW' => 'new',
'OPEN' => 'open',
'PENDING' => 'pending reminder',
'COMPLETED' => 'closed',
'CLOSED' => 'closed',
'CUSTOM' => 'open',
}.freeze
end
end
end
end
end
end
end

View file

@ -0,0 +1,22 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class Tags < Sequencer::Unit::Common::Model::Tags
uses :resource
private
def tags
resource['tags']&.map { |tag| tag['name'] }
end
end
end
end
end
end
end

View file

@ -0,0 +1,23 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class Type < Sequencer::Unit::Common::Provider::Named
uses :resource
private
def type
type = resource['type']&.fetch('type')
type&.capitalize
end
end
end
end
end
end
end

View file

@ -0,0 +1,22 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Case
class UpdatedById < Sequencer::Unit::Common::Provider::Named
uses :resource, :id_map, :created_by_id
private
def updated_by_id
id_map['User'].fetch(resource['last_updated_by']&.fetch('id'), created_by_id)
end
end
end
end
end
end
end

View file

@ -0,0 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class CaseFields < Sequencer::Unit::Import::Kayako::SubSequence::Field
end
end
end
end
end

View file

@ -0,0 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Cases < Sequencer::Unit::Import::Kayako::SubSequence::Object
end
end
end
end
end

View file

@ -0,0 +1,33 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Common
class ArticleSenderId < Sequencer::Unit::Common::Provider::Named
uses :created_by_id
private
def article_sender_id
return article_sender('Customer') if author.role?('Customer')
return article_sender('Agent') if author.role?('Agent')
article_sender('System')
end
def author
@author ||= ::User.find(created_by_id)
end
def article_sender(name)
::Ticket::Article::Sender.select(:id).find_by(name: name).id
end
end
end
end
end
end
end

View file

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Common
class ArticleSourceChannel < Sequencer::Unit::Common::Provider::Named
uses :resource, :id_map
private
def article_source_channel
channel = resource['source_channel']&.fetch('type')
return if !channel
"Sequencer::Unit::Import::Kayako::Post::Channel::#{channel.capitalize}".constantize.new(resource)
end
end
end
end
end
end
end

View file

@ -0,0 +1,22 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Common
class CreatedById < Sequencer::Unit::Common::Provider::Named
uses :resource, :id_map
private
def created_by_id
id_map['User'].fetch(resource['creator']&.fetch('id'), 1)
end
end
end
end
end
end
end

View file

@ -0,0 +1,43 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class DefaultLanguage < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Kayako::Requester
provides :default_language
def process
state.provide(:default_language, default_language)
end
private
def default_language
settings = fetch_settings
default_language_setting = settings.detect { |item| item['name'] == 'default_language' }
default_language_setting['value'] || 'en-us'
end
def fetch_settings
response = request(
api_path: 'settings'
)
body = JSON.parse(response.body)
body['data']
rescue => e
logger.error 'Error when fetching settings for default language'
logger.error e
nil
end
end
end
end
end
end

View file

@ -0,0 +1,16 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class FieldMap < Sequencer::Unit::Common::Provider::Named
def field_map
{}
end
end
end
end
end
end

View file

@ -0,0 +1,16 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class IdMap < Sequencer::Unit::Common::Provider::Named
def id_map
{}
end
end
end
end
end
end

View file

@ -0,0 +1,16 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class ImportSettingsUnset < Sequencer::Unit::Base
def process
Setting.set('import_kayako_endpoint_username', nil)
Setting.set('import_kayako_endpoint_password', nil)
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class MapId < Sequencer::Unit::Base
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
optional :action
skip_action :skipped, :failed
uses :id_map, :model_class, :resource, :instance
def process
id_map[model_class.name] ||= {}
id_map[model_class.name][resource['id']] = instance.id
end
end
end
end
end
end

View file

@ -0,0 +1,48 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Mapping
class CustomFields < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :resource, :field_map, :model_class, :default_language
def process
provide_mapped do
custom_fields || {}
end
end
private
def custom_fields
resource['custom_fields']&.each_with_object({}) do |item, result|
field = item['field']
local_name = custom_fields_map[field['key']]
next if local_name.nil? || item['value'].empty?
field_type_instance = attribute_type_instance(field)
result[ local_name.to_sym ] = field_type_instance.local_value(item['value'])
end
end
def custom_fields_map
@custom_fields_map ||= field_map[model_class.name]
end
def attribute_type_instance(field)
"Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::#{field['type'].capitalize}".constantize.new(field, default_language)
rescue
nil
end
end
end
end
end
end
end

View file

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Mapping
class Timestamps < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :resource
def process
provide_mapped do
{
created_at: resource['created_at'],
updated_at: resource['updated_at'],
}
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,29 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class ModelClass < Sequencer::Unit::Common::Provider::Named
uses :object
MAP = {
'Organization' => ::Organization,
'User' => ::User,
'Team' => ::Group,
'Case' => ::Ticket,
'Post' => ::Ticket::Article,
'TimeEntry' => ::Ticket::TimeAccounting,
}.freeze
private
def model_class
MAP[object]
end
end
end
end
end
end

View file

@ -0,0 +1,23 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
class Add < Sequencer::Unit::Base
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
skip_any_action
uses :config
def process
ObjectManager::Attribute.add(config)
end
end
end
end
end
end
end

View file

@ -0,0 +1,91 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Base
attr_reader :attribute, :default_language
def initialize(attribute, default_language)
@attribute = attribute
@default_language = default_language
end
def config
{
display: attribute['title'],
data_type: data_type,
data_option: data_option,
editable: true,
active: attribute['is_enabled'],
screens: screens,
position: attribute['sort_order'],
created_by_id: 1,
updated_by_id: 1,
}
end
def local_value(value)
value
end
private
def screens
default = {
view: {
'-all-' => {
shown: true,
null: true,
},
Customer: {
shown: attribute['is_visible_to_customers'],
null: true,
},
},
edit: {
'-all-' => {
shown: true,
null: true,
},
Customer: {
shown: attribute['is_customer_editable'],
null: !attribute['is_required_for_customers'],
},
}
}
if attribute['is_required_for_agents']
default[:edit]['ticket.agent'] = {
shown: true,
required: true,
}
end
default
end
def data_option
{
null: true,
note: '',
}.merge(data_type_specific_options)
end
def data_type_specific_options
{}
end
def data_type
attribute['type'].downcase
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,64 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Cascadingselect < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Select
def local_value(value)
super.gsub('\\', '::')
end
private
def data_type
'tree_select'
end
def options
result = []
attribute['options'].each do |item|
locale_item = item['values'].detect { |value| value['locale'] == default_language }
next if locale_item['translation'].nil?
transformed_tree_path = locale_item['translation'].gsub('\\', '::')
process_option(transformed_tree_path, result)
end
result
end
def process_option(tree_path, current_options, parent_tree_path = nil)
fragments = tree_path.split('::')
current_fragment = fragments.shift
current_tree_path = parent_tree_path.nil? ? current_fragment : "#{parent_tree_path}::#{current_fragment}"
current_option = current_options.detect { |option| option[:value] == current_tree_path }
remaining_tree_path = fragments.join('::')
if current_option.nil?
current_options.push({ name: current_fragment, value: current_tree_path })
current_option = current_options.last
end
return if remaining_tree_path.empty?
current_option[:children] ||= []
process_option(remaining_tree_path, current_option[:children], current_tree_path)
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,25 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Date < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Base
private
def data_type_specific_options
{
future: true,
past: true,
diff: 0,
}
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,16 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Decimal < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Text
end
end
end
end
end
end
end

View file

@ -0,0 +1,28 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Numeric < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Base
private
def data_type
'integer'
end
def data_type_specific_options
{
min: 0,
max: 999_999_999,
}
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,16 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Radio < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Select
end
end
end
end
end
end
end

View file

@ -0,0 +1,23 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Regex < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Text
private
def data_type_specific_options
super.merge(
regex: attribute['regular_expression'],
)
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,44 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Select < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Base
def local_value(value)
option_value = attribute['options'].detect { |option| option['id'] == value.to_i }
value_locale = option_value['values'].detect { |locale_item| locale_item['locale'] == default_language }
value_locale['translation']
end
private
def data_type
'select'
end
def data_type_specific_options
{
default: '',
options: options,
}
end
def options
result = {}
attribute['options'].each do |item|
locale_item = item['values'].detect { |value| value['locale'] == default_language }
result[ locale_item['translation'] ] = locale_item['translation']
end
result
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,28 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Text < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Base
private
def data_type
'input'
end
def data_type_specific_options
{
type: 'text',
maxlength: 255,
}
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Textarea < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Base
private
def data_type_specific_options
{
type: 'textarea',
maxlength: 255,
}
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Type < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Select
private
def options
super.merge(
'Question' => 'Question',
'Task' => 'Task',
'Problem' => 'Problem',
'Incident' => 'Incident',
)
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,35 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
module AttributeType
class Yesno < Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::Base
def local_value(value)
value == 'yes'
end
private
def data_type
'boolean'
end
def data_type_specific_options
{
default: false,
options: {
true => 'yes',
false => 'no',
},
}
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,41 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
class Config < Sequencer::Unit::Base
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
skip_any_action
uses :resource, :sanitized_name, :model_class, :default_language
provides :config
def process
attribute_config = attribute_type.config
state.provide(:config) do
{
object: model_class.to_s,
name: sanitized_name,
}.merge(attribute_config)
end
rescue => e
logger.error "The custom field type '#{resource['type']}' can not be mapped to an internal field."
handle_failure(e)
end
private
def attribute_type
"Sequencer::Unit::Import::Kayako::ObjectAttribute::AttributeType::#{resource['type'].capitalize}".constantize.new(resource, default_language)
end
end
end
end
end
end
end

View file

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
class FieldMap < Sequencer::Unit::Base
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
skip_any_action
optional :action
uses :field_map, :model_class, :resource, :sanitized_name
def process
field_map[model_class.name] ||= {}
field_map[model_class.name][ resource['key'] ] = sanitized_name
end
end
end
end
end
end
end

View file

@ -0,0 +1,21 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
class MigrationExecute < Sequencer::Unit::Base
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
skip_any_action
def process
ObjectManager::Attribute.migration_execute(false)
end
end
end
end
end
end
end

View file

@ -0,0 +1,25 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
class SanitizedName < Sequencer::Unit::Import::Common::ObjectAttribute::SanitizedName
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
skip_any_action
uses :resource
private
def unsanitized_name
resource['key']
end
end
end
end
end
end
end

View file

@ -0,0 +1,33 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module ObjectAttribute
class Skip < Sequencer::Unit::Base
uses :resource
provides :action
def process
return if (!resource['is_system'] && skip_attribute_types.exclude?(resource['type'])) || allowed_system_attributes.include?(resource['key'])
state.provide(:action, :skipped)
end
private
def skip_attribute_types
@skip_attribute_types ||= %w[FILE CHECKBOX]
end
def allowed_system_attributes
@allowed_system_attributes ||= %w[type]
end
end
end
end
end
end
end

View file

@ -0,0 +1,32 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class ObjectCount < Sequencer::Unit::Common::Provider::Attribute
include ::Sequencer::Unit::Import::Common::Model::Statistics::Mixin::EmptyDiff
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
skip_action :skipped, :failed
uses :model_class, :resources
private
def statistics_diff
{
model_key => empty_diff.merge!(
total: resources.count
)
}
end
def model_key
model_class.name.pluralize.to_sym
end
end
end
end
end
end

View file

@ -0,0 +1,36 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Organization
class Mapping < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :resource
def process
provide_mapped do
{
name: resource['name'],
domain: domain,
domain_assignment: domain.present?,
}
end
end
private
def domain
@domain ||= begin
primary_domain = resource['domains']&.detect { |item| item['is_primary'] }
primary_domain&.fetch('domain')
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class OrganizationFields < Sequencer::Unit::Import::Kayako::SubSequence::Field
end
end
end
end
end

View file

@ -0,0 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Organizations < Sequencer::Unit::Import::Kayako::SubSequence::Object
end
end
end
end
end

View file

@ -0,0 +1,33 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Perform < Sequencer::Unit::Base
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
skip_action :skipped, :failed
uses :resources, :object, :import_job, :dry_run, :field_map, :id_map, :default_language
optional :instance
def process
resources.each do |resource|
::Sequencer.process("Import::Kayako::#{object}",
parameters: {
import_job: import_job,
dry_run: dry_run,
resource: resource,
default_language: default_language,
field_map: field_map,
id_map: id_map,
instance: instance,
})
end
end
end
end
end
end
end

View file

@ -0,0 +1,71 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
class Attachments < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Kayako::Requester
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
optional :action
skip_action :skipped, :failed
uses :resource, :instance, :model_class, :dry_run
def self.mutex
@mutex ||= Mutex.new
end
def process
return if resource['attachments'].blank?
download_threads.each(&:join)
end
private
def download_threads
resource['attachments'].map do |attachment|
Thread.new do
sync(attachment)
end
end
end
def sync(attachment)
logger.debug { "Downloading attachment #{attachment}" }
response = request(
api_path: attachment['url_download'].gsub("#{Setting.get('import_kayako_endpoint')}/", ''),
)
return if dry_run
store_attachment(attachment, response)
rescue => e
logger.error(e)
end
def store_attachment(attachment, response)
self.class.mutex.synchronize do
::Store.add(
object: model_class.name,
o_id: instance.id,
data: response.body,
filename: attachment['name'],
preferences: {
'Content-Type' => attachment['type']
},
created_by_id: 1
)
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,60 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
module Channel
class Base
attr_reader :resource
def initialize(resource)
@resource = resource
end
def mapping
{
message_id: resource['id'],
internal: internal?,
from: from,
type_id: article_type_id,
}
end
def article_type_id
return if article_type_name.nil?
::Ticket::Article::Type.select(:id).find_by(name: article_type_name).id
end
private
def internal?
false
end
def original_post
resource['original']
end
def article_type_name
raise NotImplementedError
end
def identify_key
raise NotImplementedError
end
def from
return if resource['identity'].nil?
resource['identity'][identify_key]
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,42 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
module Channel
class Facebook < Sequencer::Unit::Import::Kayako::Post::Channel::Base
def mapping
super.merge(
message_id: original_post['id'],
to: to,
body: original_post['contents'],
)
end
private
def article_type_name
return 'facebook direct-message' if original_post['resource_type'] == 'facebook_message'
return 'facebook feed comment' if original_post['resource_type'] == 'facebook_post_comment'
'facebook feed post'
end
def identify_key
'facebook_id'
end
def to
return if original_post['recipient'].nil?
original_post['recipient'][identify_key]
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,21 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
module Channel
class Helpcenter < Sequencer::Unit::Import::Kayako::Post::Channel::Mail
private
def article_type_name
'web'
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,65 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
module Channel
class Mail < Sequencer::Unit::Import::Kayako::Post::Channel::Base
def mapping
super.merge(
to: to,
cc: cc,
body: original_post['body_html'] || original_post['body_text'] || '',
content_type: 'text/html',
)
end
private
def article_type_name
'email'
end
def identify_key
'email'
end
def from
return super if resource['is_requester'] || original_post['mailbox'].blank?
original_post['mailbox']['address']
end
def to
recipients = build_recipients('TO')
# Add the mailbox address to the 'TO' field if it's a requester post.
if resource['is_requester'] && original_post['mailbox'].present?
recipients = "#{original_post['mailbox']['address']}#{", #{recipients}" if recipients.present?}"
end
recipients
end
def cc
build_recipients('CC')
end
def build_recipients(field_type)
return if !original_post.key?('recipients') || original_post['recipients'].empty?
original_post['recipients'].filter_map do |recipient|
next if recipient['type'] != field_type
recipient[identify_key]
end.join(', ')
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,21 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
module Channel
class Messenger < Sequencer::Unit::Import::Kayako::Post::Channel::Mail
private
def article_type_name
'chat'
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,36 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
module Channel
class Note < Sequencer::Unit::Import::Kayako::Post::Channel::Base
def mapping
super.merge(
body: original_post['body_html'] || original_post['body_text'] || '',
content_type: 'text/html',
)
end
private
def identify_key
'email'
end
def article_type_name
'note'
end
def internal?
true
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,41 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
module Channel
class Twitter < Sequencer::Unit::Import::Kayako::Post::Channel::Base
def mapping
super.merge(
message_id: original_post['id'],
to: to,
body: original_post['contents'],
)
end
private
def article_type_name
return 'twitter direct-message' if original_post['resource_type'] == 'twitter_message'
'twitter status'
end
def identify_key
'screen_name'
end
def to
return if original_post['recipient'].nil?
original_post['recipient'][identify_key]
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
class EmailAddressFields < Sequencer::Unit::Base
uses :resource, :id_map
provides :from, :to, :cc
def process
state.provide(:user_id) do
user_map.fetch(resource.author_id, 1)
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,77 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
class InlineImages < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :mapped
def process
return if !contains_inline_image?(mapped[:body])
provide_mapped do
{
body: replaced_inline_images,
}
end
end
def self.inline_data(kayako_url)
@cache ||= {}
return @cache[kayako_url] if @cache[kayako_url]
image_data = download(kayako_url)
return if image_data.blank?
@cache[kayako_url] = "data:image/png;base64,#{Base64.strict_encode64(image_data)}"
@cache[kayako_url]
end
def self.download(kayako_url)
logger.debug { "Downloading inline image from #{kayako_url}" }
response = UserAgent.get(
kayako_url,
{},
{
open_timeout: 20,
read_timeout: 240,
verify_ssl: true,
},
)
return response.body if response.success?
logger.error response.error
nil
end
private
def contains_inline_image?(string)
return false if string.blank?
string.include?('kayako.com/media/url')
end
def replaced_inline_images
body_html = Nokogiri::HTML(mapped[:body])
body_html.css('img').each do |node|
next if !contains_inline_image?(node['src'])
node.attributes['src'].value = self.class.inline_data(node['src'])
end
body_html.to_html
end
end
end
end
end
end
end

View file

@ -0,0 +1,29 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
class Mapping < Sequencer::Unit::Base
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :instance, :resource, :created_by_id, :article_sender_id, :article_source_channel
provides :mapped
def process
provide_mapped do
{
ticket_id: instance.id,
sender_id: article_sender_id,
created_by_id: created_by_id,
updated_by_id: created_by_id,
}.merge(article_source_channel.mapping)
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,15 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Post
class UnsetInstance < Sequencer::Unit::Common::UnsetAttributes
uses :instance
end
end
end
end
end
end

View file

@ -0,0 +1,40 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Request < Sequencer::Unit::Common::Provider::Attribute
extend ::Sequencer::Unit::Import::Kayako::Requester
uses :object, :request_params
provides :response
private
def response
builder = backend.new(
object: object,
request_params: request_params
)
self.class.request(
api_path: builder.api_path,
params: builder.params,
)
end
def backend
request_class = "::Sequencer::Unit::Import::Kayako::Request::#{object}".safe_constantize
return request_class if request_class.present?
return ::Sequencer::Unit::Import::Kayako::Request::GenericField if object.include?('Field')
::Sequencer::Unit::Import::Kayako::Request::Generic
end
end
end
end
end
end

View file

@ -0,0 +1,20 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Request < Sequencer::Unit::Common::Provider::Attribute
class Case < Sequencer::Unit::Import::Kayako::Request::Generic
def params
super.merge(
include: 'user,case_priority,case_status,channel,tag,case_type',
fields: '+tags',
)
end
end
end
end
end
end
end

View file

@ -0,0 +1,30 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Request < Sequencer::Unit::Common::Provider::Attribute
class Generic
attr_reader :object, :request_params
def initialize(object:, request_params:)
@object = object
@request_params = request_params
end
def api_path
object.underscore.split('_').map(&:pluralize).join('/')
end
def params
request_params.merge(
limit: 100,
)
end
end
end
end
end
end
end

View file

@ -0,0 +1,19 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Request < Sequencer::Unit::Common::Provider::Attribute
class GenericField < Sequencer::Unit::Import::Kayako::Request::Generic
def params
super.merge(
include: 'field_option,locale_field',
)
end
end
end
end
end
end
end

View file

@ -0,0 +1,19 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Request < Sequencer::Unit::Common::Provider::Attribute
class Organization < Sequencer::Unit::Import::Kayako::Request::Generic
def params
super.merge(
include: 'organization_field,field_option,locale_field,identity_domain',
)
end
end
end
end
end
end
end

View file

@ -0,0 +1,30 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Request < Sequencer::Unit::Common::Provider::Attribute
class Post < Sequencer::Unit::Import::Kayako::Request::Generic
attr_reader :ticket
def initialize(...)
super
@ticket = request_params.delete(:ticket)
end
def api_path
"cases/#{ticket['id']}/posts"
end
def params
super.merge(
include: 'mailbox,message_recipient,channel,attachment,case_message,note,chat_message,identity_email,identity_twitter,identity_facebook,facebook_message,facebook_post,facebook_post_comment,twitter_message,twitter_tweet',
)
end
end
end
end
end
end
end

View file

@ -0,0 +1,17 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Request < Sequencer::Unit::Common::Provider::Attribute
class TimeEntry < Sequencer::Unit::Import::Kayako::Request::Generic
def api_path
'timetracking'
end
end
end
end
end
end
end

View file

@ -0,0 +1,19 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Request < Sequencer::Unit::Common::Provider::Attribute
class User < Sequencer::Unit::Import::Kayako::Request::Generic
def params
super.merge(
include: 'user_field,field_option,locale_field,identity_email,identify_phone,identity_twitter,identity_facebook,role',
)
end
end
end
end
end
end
end

View file

@ -0,0 +1,86 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module Requester
mattr_accessor :session_id
def request(api_path:, params: nil)
10.times do |iteration|
response = perform_request(
api_path: api_path,
params: params,
)
if response.is_a? Net::HTTPOK
refresh_session_id(response)
return response
end
handle_error response, iteration
rescue Net::HTTPClientError => e
handle_exception e, iteration
end
nil
end
def handle_error(response, iteration)
reset_session_id if response.is_a? Net::HTTPUnauthorized
sleep_for = 10
case response
when Net::HTTPTooManyRequests
sleep_for = response.header['retry-after'].to_i + 10
logger.info "Rate limit: #{response.header.to_hash} (429 Too Many Requests). Sleeping #{sleep_for} seconds and retry (##{iteration + 1}/10)."
else
logger.info "Unknown response: #{response.inspect}. Sleeping 10 seconds and retry (##{iteration + 1}/10)."
end
sleep sleep_for
end
def handle_exception(e, iteration)
logger.error e
logger.info "Sleeping 10 seconds after #{e.class.name} and retry (##{iteration + 1}/10)."
sleep 10
end
def refresh_session_id(response)
return if response.header['content-type'] != 'application/json'
body = JSON.parse(response.body)
return if body['session_id'].blank?
self.session_id = body['session_id']
end
def reset_session_id
self.session_id = nil
end
def perform_request(api_path:, params: nil)
uri = URI("#{Setting.get('import_kayako_endpoint')}/#{api_path}")
uri.query = URI.encode_www_form(params) if params.present?
headers = {
'Content-Type' => 'application/json',
'X-Session-ID' => session_id
}
Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 600) do |http|
# for those special moments...
# http.set_debug_output($stdout)
request = Net::HTTP::Get.new(uri, headers)
if session_id.blank?
request.basic_auth(Setting.get('import_kayako_endpoint_username'), Setting.get('import_kayako_endpoint_password'))
end
return http.request(request)
end
end
end
end
end
end
end

View file

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
class Resources < Sequencer::Unit::Common::Provider::Named
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
uses :response
private
def resources
body = JSON.parse(response.body)
body['data']
rescue => e
logger.error "Won't be continued, because no response is available."
handle_failure(e)
end
end
end
end
end
end

View file

@ -0,0 +1,18 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module SubSequence
class Field < Sequencer::Unit::Import::Kayako::SubSequence::Generic
def sequence_name
'Sequencer::Sequence::Import::Kayako::GenericField'.freeze
end
end
end
end
end
end
end

View file

@ -0,0 +1,99 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Kayako
module SubSequence
class Generic < Sequencer::Unit::Base
uses :dry_run, :import_job, :field_map, :id_map, :default_language
attr_accessor :iteration, :result
EXPECTING = %i[action response].freeze
def process
loop.each_with_index do |_, iteration|
@iteration = iteration
@result = ::Sequencer.process(sequence_name,
parameters: sequence_params,
expecting: self.class.const_get(:EXPECTING))
break if iteration_should_stop?
end
end
def sequence_params
{
request_params: request_params,
import_job: import_job,
dry_run: dry_run,
object: object,
default_language: default_language,
field_map: field_map,
id_map: id_map,
}
end
def request_params
return {} if iteration.zero?
if cursor_pagination?
return cursor_pagination
end
offset_pagination
end
def object
@object ||= self.class.name.demodulize.singularize
end
def sequence_name
raise NotImplementedError
end
private
def offset_pagination
{
offset: offset,
}
end
def offset
iteration * 5 # TODO: only ddebug, normally 100
end
def cursor_pagination?
return if result.nil?
@cursor_pagination ||= result[:response].header['link'].include?('after_id')
end
def cursor_pagination
{
after_id: cursor_after_id
}
end
def cursor_after_id
unescaped_header_next_link.match(%r{after_id=(\d+)})[1]
end
def unescaped_header_next_link
CGI.unescape(CGI.unescape(result[:response].header['link']))
end
def iteration_should_stop?
return true if result[:action] == :failed
return true if result[:response].header['link'].blank? || result[:response].header['link'].exclude?('rel="next"')
false
end
end
end
end
end
end
end

Some files were not shown because too many files have changed in this diff Show more