diff --git a/Gemfile b/Gemfile index 0bc92366e..6fc764e0c 100644 --- a/Gemfile +++ b/Gemfile @@ -42,6 +42,7 @@ gem 'omniauth-linkedin-oauth2' gem 'omniauth-twitter' gem 'twitter' +gem 'telegramAPI' gem 'koala' gem 'mail' gem 'email_verifier' diff --git a/Gemfile.lock b/Gemfile.lock index 8de6cbc06..b2d2e72c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -198,6 +198,7 @@ GEM nenv (0.3.0) nestful (1.1.1) net-ldap (0.15.0) + netrc (0.11.0) nokogiri (1.6.8) mini_portile2 (~> 2.1.0) pkg-config (~> 1.1.7) @@ -293,6 +294,10 @@ GEM rb-inotify (0.9.7) ffi (>= 0.5.0) ref (2.0.0) + rest-client (2.0.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) retriable (2.1.0) rspec-core (3.5.4) rspec-support (~> 3.5.0) @@ -358,6 +363,8 @@ GEM activesupport (>= 4.0) sprockets (>= 3.0.0) sqlite3 (1.3.11) + telegramAPI (1.2.2) + rest-client (~> 2.0, >= 1.7.3) term-ansicolor (1.3.2) tins (~> 1.0) test-unit (3.2.1) @@ -467,6 +474,7 @@ DEPENDENCIES spring-commands-rspec sprockets sqlite3 + telegramAPI test-unit therubyracer twitter diff --git a/app/assets/javascripts/app/controllers/_channel/email.coffee b/app/assets/javascripts/app/controllers/_channel/email.coffee index 586caf4a7..f37c3e2fa 100644 --- a/app/assets/javascripts/app/controllers/_channel/email.coffee +++ b/app/assets/javascripts/app/controllers/_channel/email.coffee @@ -243,7 +243,7 @@ class App.ChannelEmailAccountOverview extends App.Controller @ajax( id: 'email_index' type: 'GET' - url: @apiPath + '/channels/email_index' + url: "#{@apiPath}/channels_email" processData: true success: (data, status, xhr) => @stopLoading() @@ -324,34 +324,44 @@ class App.ChannelEmailAccountOverview extends App.Controller delete: (e) => e.preventDefault() id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - new App.ControllerGenericDestroyConfirm( - item: item + new App.ControllerConfirm( + message: 'Sure?' + callback: => + @ajax( + id: 'email_delete' + type: 'DELETE' + url: "#{@apiPath}/channels_email" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + ) container: @el.closest('.content') - callback: @load ) disable: (e) => e.preventDefault() id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - item.active = false - item.save( - done: => - @load() - fail: => + @ajax( + id: 'email_disable' + type: 'POST' + url: "#{@apiPath}/channels_email_disable" + data: JSON.stringify(id: id) + processData: true + success: => @load() ) enable: (e) => e.preventDefault() id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - item.active = true - item.save( - done: => - @load() - fail: => + @ajax( + id: 'email_enable' + type: 'POST' + url: "#{@apiPath}/channels_email_enable" + data: JSON.stringify(id: id) + processData: true + success: => @load() ) @@ -441,7 +451,7 @@ class App.ChannelEmailEdit extends App.ControllerModal # show errors in form if errors @log 'error', errors - @formValidate( form: e.target, errors: errors ) + @formValidate(form: e.target, errors: errors) return false # disable form @@ -449,16 +459,18 @@ class App.ChannelEmailEdit extends App.ControllerModal # update @ajax( - id: 'channel_group_update' + id: 'channel_email_group' type: 'POST' - url: "#{@apiPath}/channels/group/#{@item.id}" - data: JSON.stringify( params ) + url: "#{@apiPath}/channels_email_group/#{@item.id}" + data: JSON.stringify(params) processData: true success: (data, status, xhr) => @callback() @close() - fail: => + error: (xhr) => + data = JSON.parse(xhr.responseText) @formEnable(e) + @el.find('.alert').removeClass('hidden').text(data.error || 'Unable to save changes.') ) class App.ChannelEmailAccountWizard extends App.WizardModal @@ -651,7 +663,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal @ajax( id: 'email_probe' type: 'POST' - url: @apiPath + '/channels/email_probe' + url: "#{@apiPath}/channels_email_probe" data: JSON.stringify(params) processData: true success: (data, status, xhr) => @@ -683,7 +695,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal @$('.js-inbound [name="options::password"]').val(@account['meta']['password']) @enable(e) - fail: => + error: => @enable(e) @showSlide('js-intro') ) @@ -705,7 +717,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal @ajax( id: 'email_inbound' type: 'POST' - url: @apiPath + '/channels/email_inbound' + url: "#{@apiPath}/channels_email_inbound" data: JSON.stringify(params) processData: true success: (data, status, xhr) => @@ -738,9 +750,10 @@ class App.ChannelEmailAccountWizard extends App.WizardModal @showAlert('js-inbound', data.message_human || data.message) @showInvalidField('js-inbound', data.invalid_field) @enable(e) - fail: => + error: (xhr) => + data = JSON.parse(xhr.responseText) @showSlide('js-inbound') - @showAlert('js-inbound', data.message_human || data.message) + @showAlert('js-inbound', data.message_human || data.message || data.error) @showInvalidField('js-inbound', data.invalid_field) @enable(e) ) @@ -768,8 +781,8 @@ class App.ChannelEmailAccountWizard extends App.WizardModal @ajax( id: 'email_outbound' type: 'POST' - url: @apiPath + '/channels/email_outbound' - data: JSON.stringify( params ) + url: "#{@apiPath}/channels_email_outbound" + data: JSON.stringify(params) processData: true success: (data, status, xhr) => if data.result is 'ok' @@ -783,9 +796,10 @@ class App.ChannelEmailAccountWizard extends App.WizardModal @showAlert('js-outbound', data.message_human || data.message) @showInvalidField('js-outbound', data.invalid_field) @enable(e) - fail: => + error: (xhr) => + data = JSON.parse(xhr.responseText) @showSlide('js-outbound') - @showAlert('js-outbound', data.message_human || data.message) + @showAlert('js-outbound', data.message_human || data.message || data.error) @showInvalidField('js-outbound', data.invalid_field) @enable(e) ) @@ -810,7 +824,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal @ajax( id: 'email_verify' type: 'POST' - url: @apiPath + '/channels/email_verify' + url: "#{@apiPath}/channels_email_verify" data: JSON.stringify(account) processData: true success: (data, status, xhr) => @@ -835,7 +849,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal if data.subject && @account @account.subject = data.subject @verify(@account, count + 1) - fail: => + error: => @showSlide('js-intro') @showAlert('js-intro', 'Unable to verify sending and receiving. Please check your settings.') ) @@ -946,7 +960,7 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal @ajax( id: 'email_outbound' type: 'POST' - url: "#{@apiPath}/channels/email_notification" + url: "#{@apiPath}/channels_email_notification" data: JSON.stringify(params) processData: true success: (data, status, xhr) => @@ -957,9 +971,10 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal @showAlert('js-outbound', data.message_human || data.message) @showInvalidField('js-outbound', data.invalid_field) @enable(e) - fail: => + error: (xhr) => + data = JSON.parse(xhr.responseText) @showSlide('js-outbound') - @showAlert('js-outbound', data.message_human || data.message) + @showAlert('js-outbound', data.message_human || data.message || data.error) @showInvalidField('js-outbound', data.invalid_field) @enable(e) ) diff --git a/app/assets/javascripts/app/controllers/_channel/facebook.coffee b/app/assets/javascripts/app/controllers/_channel/facebook.coffee index 53634bde9..10e1eabfb 100644 --- a/app/assets/javascripts/app/controllers/_channel/facebook.coffee +++ b/app/assets/javascripts/app/controllers/_channel/facebook.coffee @@ -20,7 +20,7 @@ class Index extends App.ControllerSubContent @ajax( id: 'facebook_index' type: 'GET' - url: "#{@apiPath}/channels/facebook_index" + url: "#{@apiPath}/channels_facebook" processData: true success: (data, status, xhr) => @stopLoading() @@ -62,9 +62,6 @@ class Index extends App.ControllerSubContent @html App.view('facebook/list')( channels: channels ) - # accounts: accounts - # showDescription: showDescription - # description: description if @channel_id @edit(undefined, @channel_id) @@ -103,41 +100,45 @@ class Index extends App.ControllerSubContent delete: (e) => e.preventDefault() id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - new App.ControllerGenericDestroyConfirm( - item: item + new App.ControllerConfirm( + message: 'Sure?' + callback: => + @ajax( + id: 'facebook_delete' + type: 'DELETE' + url: "#{@apiPath}/channels_facebook" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + ) container: @el.closest('.content') - callback: @load ) disable: (e) => e.preventDefault() id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - item.active = false - item.save( - done: => - @load() - fail: => + @ajax( + id: 'facebook_disable' + type: 'POST' + url: "#{@apiPath}/channels_facebook_disable" + data: JSON.stringify(id: id) + processData: true + success: => @load() ) enable: (e) => e.preventDefault() id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - item.active = true - item.save( - done: => + @ajax( + id: 'facebook_enable' + type: 'POST' + url: "#{@apiPath}/channels_facebook_enable" + data: JSON.stringify(id: id) + processData: true + success: => @load() - fail: => - @load() - ) - - description: (e) => - new App.ControllerGenericDescription( - description: App.Twitter.description - container: @el.closest('.content') ) class AppConfig extends App.ControllerModal @@ -182,7 +183,7 @@ class AppConfig extends App.ControllerModal done: => @isChanged = true @close() - fail: -> + fail: => @el.find('.alert').removeClass('hidden').text('Unable to create entry.') ) return @@ -241,14 +242,16 @@ class AccountEdit extends App.ControllerModal @ajax( id: 'channel_facebook_update' type: 'POST' - url: "#{@apiPath}/channels/facebook_verify/#{@channel.id}" + url: "#{@apiPath}/channels_facebook/#{@channel.id}" data: JSON.stringify(@channel.attributes()) processData: true success: (data, status, xhr) => @isChanged = true @close() - fail: => + error: (xhr) => + data = JSON.parse(xhr.responseText) @formEnable(e) + @el.find('.alert').removeClass('hidden').text(data.error || 'Unable to save changes.') ) App.Config.set('Facebook', { prio: 5100, name: 'Facebook', parent: '#channels', target: '#channels/facebook', controller: Index, permission: ['admin.channel_facebook'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/_channel/telegram.coffee b/app/assets/javascripts/app/controllers/_channel/telegram.coffee new file mode 100644 index 000000000..39992ae48 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_channel/telegram.coffee @@ -0,0 +1,202 @@ +class Index extends App.ControllerSubContent + requiredPermission: 'admin.channel_telegram' + events: + 'click .js-new': 'new' + 'click .js-edit': 'edit' + 'click .js-delete': 'delete' + 'click .js-disable': 'disable' + 'click .js-enable': 'enable' + + constructor: -> + super + + #@interval(@load, 60000) + @load() + + load: => + @startLoading() + @ajax( + id: 'telegram_index' + type: 'GET' + url: "#{@apiPath}/channels_telegram" + processData: true + success: (data) => + @stopLoading() + App.Collection.loadAssets(data.assets) + @render(data) + ) + + render: (data) => + + channels = [] + for channel_id in data.channel_ids + channel = App.Channel.find(channel_id) + if channel && channel.options + displayName = '-' + if channel.group_id + group = App.Group.find(channel.group_id) + displayName = group.displayName() + channel.options.groupName = displayName + channels.push channel + @html App.view('telegram/index')( + channels: channels + ) + + new: (e) => + e.preventDefault() + new BotAdd( + container: @el.parents('.content') + load: @load + ) + + edit: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + channel = App.Channel.find(id) + new BotEdit( + channel: channel + container: @el.parents('.content') + load: @load + ) + + delete: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + new App.ControllerConfirm( + message: 'Sure?' + callback: => + @ajax( + id: 'telegram_delete' + type: 'DELETE' + url: "#{@apiPath}/channels_telegram" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + ) + container: @el.closest('.content') + ) + + disable: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + @ajax( + id: 'telegram_disable' + type: 'POST' + url: "#{@apiPath}/channels_telegram_disable" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + ) + + enable: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + @ajax( + id: 'telegram_enable' + type: 'POST' + url: "#{@apiPath}/channels_telegram_enable" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + ) + +class BotAdd extends App.ControllerModal + head: 'Add Telegram Bot' + shown: true + button: 'Add' + buttonCancel: true + small: true + + content: -> + content = $(App.view('telegram/bot_add')()) + createGroupSelection = (selected_id) -> + return App.UiElement.select.render( + name: 'group_id' + multiple: false + limit: 100 + null: false + relation: 'Group' + nulloption: true + value: selected_id + class: 'form-control--small' + ) + + content.find('.js-select').on('click', (e) => + @selectAll(e) + ) + content.find('.js-messagesGroup').replaceWith createGroupSelection(1) + content + + onClosed: => + return if !@isChanged + @isChanged = false + @load() + + onSubmit: (e) => + @formDisable(e) + @ajax( + id: 'telegram_app_verify' + type: 'POST' + url: "#{@apiPath}/channels_telegram" + data: JSON.stringify(@formParams()) + processData: true + success: => + @isChanged = true + @close() + error: (xhr) => + data = JSON.parse(xhr.responseText) + @formEnable(e) + @el.find('.alert').removeClass('hidden').text(data.error || 'Unable to save Bot.') + ) + +class BotEdit extends App.ControllerModal + head: 'Telegram Account' + shown: true + buttonCancel: true + + content: -> + content = $(App.view('telegram/bot_edit')(channel: @channel)) + + createGroupSelection = (selected_id) -> + return App.UiElement.select.render( + name: 'group_id' + multiple: false + limit: 100 + null: false + relation: 'Group' + nulloption: true + value: selected_id + class: 'form-control--small' + ) + + content.find('.js-messagesGroup').replaceWith createGroupSelection(@channel.group_id) + content + + onClosed: => + return if !@isChanged + @isChanged = false + @load() + + onSubmit: (e) => + @formDisable(e) + params = @formParams() + @channel.options = params + @ajax( + id: 'channel_telegram_update' + type: 'PUT' + url: "#{@apiPath}/channels_telegram/#{@channel.id}" + data: JSON.stringify(@formParams()) + processData: true + success: => + @isChanged = true + @close() + error: (xhr) => + data = JSON.parse(xhr.responseText) + @formEnable(e) + @el.find('.alert').removeClass('hidden').text(data.error || 'Unable to save changes.') + ) + +App.Config.set('Telegram', { prio: 5100, name: 'Telegram', parent: '#channels', target: '#channels/telegram', controller: Index, permission: ['admin.channel_telegram'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/_channel/twitter.coffee b/app/assets/javascripts/app/controllers/_channel/twitter.coffee index 77243b001..2e756c0f1 100644 --- a/app/assets/javascripts/app/controllers/_channel/twitter.coffee +++ b/app/assets/javascripts/app/controllers/_channel/twitter.coffee @@ -19,7 +19,7 @@ class Index extends App.ControllerSubContent @ajax( id: 'twitter_index' type: 'GET' - url: "#{@apiPath}/channels/twitter_index" + url: "#{@apiPath}/channels_twitter" processData: true success: (data, status, xhr) => @stopLoading() @@ -61,9 +61,6 @@ class Index extends App.ControllerSubContent @html App.view('twitter/list')( channels: channels ) - # accounts: accounts - # showDescription: showDescription - # description: description if @channel_id @edit(undefined, @channel_id) @@ -102,41 +99,45 @@ class Index extends App.ControllerSubContent delete: (e) => e.preventDefault() id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - new App.ControllerGenericDestroyConfirm( - item: item + new App.ControllerConfirm( + message: 'Sure?' + callback: => + @ajax( + id: 'twitter_delete' + type: 'DELETE' + url: "#{@apiPath}/channels_twitter" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + ) container: @el.closest('.content') - callback: @load ) disable: (e) => e.preventDefault() id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - item.active = false - item.save( - done: => - @load() - fail: => + @ajax( + id: 'twitter_disable' + type: 'POST' + url: "#{@apiPath}/channels_twitter_disable" + data: JSON.stringify(id: id) + processData: true + success: => @load() ) enable: (e) => e.preventDefault() id = $(e.target).closest('.action').data('id') - item = App.Channel.find(id) - item.active = true - item.save( - done: => + @ajax( + id: 'twitter_enable' + type: 'POST' + url: "#{@apiPath}/channels_twitter_enable" + data: JSON.stringify(id: id) + processData: true + success: => @load() - fail: => - @load() - ) - - description: (e) => - new App.ControllerGenericDescription( - description: App.Twitter.description - container: @el.closest('.content') ) class AppConfig extends App.ControllerModal @@ -277,14 +278,16 @@ class AccountEdit extends App.ControllerModal @ajax( id: 'channel_twitter_update' type: 'POST' - url: "#{@apiPath}/channels/twitter_verify/#{@channel.id}" + url: "#{@apiPath}/channels_twitter/#{@channel.id}" data: JSON.stringify(@channel.attributes()) processData: true success: (data, status, xhr) => @isChanged = true @close() - fail: => + error: (xhr) => + data = JSON.parse(xhr.responseText) @formEnable(e) + @el.find('.alert').removeClass('hidden').text(data.error || 'Unable to save changes.') ) App.Config.set('Twitter', { prio: 5000, name: 'Twitter', parent: '#channels', target: '#channels/twitter', controller: Index, permission: ['admin.channel_twitter'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee index 1f01e188b..081d9ad7b 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee @@ -1,13 +1,14 @@ class App.TicketZoomArticleActions extends App.Controller events: - 'click [data-type=public]': 'publicInternal' - 'click [data-type=internal]': 'publicInternal' - 'click [data-type=emailReply]': 'emailReply' - 'click [data-type=emailReplyAll]': 'emailReplyAll' - 'click [data-type=twitterStatusReply]': 'twitterStatusReply' - 'click [data-type=twitterDirectMessageReply]': 'twitterDirectMessageReply' - 'click [data-type=facebookFeedReply]': 'facebookFeedReply' - 'click [data-type=delete]': 'delete' + 'click [data-type=public]': 'publicInternal' + 'click [data-type=internal]': 'publicInternal' + 'click [data-type=emailReply]': 'emailReply' + 'click [data-type=emailReplyAll]': 'emailReplyAll' + 'click [data-type=twitterStatusReply]': 'twitterStatusReply' + 'click [data-type=twitterDirectMessageReply]': 'twitterDirectMessageReply' + 'click [data-type=facebookFeedReply]': 'facebookFeedReply' + 'click [data-type=telegramPersonalMessageReply]': 'telegramPersonalMessageReply' + 'click [data-type=delete]': 'delete' constructor: -> super @@ -151,6 +152,13 @@ class App.TicketZoomArticleActions extends App.Controller icon: 'reply' href: '#' } + if article.sender.name is 'Customer' && article.type.name is 'telegram personal-message' + actions.push { + name: 'reply' + type: 'telegramPersonalMessageReply' + icon: 'reply' + href: '#' + } actions.push { name: 'split' @@ -399,6 +407,34 @@ class App.TicketZoomArticleActions extends App.Controller App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } ) + telegramPersonalMessageReply: (e) => + e.preventDefault() + + # get reference article + article_id = $(e.target).parents('[data-id]').data('id') + article = App.TicketArticle.fullLocal(article_id) + sender = App.TicketArticleSender.find(article.sender_id) + type = App.TicketArticleType.find(article.type_id) + customer = App.User.find(article.created_by_id) + + @scrollToCompose() + + # empty form + articleNew = { + to: '' + cc: '' + body: '' + in_reply_to: '' + } + + if article.message_id + articleNew.in_reply_to = article.message_id + + # get current body + articleNew.body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || '' + + App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew, position: 'end' } ) + delete: (e) => e.preventDefault() diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index 7566674b3..0782d2f3f 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -42,6 +42,8 @@ class App.TicketZoomArticleNew extends App.Controller possibleArticleType['email'] = true else if articleTypeCreate is 'facebook feed post' possibleArticleType['facebook feed comment'] = true + else if articleTypeCreate is 'telegram personal-message' + possibleArticleType['telegram personal-message'] = true if @ticket && @ticket.customer_id customer = App.User.find(@ticket.customer_id) if customer.email @@ -105,6 +107,16 @@ class App.TicketZoomArticleNew extends App.Controller internal: false, features: ['attachment'] } + if possibleArticleType['telegram personal-message'] + @articleTypes.push { + name: 'telegram personal-message' + icon: 'telegram' + attributes: [] + internal: false, + features: ['attachment'] + maxTextLength: 10000 + warningTextLength: 5000 + } if @permissionCheck('ticket.customer') @type = 'note' @@ -335,6 +347,11 @@ class App.TicketZoomArticleNew extends App.Controller params.content_type = 'text/plain' params.body = App.Utils.html2text(params.body, true) + if params.type is 'telegram personal-message' + App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false) + params.content_type = 'text/plain' + params.body = App.Utils.html2text(params.body, true) + params validate: => @@ -499,7 +516,7 @@ class App.TicketZoomArticleNew extends App.Controller @$('[data-name=body] [data-signature=true]').remove() # remove richtext - if @type is 'twitter status' || @type is 'twitter direct-message' + if @type is 'twitter status' || @type is 'twitter direct-message' || @type is 'telegram personal-message' rawHTML = @$('[data-name=body]').html() cleanHTML = App.Utils.htmlRemoveRichtext(rawHTML) if cleanHTML && cleanHTML.html() != rawHTML diff --git a/app/assets/javascripts/app/views/telegram/bot_add.jst.eco b/app/assets/javascripts/app/views/telegram/bot_add.jst.eco new file mode 100644 index 000000000..4626b023f --- /dev/null +++ b/app/assets/javascripts/app/views/telegram/bot_add.jst.eco @@ -0,0 +1,32 @@ + +

+ <%- @T('The tutorial on how to manage a %s is hosted on our [online documentation](https://zammad.org/documentation/channel/telegram).', 'Telegram Bot') %> +

+
+

<%- @T('Enter your %s App Keys', 'Telegram') %>

+
+
+ +
+
+ +
+
+

<%- @T('Settings') %>

+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+
diff --git a/app/assets/javascripts/app/views/telegram/bot_edit.jst.eco b/app/assets/javascripts/app/views/telegram/bot_edit.jst.eco new file mode 100644 index 000000000..07b6de1c2 --- /dev/null +++ b/app/assets/javascripts/app/views/telegram/bot_edit.jst.eco @@ -0,0 +1,29 @@ + +
+

<%- @T('Enter your %s App Keys', 'Telegram') %>

+
+
+ +
+
+ +
+
+

<%- @T('Settings') %>

+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+
diff --git a/app/assets/javascripts/app/views/telegram/index.jst.eco b/app/assets/javascripts/app/views/telegram/index.jst.eco new file mode 100644 index 000000000..b88d9506c --- /dev/null +++ b/app/assets/javascripts/app/views/telegram/index.jst.eco @@ -0,0 +1,48 @@ + + +
+ +<% if _.isEmpty(@channels): %> +
+

<%- @T('You have no configured %s right now.', 'Telegram Bot') %>

+
+<% else: %> + +<% for channel in @channels: %> +
+
+

<%- @Icon('status', 'supergood-color inline') %> <%= channel.options.bot.first_name %> @<%= channel.options.bot.username %>

+
+
+
+

<%- @T('Messages') %>

+ @<%= channel.options.bot.username %> +
+ <%- @Icon('arrow-right', 'action-flow-icon') %> +
+

<%- @T('Group') %>

+ <% if channel.options: %> + <%= channel.options.groupName %> + <% end %> +
+
+
+
<%- @T('Delete') %>
+ <% if channel.active is true: %> +
<%- @T('Disable') %>
+ <% else: %> +
<%- @T('Enable') %>
+ <% end %> +
<%- @T('Edit') %>
+
+
+<% end %> +
diff --git a/app/controllers/channels_controller.rb b/app/controllers/channels_email_controller.rb similarity index 74% rename from app/controllers/channels_controller.rb rename to app/controllers/channels_email_controller.rb index 818ac9525..8dbc68224 100644 --- a/app/controllers/channels_controller.rb +++ b/app/controllers/channels_email_controller.rb @@ -1,99 +1,9 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -class ChannelsController < ApplicationController +class ChannelsEmailController < ApplicationController before_action :authentication_check -=begin - -Resource: -POST /api/v1/channels/group/{id}.json - -Response: -{} - -Test: -curl http://localhost/api/v1/group/channels.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST '{group_id:123}' - -=end - - def group_update - permission_check('admin') - check_access - - channel = Channel.find(params[:id]) - channel.group_id = params[:group_id] - channel.save - render json: {} - end - -=begin - -Resource: -DELETE /api/v1/channels/{id}.json - -Response: -{} - -Test: -curl http://localhost/api/v1/channels.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X DELETE - -=end - - def destroy - permission_check('admin') - check_access - model_destroy_render(Channel, params) - end - - def twitter_index - permission_check('admin.channel_twitter') - assets = {} - ExternalCredential.where(name: 'twitter').each { |external_credential| - assets = external_credential.assets(assets) - } - channel_ids = [] - Channel.order(:id).each { |channel| - next if channel.area != 'Twitter::Account' - assets = channel.assets(assets) - channel_ids.push channel.id - } - render json: { - assets: assets, - channel_ids: channel_ids, - callback_url: ExternalCredential.callback_url('twitter'), - } - end - - def twitter_verify - permission_check('admin.channel_twitter') - model_update_render(Channel, params) - end - - def facebook_index - permission_check('admin.channel_facebook') - assets = {} - ExternalCredential.where(name: 'facebook').each { |external_credential| - assets = external_credential.assets(assets) - } - channel_ids = [] - Channel.order(:id).each { |channel| - next if channel.area != 'Facebook::Account' - assets = channel.assets(assets) - channel_ids.push channel.id - } - render json: { - assets: assets, - channel_ids: channel_ids, - callback_url: ExternalCredential.callback_url('facebook'), - } - end - - def facebook_verify - permission_check('admin.channel_facebook') - model_update_render(Channel, params) - end - - def email_index + def index permission_check('admin.channel_email') system_online_service = Setting.get('system_online_service') account_channel_ids = [] @@ -142,7 +52,7 @@ curl http://localhost/api/v1/channels.json -v -u #{login}:#{password} -H "Conten } end - def email_probe + def probe # check admin permissions permission_check('admin.channel_email') @@ -156,13 +66,13 @@ curl http://localhost/api/v1/channels.json -v -u #{login}:#{password} -H "Conten # verify if user+host already exists if result[:result] == 'ok' - return if email_account_duplicate?(result) + return if account_duplicate?(result) end render json: result end - def email_outbound + def outbound # check admin permissions permission_check('admin.channel_email') @@ -174,7 +84,7 @@ curl http://localhost/api/v1/channels.json -v -u #{login}:#{password} -H "Conten render json: EmailHelper::Probe.outbound(params, params[:email]) end - def email_inbound + def inbound # check admin permissions permission_check('admin.channel_email') @@ -186,12 +96,12 @@ curl http://localhost/api/v1/channels.json -v -u #{login}:#{password} -H "Conten result = EmailHelper::Probe.inbound(params) # check account duplicate - return if email_account_duplicate?({ setting: { inbound: params } }, params[:channel_id]) + return if account_duplicate?({ setting: { inbound: params } }, params[:channel_id]) render json: result end - def email_verify + def verify # check admin permissions permission_check('admin.channel_email') @@ -204,7 +114,7 @@ curl http://localhost/api/v1/channels.json -v -u #{login}:#{password} -H "Conten return if channel_id && !check_access(channel_id) # check account duplicate - return if email_account_duplicate?({ setting: { inbound: params[:inbound] } }, channel_id) + return if account_duplicate?({ setting: { inbound: params[:inbound] } }, channel_id) # check delivery for 30 sek. result = EmailHelper::Verify.email( @@ -284,7 +194,38 @@ curl http://localhost/api/v1/channels.json -v -u #{login}:#{password} -H "Conten render json: result end - def email_notification + def enable + permission_check('admin.channel_email') + channel = Channel.find_by(id: params[:id], area: 'Email::Account') + channel.active = true + channel.save! + render json: {} + end + + def disable + permission_check('admin.channel_email') + channel = Channel.find_by(id: params[:id], area: 'Email::Account') + channel.active = false + channel.save! + render json: {} + end + + def destroy + permission_check('admin.channel_email') + channel = Channel.find_by(id: params[:id], area: 'Email::Account') + channel.destroy + render json: {} + end + + def group + check_access + channel = Channel.find_by(id: params[:id], area: 'Email::Account') + channel.group_id = params[:group_id] + channel.save! + render json: {} + end + + def notification check_online_service @@ -323,7 +264,7 @@ curl http://localhost/api/v1/channels.json -v -u #{login}:#{password} -H "Conten private - def email_account_duplicate?(result, channel_id = nil) + def account_duplicate?(result, channel_id = nil) Channel.where(area: 'Email::Account').each { |channel| next if !channel.options next if !channel.options[:inbound] diff --git a/app/controllers/channels_facebook_controller.rb b/app/controllers/channels_facebook_controller.rb new file mode 100644 index 000000000..b4254a5a1 --- /dev/null +++ b/app/controllers/channels_facebook_controller.rb @@ -0,0 +1,47 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class ChannelsFacebookController < ApplicationController + before_action { authentication_check(permission: 'admin.channel_facebook') } + + def index + assets = {} + ExternalCredential.where(name: 'facebook').each { |external_credential| + assets = external_credential.assets(assets) + } + channel_ids = [] + Channel.where(area: 'Facebook::Account').order(:id).each { |channel| + assets = channel.assets(assets) + channel_ids.push channel.id + } + render json: { + assets: assets, + channel_ids: channel_ids, + callback_url: ExternalCredential.callback_url('facebook'), + } + end + + def update + model_update_render(Channel, params) + end + + def enable + channel = Channel.find_by(id: params[:id], area: 'Facebook::Account') + channel.active = true + channel.save! + render json: {} + end + + def disable + channel = Channel.find_by(id: params[:id], area: 'Facebook::Account') + channel.active = false + channel.save! + render json: {} + end + + def destroy + channel = Channel.find_by(id: params[:id], area: 'Facebook::Account') + channel.destroy + render json: {} + end + +end diff --git a/app/controllers/channels_telegram_controller.rb b/app/controllers/channels_telegram_controller.rb new file mode 100644 index 000000000..cd40fc68c --- /dev/null +++ b/app/controllers/channels_telegram_controller.rb @@ -0,0 +1,74 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class ChannelsTelegramController < ApplicationController + before_action -> { authentication_check(permission: 'admin.channel_telegram') }, except: [:webhook] + + def index + assets = {} + channel_ids = [] + Channel.where(area: 'Telegram::Bot').order(:id).each { |channel| + assets = channel.assets(assets) + channel_ids.push channel.id + } + render json: { + assets: assets, + channel_ids: channel_ids + } + end + + def add + begin + channel = Telegram.create_or_update_channel(params[:api_token], params) + rescue => e + raise Exceptions::UnprocessableEntity, e.message + end + render json: channel + end + + def update + channel = Channel.find_by(id: params[:id], area: 'Telegram::Bot') + begin + channel = Telegram.create_or_update_channel(params[:api_token], params, channel) + rescue => e + raise Exceptions::UnprocessableEntity, e.message + end + render json: channel + end + + def enable + channel = Channel.find_by(id: params[:id], area: 'Telegram::Bot') + channel.active = true + channel.save! + render json: {} + end + + def disable + channel = Channel.find_by(id: params[:id], area: 'Telegram::Bot') + channel.active = false + channel.save! + render json: {} + end + + def destroy + channel = Channel.find_by(id: params[:id], area: 'Telegram::Bot') + channel.destroy + render json: {} + end + + def webhook + raise Exceptions::UnprocessableEntity, 'bot param missing' if params['bid'].blank? + + channel = Telegram.bot_by_bot_id(params['bid']) + raise Exceptions::UnprocessableEntity, 'bot not found' if !channel + + if channel.options[:callback_token] != params['callback_token'] + raise Exceptions::UnprocessableEntity, 'invalid callback token' + end + + telegram = Telegram.new(channel.options[:api_token]) + telegram.to_group(params, channel.group_id, channel) + + render json: {}, status: :ok + end + +end diff --git a/app/controllers/channels_twitter_controller.rb b/app/controllers/channels_twitter_controller.rb new file mode 100644 index 000000000..b4bf0ec90 --- /dev/null +++ b/app/controllers/channels_twitter_controller.rb @@ -0,0 +1,47 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class ChannelsTwitterController < ApplicationController + before_action { authentication_check(permission: 'admin.channel_twitter') } + + def index + assets = {} + ExternalCredential.where(name: 'twitter').each { |external_credential| + assets = external_credential.assets(assets) + } + channel_ids = [] + Channel.where(area: 'Twitter::Account').order(:id).each { |channel| + assets = channel.assets(assets) + channel_ids.push channel.id + } + render json: { + assets: assets, + channel_ids: channel_ids, + callback_url: ExternalCredential.callback_url('twitter'), + } + end + + def update + model_update_render(Channel, params) + end + + def enable + channel = Channel.find_by(id: params[:id], area: 'Twitter::Account') + channel.active = true + channel.save! + render json: {} + end + + def disable + channel = Channel.find_by(id: params[:id], area: 'Twitter::Account') + channel.active = false + channel.save! + render json: {} + end + + def destroy + channel = Channel.find_by(id: params[:id], area: 'Twitter::Account') + channel.destroy + render json: {} + end + +end diff --git a/app/models/channel/driver/telegram.rb b/app/models/channel/driver/telegram.rb new file mode 100644 index 000000000..ffa24fc67 --- /dev/null +++ b/app/models/channel/driver/telegram.rb @@ -0,0 +1,44 @@ +# Copyright (C) 2012-2015 Zammad Foundation, http://zammad-foundation.org/ + +class Channel::Driver::Telegram + +=begin + + instance = Channel::Driver::Telegram.new + instance.send( + { + adapter: 'telegram', + auth: { + api_key: api_key + }, + }, + telegram_attributes, + notification + ) + +=end + + def send(options, article, _notification = false) + + # return if we run import mode + return if Setting.get('import_mode') + + options = check_external_credential(options) + + @client = Telegram.new(options[:auth][:api_key]) + message = @client.from_article(article) + message + end + + private + + def check_external_credential(options) + if options[:auth] && options[:auth][:external_credential_id] + external_credential = ExternalCredential.find_by(id: options[:auth][:external_credential_id]) + raise "No such ExternalCredential.find(#{options[:auth][:external_credential_id]})" if !external_credential + options[:auth][:api_key] = external_credential.credentials['api_key'] + end + options + end + +end diff --git a/app/models/observer/ticket/article/communicate_telegram.rb b/app/models/observer/ticket/article/communicate_telegram.rb new file mode 100644 index 000000000..9995763fb --- /dev/null +++ b/app/models/observer/ticket/article/communicate_telegram.rb @@ -0,0 +1,25 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class Observer::Ticket::Article::CommunicateTelegram < ActiveRecord::Observer + observe 'ticket::_article' + + def after_create(record) + + # return if we run import mode + return if Setting.get('import_mode') + + # if sender is customer, do not communicate + return if !record.sender_id + sender = Ticket::Article::Sender.lookup(id: record.sender_id) + return if sender.nil? + return if sender['name'] == 'Customer' + + # only apply on telegram messages + return if !record.type_id + type = Ticket::Article::Type.lookup(id: record.type_id) + return if type['name'] !~ /\Atelegram/i + + Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateTelegram::BackgroundJob.new(record.id)) + end + +end diff --git a/app/models/observer/ticket/article/communicate_telegram/background_job.rb b/app/models/observer/ticket/article/communicate_telegram/background_job.rb new file mode 100644 index 000000000..0922b639a --- /dev/null +++ b/app/models/observer/ticket/article/communicate_telegram/background_job.rb @@ -0,0 +1,108 @@ +class Observer::Ticket::Article::CommunicateTelegram::BackgroundJob + def initialize(id) + @article_id = id + end + + def perform + article = Ticket::Article.find(@article_id) + + # set retry count + if !article.preferences['delivery_retry'] + article.preferences['delivery_retry'] = 0 + end + article.preferences['delivery_retry'] += 1 + + ticket = Ticket.lookup(id: article.ticket_id) + log_error(article, "Can't find ticket.preferences for Ticket.find(#{article.ticket_id})") if !ticket.preferences + log_error(article, "Can't find ticket.preferences['telegram'] for Ticket.find(#{article.ticket_id})") if !ticket.preferences['telegram'] + log_error(article, "Can't find ticket.preferences['telegram']['chat_id'] for Ticket.find(#{article.ticket_id})") if !ticket.preferences['telegram']['chat_id'] + if ticket.preferences['telegram'] && ticket.preferences['telegram']['bid'] + channel = Telegram.bot_by_bot_id(ticket.preferences['telegram']['bid']) + end + if !channel + channel = Channel.lookup(id: ticket.preferences['channel_id']) + end + log_error(article, "No such channel for bot #{ticket.preferences['bid']} or channel id #{ticket.preferences['channel_id']}") if !channel + #log_error(article, "Channel.find(#{channel.id}) isn't a telegram channel!") if channel.options[:adapter] !~ /\Atelegram/i + log_error(article, "Channel.find(#{channel.id}) has not telegram api token!") if channel.options[:api_token].blank? + + begin + api = TelegramAPI.new(channel.options[:api_token]) + chat_id = ticket.preferences[:telegram][:chat_id] + result = api.sendMessage(chat_id, article.body) + article.attachments.each { |file| + parts = file.filename.split(/^(.*)(\..+?)$/) + t = Tempfile.new([parts[1], parts[2]]) + t.binmode + t.write(file.content) + t.rewind + api.sendDocument(chat_id, t.path.to_s) + } + rescue => e + log_error(article, e.message) + return + end + + # fill article with message info + article.from = "@#{result['from']['username']}" + article.to = "@#{result['chat']['username']}" + + article.preferences['telegram'] = { + date: result['date'], + from_id: result['from']['id'], + chat_id: result['chat']['id'], + message_id: result['message_id'] + } + + # set delivery status + article.preferences['delivery_status_message'] = nil + article.preferences['delivery_status'] = 'success' + article.preferences['delivery_status_date'] = Time.zone.now + + article.message_id = "telegram.#{result['message_id']}.#{result['chat']['id']}" + + article.save! + + Rails.logger.info "Send telegram message to: '#{article.to}' (from #{article.from})" + + article + end + + def log_error(local_record, message) + local_record.preferences['delivery_status'] = 'fail' + local_record.preferences['delivery_status_message'] = message + local_record.preferences['delivery_status_date'] = Time.zone.now + local_record.save + Rails.logger.error message + + if local_record.preferences['delivery_retry'] > 3 + Ticket::Article.create( + ticket_id: local_record.ticket_id, + content_type: 'text/plain', + body: "Unable to send telegram message: #{message}", + internal: true, + sender: Ticket::Article::Sender.find_by(name: 'System'), + type: Ticket::Article::Type.find_by(name: 'note'), + preferences: { + delivery_article_id_related: local_record.id, + delivery_message: true, + }, + updated_by_id: 1, + created_by_id: 1, + ) + end + + raise message + end + + def max_attempts + 4 + end + + def reschedule_at(current_time, attempts) + if Rails.env.production? + return current_time + attempts * 120.seconds + end + current_time + 5.seconds + end +end diff --git a/config/application.rb b/config/application.rb index 5b8a77bc5..32fed4b20 100644 --- a/config/application.rb +++ b/config/application.rb @@ -30,6 +30,7 @@ module Zammad 'observer::_ticket::_article::_communicate_email', 'observer::_ticket::_article::_communicate_facebook', 'observer::_ticket::_article::_communicate_twitter', + 'observer::_ticket::_article::_communicate_telegram', 'observer::_ticket::_reset_new_state', 'observer::_ticket::_ref_object_touch', 'observer::_ticket::_online_notification_seen', diff --git a/config/routes/channel.rb b/config/routes/channel.rb deleted file mode 100644 index 0bc9e246a..000000000 --- a/config/routes/channel.rb +++ /dev/null @@ -1,24 +0,0 @@ -Zammad::Application.routes.draw do - api_path = Rails.configuration.api_path - - # email helper - match api_path + '/channels/email_index', to: 'channels#email_index', via: :get - match api_path + '/channels/email_probe', to: 'channels#email_probe', via: :post - match api_path + '/channels/email_outbound', to: 'channels#email_outbound', via: :post - match api_path + '/channels/email_inbound', to: 'channels#email_inbound', via: :post - match api_path + '/channels/email_verify', to: 'channels#email_verify', via: :post - match api_path + '/channels/email_notification', to: 'channels#email_notification', via: :post - - # twitter helper - match api_path + '/channels/twitter_index', to: 'channels#twitter_index', via: :get - match api_path + '/channels/twitter_verify/:id', to: 'channels#twitter_verify', via: :post - - # facebook helper - match api_path + '/channels/facebook_index', to: 'channels#facebook_index', via: :get - match api_path + '/channels/facebook_verify/:id', to: 'channels#facebook_verify', via: :post - - # channels - match api_path + '/channels/group/:id', to: 'channels#group_update', via: :post - match api_path + '/channels/:id', to: 'channels#destroy', via: :delete - -end diff --git a/config/routes/channel_email.rb b/config/routes/channel_email.rb new file mode 100644 index 000000000..4226128cb --- /dev/null +++ b/config/routes/channel_email.rb @@ -0,0 +1,15 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + match api_path + '/channels_email', to: 'channels_email#index', via: :get + match api_path + '/channels_email_probe', to: 'channels_email#probe', via: :post + match api_path + '/channels_email_outbound', to: 'channels_email#outbound', via: :post + match api_path + '/channels_email_inbound', to: 'channels_email#inbound', via: :post + match api_path + '/channels_email_verify', to: 'channels_email#verify', via: :post + match api_path + '/channels_email_notification', to: 'channels_email#notification', via: :post + match api_path + '/channels_email_disable', to: 'channels_email#disable', via: :post + match api_path + '/channels_email_enable', to: 'channels_email#enable', via: :post + match api_path + '/channels_email', to: 'channels_email#destroy', via: :delete + match api_path + '/channels_email_group/:id', to: 'channels_email#group', via: :post + +end diff --git a/config/routes/channel_facebook.rb b/config/routes/channel_facebook.rb new file mode 100644 index 000000000..32098a861 --- /dev/null +++ b/config/routes/channel_facebook.rb @@ -0,0 +1,9 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + match api_path + '/channels_facebook', to: 'channels_facebook#index', via: :get + match api_path + '/channels_facebook/:id', to: 'channels_facebook#update', via: :post + match api_path + '/channels_facebook_disable', to: 'channels_facebook#disable', via: :post + match api_path + '/channels_facebook_enable', to: 'channels_facebook#enable', via: :post + match api_path + '/channels_facebook', to: 'channels_facebook#destroy', via: :delete +end diff --git a/config/routes/channel_telegram.rb b/config/routes/channel_telegram.rb new file mode 100644 index 000000000..bf0845167 --- /dev/null +++ b/config/routes/channel_telegram.rb @@ -0,0 +1,12 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + match api_path + '/channels_telegram', to: 'channels_telegram#index', via: :get + match api_path + '/channels_telegram', to: 'channels_telegram#add', via: :post + match api_path + '/channels_telegram/:id', to: 'channels_telegram#update', via: :put + match api_path + '/channels_telegram_webhook/:callback_token', to: 'channels_telegram#webhook', via: :post + match api_path + '/channels_telegram_disable', to: 'channels_telegram#disable', via: :post + match api_path + '/channels_telegram_enable', to: 'channels_telegram#enable', via: :post + match api_path + '/channels_telegram', to: 'channels_telegram#destroy', via: :delete + +end diff --git a/config/routes/channel_twitter.rb b/config/routes/channel_twitter.rb new file mode 100644 index 000000000..202ac157a --- /dev/null +++ b/config/routes/channel_twitter.rb @@ -0,0 +1,9 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + match api_path + '/channels_twitter', to: 'channels_twitter#index', via: :get + match api_path + '/channels_twitter/:id', to: 'channels_twitter#update', via: :post + match api_path + '/channels_twitter_disable', to: 'channels_twitter#disable', via: :post + match api_path + '/channels_twitter_enable', to: 'channels_twitter#enable', via: :post + match api_path + '/channels_twitter', to: 'channels_twitter#destroy', via: :delete +end diff --git a/db/migrate/20170215000001_telegram_support.rb b/db/migrate/20170215000001_telegram_support.rb new file mode 100644 index 000000000..8bd689ca5 --- /dev/null +++ b/db/migrate/20170215000001_telegram_support.rb @@ -0,0 +1,22 @@ +class TelegramSupport < ActiveRecord::Migration + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + Permission.create_if_not_exists( + name: 'admin.channel_telegram', + note: 'Manage %s', + preferences: { + translations: ['Channel - Telegram'] + }, + ) + + Ticket::Article::Type.create_if_not_exists( + id: 12, + name: 'telegram personal-message', + communication: true, + ) + + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 435f7d8ea..ddceeecd3 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -2894,6 +2894,13 @@ Permission.create_if_not_exists( translations: ['Channel - Facebook'] }, ) +Permission.create_if_not_exists( + name: 'admin.channel_telegram', + note: 'Manage %s', + preferences: { + translations: ['Channel - Telegram'] + }, +) Permission.create_if_not_exists( name: 'admin.channel_chat', note: 'Manage %s', @@ -3241,6 +3248,7 @@ Ticket::Article::Type.create_if_not_exists(id: 8, name: 'facebook feed post', co Ticket::Article::Type.create_if_not_exists(id: 9, name: 'facebook feed comment', communication: true) 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::Sender.create_if_not_exists(id: 1, name: 'Agent') Ticket::Article::Sender.create_if_not_exists(id: 2, name: 'Customer') diff --git a/lib/telegram.rb b/lib/telegram.rb new file mode 100644 index 000000000..f16da7d64 --- /dev/null +++ b/lib/telegram.rb @@ -0,0 +1,573 @@ +# Copyright (C) 2012-2015 Zammad Foundation, http://zammad-foundation.org/ + +class Telegram + + attr_accessor :client + +=begin + +check token and return bot attributes of token + + bot = Telegram.check_token('token') + +=end + + def self.check_token(token) + api = TelegramAPI.new(token) + begin + bot = api.getMe() + rescue + raise 'invalid api token' + end + bot + end + +=begin + +set webhool for bot + + success = Telegram.set_webhook('token', callback_url) + +returns + + true|false + +=end + + def self.set_webhook(token, callback_url) + if callback_url =~ /^http:\/\//i + raise 'webhook url need to start with https://, you use http://' + end + api = TelegramAPI.new(token) + begin + api.setWebhook(callback_url) + rescue + raise 'Unable to set webhook at Telegram, seems to be a invalid url.' + end + true + end + +=begin + +create or update channel, store bot attributes and verify token + + channel = Telegram.create_or_update_channel('token', params) + +returns + + channel # instance of Channel + +=end + + def self.create_or_update_channel(token, params, channel = nil) + + # verify token + bot = Telegram.check_token(token) + + if !channel + if Telegram.bot_duplicate?(bot['id']) + raise 'Bot already exists!' + end + end + + if params[:group_id].blank? + raise 'Group needed!' + else + group = Group.find_by(id: params[:group_id]) + end + if !group + raise 'Group invalid!' + end + + # generate randam callback token + callback_token = SecureRandom.urlsafe_base64(10) + + # set webhook / callback url for this bot @ telegram + callback_url = "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/api/v1/channels_telegram_webhook/#{callback_token}?bid=#{bot['id']}" + Telegram.set_webhook(token, callback_url) + + if !channel + channel = Telegram.bot_by_bot_id(bot['id']) + if !channel + channel = Channel.new + end + end + channel.area = 'Telegram::Bot' + channel.options = { + bot: { + id: bot['id'], + username: bot['username'], + first_name: bot['first_name'], + last_name: bot['last_name'], + }, + callback_token: callback_token, + callback_url: callback_url, + api_token: token, + welcome: params[:welcome], + } + channel.group_id = group.id + channel.active = true + channel.save! + channel + end + +=begin + +check if bot already exists as channel + + success = Telegram.bot_duplicate?(bot_id) + +returns + + channel # instance of Channel + +=end + + def self.bot_duplicate?(bot_id, channel_id = nil) + Channel.where(area: 'Telegram::Bot').each { |channel| + next if !channel.options + next if !channel.options[:bot] + next if !channel.options[:bot][:id] + next if channel.options[:bot][:id] != bot_id + next if channel.id.to_s == channel_id.to_s + return true + } + false + end + +=begin + +get channel by bot_id + + channel = Telegram.bot_by_bot_id(bot_id) + +returns + + true|false + +=end + + def self.bot_by_bot_id(bot_id) + Channel.where(area: 'Telegram::Bot').each { |channel| + next if !channel.options + next if !channel.options[:bot] + next if !channel.options[:bot][:id] + return channel if channel.options[:bot][:id].to_s == bot_id.to_s + } + nil + end + +=begin + +generate message_id for message + + message_id = Telegram.message_id(message) + +returns + + message_id # 123456@telegram + +=end + + def self.message_id(params) + message_id = nil + [:message, :edited_message].each { |key| + next if !params[key] + next if !params[key][:message_id] + message_id = params[key][:message_id] + break + } + if !message_id + message_id = params[:update_id] + end + "#{message_id}@telegram" + end + +=begin + + client = Telegram.new('token') + +=end + + def initialize(token) + @token = token + @api = TelegramAPI.new(token) + end + +=begin + + client.message(chat_id, 'some message') + +=end + + def message(chat_id, message) + return if Rails.env.test? + @api.sendMessage(chat_id, message) + end + + def user(params) + { + id: params[:message][:from][:id], + username: params[:message][:from][:username], + first_name: params[:message][:from][:first_name], + last_name: params[:message][:from][:last_name] + } + end + + def to_user(params) + Rails.logger.debug 'Create user from message...' + Rails.logger.debug params.inspect + + # do message_user lookup + message_user = user(params) + + auth = Authorization.find_by(uid: message_user[:id], provider: 'telegram') + + # create or update user + user_data = { + login: message_user[:username], + firstname: message_user[:first_name], + lastname: message_user[:last_name], + } + if auth + user = User.find(auth.user_id) + user.update_attributes(user_data) + else + user_data[:note] = "Telegram @#{message_user[:username]}" + user_data[:active] = true + user_data[:role_ids] = Role.signup_role_ids + user = User.create(user_data) + end + + # create or update authorization + auth_data = { + uid: message_user[:id], + username: message_user[:username], + user_id: user.id, + provider: 'telegram' + } + if auth + auth.update_attributes(auth_data) + else + Authorization.create(auth_data) + end + + user + end + + def to_ticket(params, user, group_id, channel) + UserInfo.current_user_id = user.id + + Rails.logger.debug 'Create ticket from message...' + Rails.logger.debug params.inspect + Rails.logger.debug user.inspect + Rails.logger.debug group_id.inspect + + # find ticket or create one + state_ids = Ticket::State.where(name: %w(closed merged removed)).pluck(:id) + ticket = Ticket.where(customer_id: user.id).where.not(state_id: state_ids).order(:updated_at).first + if ticket + new_state = Ticket::State.find_by(name: 'new') + if ticket.state_id != new_state.id + ticket.state = Ticket::State.find_by(name: 'open') + end + ticket.save! + return ticket + end + + # prepare title + title = params[:message][:text] + if title.length > 60 + title = "#{title[0, 60]}..." + end + + ticket = Ticket.new( + group_id: group_id, + title: title, + state_id: Ticket::State.find_by(name: 'new').id, + priority_id: Ticket::Priority.find_by(name: '2 normal').id, + customer_id: user.id, + preferences: { + channel_id: channel.id, + telegram: { + bid: params['bid'], + chat_id: params[:message][:chat][:id] + } + }, + ) + ticket.save! + ticket + end + + def to_article(params, user, ticket, channel, article = nil) + + if article + Rails.logger.debug 'Update article from message...' + else + Rails.logger.debug 'Create article from message...' + end + Rails.logger.debug params.inspect + Rails.logger.debug user.inspect + Rails.logger.debug ticket.inspect + + UserInfo.current_user_id = user.id + + if article + article.preferences[:edited_message] = { + message: { + created_at: params[:message][:date], + message_id: params[:message][:message_id], + from: params[:message][:from], + }, + update_id: params[:update_id], + } + else + article = Ticket::Article.new( + ticket_id: ticket.id, + type_id: Ticket::Article::Type.find_by(name: 'telegram personal-message').id, + sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id, + from: user(params)[:username], + to: "@#{channel[:options][:bot][:username]}", + message_id: Telegram.message_id(params), + internal: false, + preferences: { + message: { + created_at: params[:message][:date], + message_id: params[:message][:message_id], + from: params[:message][:from], + }, + update_id: params[:update_id], + } + ) + end + + # add article + if params[:message][:photo] + + # find photo with best resolution for us + photo = nil + max_width = 650 * 2 + last_width = 0 + last_height = 0 + params[:message][:photo].each { |file| + if !photo + photo = file + last_width = file['width'].to_i + last_height = file['height'].to_i + end + if file['width'].to_i < max_width && last_width < file['width'].to_i + photo = file + last_width = file['width'].to_i + last_height = file['height'].to_i + end + } + if last_width > 650 + last_width = (last_width / 2).to_i + last_height = (last_height / 2).to_i + end + + # download image + result = download_file(photo['file_id']) + if !result.success? || !result.body + raise "Unable for download image from telegram: #{result.code}" + end + body = "" + if params[:message][:caption] + body += "
#{params[:message][:caption].text2html}" + end + article.content_type = 'text/html' + article.body = body + article.save! + return article + end + + # add document + if params[:message][:document] + thump = params[:message][:document][:thumb] + body = ' ' + if thump + width = thump[:width] + height = thump[:height] + result = download_file(thump['file_id']) + if !result.success? || !result.body + raise "Unable for download image from telegram: #{result.code}" + end + body = "" + end + document_result = download_file(params[:message][:document][:file_id]) + article.content_type = 'text/html' + article.body = body + article.save! + Store.remove( + object: 'Ticket::Article', + o_id: article.id, + ) + Store.add( + object: 'Ticket::Article', + o_id: article.id, + data: document_result.body, + filename: params[:message][:document][:file_name], + preferences: { + 'Mime-Type' => params[:message][:document][:mime_type], + }, + ) + return article + end + + # voice + if params[:message][:voice] + body = ' ' + if params[:message][:caption] + body = "
#{params[:message][:caption].text2html}" + end + document_result = download_file(params[:message][:voice][:file_id]) + article.content_type = 'text/html' + article.body = body + article.save! + Store.remove( + object: 'Ticket::Article', + o_id: article.id, + ) + Store.add( + object: 'Ticket::Article', + o_id: article.id, + data: document_result.body, + filename: params[:message][:voice][:file_path] || 'audio', + preferences: { + 'Mime-Type' => params[:message][:voice][:mime_type], + }, + ) + return article + end + + # text + if params[:message][:text] + article.content_type = 'text/plain' + article.body = params[:message][:text] + article.save! + return article + end + raise 'invalid action' + end + + def to_group(params, group_id, channel) + Rails.logger.debug 'import message' + + # prevent multible update + if !params[:edited_message] + return if Ticket::Article.find_by(message_id: Telegram.message_id(params)) + end + + # update article + if params[:edited_message] + article = Ticket::Article.find_by(message_id: Telegram.message_id(params)) + return if !article + params[:message] = params[:edited_message] + user = to_user(params) + to_article(params, user, article.ticket, channel, article) + return article + end + + # send welcome message and don't create ticket + text = params[:message][:text] + if text.present? && text =~ /^\/start/ + message(params[:message][:chat][:id], channel.options[:welcome] || 'You are welcome! Just ask me something!') + return + + # find ticket and close it + elsif text.present? && text =~ /^\/end/ + user = to_user(params) + ticket = Ticket.where(customer_id: user.id).order(:updated_at).first + ticket.state = Ticket::State.find_by(name: 'closed') + ticket.save! + return + end + + ticket = nil + + # use transaction + Transaction.execute(reset_user_id: true) do + user = to_user(params) + ticket = to_ticket(params, user, group_id, channel) + to_article(params, user, ticket, channel) + end + + ticket + end + + def from_article(article) + + message = nil + Rails.logger.debug "Create telegram personal message from article to '#{article[:to]}'..." + + message = {} + # TODO: create telegram message here + + Rails.logger.debug message.inspect + message + end + + def get_state(channel, telegram_update, ticket = nil) + message = telegram_update['message'] + message_user = user(message) + + # no changes in post is from page user it self + if channel.options[:bot][:id].to_s == message_user[:id].to_s + if !ticket + return Ticket::State.find_by(name: 'closed') if !ticket + end + return ticket.state + end + + state = Ticket::State.find_by(name: 'new') + return state if !ticket + return ticket.state if ticket.state.name == 'new' + Ticket::State.find_by(name: 'open') + end + + def download_file(file_id) + if Rails.env.test? + result = Result.new( + success: true, + body: 'ok', + data: 'ok', + code: 200, + content_type: 'application/stream', + ) + return result + end + document = @api.getFile(file_id) + url = "https://api.telegram.org/file/bot#{@token}/#{document['file_path']}" + UserAgent.get( + url, + {}, + { + open_timeout: 20, + read_timeout: 40, + }, + ) + end + + class Result + + attr_reader :error + attr_reader :body + attr_reader :data + attr_reader :code + attr_reader :content_type + + def initialize(options) + @success = options[:success] + @body = options[:body] + @data = options[:data] + @code = options[:code] + @content_type = options[:content_type] + @error = options[:error] + end + + def success? + return true if @success + false + end + end +end diff --git a/test/controllers/channels_controller_test.rb b/test/controllers/channels_controller_test.rb new file mode 100644 index 000000000..841ba56d1 --- /dev/null +++ b/test/controllers/channels_controller_test.rb @@ -0,0 +1,68 @@ +# encoding: utf-8 +require 'test_helper' + +class ChannelsControllerTest < ActionDispatch::IntegrationTest + setup do + + # set accept header + @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } + + # create agent + roles = Role.where(name: %w(Admin Agent)) + groups = Group.all + + UserInfo.current_user_id = 1 + @admin = User.create_or_update( + login: 'packages-admin', + firstname: 'Packages', + lastname: 'Admin', + email: 'packages-admin@example.com', + password: 'adminpw', + active: true, + roles: roles, + groups: groups, + ) + + # create agent + roles = Role.where(name: 'Agent') + @agent = User.create_or_update( + login: 'packages-agent@example.com', + firstname: 'Rest', + lastname: 'Agent', + email: 'packages-agent@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: groups, + ) + + # create customer without org + roles = Role.where(name: 'Customer') + @customer_without_org = User.create_or_update( + login: 'packages-customer1@example.com', + firstname: 'Packages', + lastname: 'Customer1', + email: 'packages-customer1@example.com', + password: 'customer1pw', + active: true, + roles: roles, + ) + + end + + test '01 telegram_webhook creates ticket' do + json = File.read('test/fixtures/telegram/personal_message_content.json') + post '/api/v1/channels/telegram_webhook', json, @headers + puts JSON.parse(@response.body).inspect + + assert_response(200) + + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal({ 'ok' => 'ok' }, result) + end + + test '0x telegram_webhook with existing ticket adds ticket_article' + test '0x telegram_webhook sends welcome message on /start' + test '0x telegram_webhook closes the ticket on /stop' +end diff --git a/test/fixtures/telegram/personal1_message_content1.json b/test/fixtures/telegram/personal1_message_content1.json new file mode 100644 index 000000000..8c722e2c8 --- /dev/null +++ b/test/fixtures/telegram/personal1_message_content1.json @@ -0,0 +1,21 @@ +{ + "update_id":10001, + "message":{ + "date":1441645532, + "chat":{ + "last_name":"Test Lastname", + "id":1111111, + "type": "private", + "first_name":"Test Firstname", + "username":"Testusername" + }, + "message_id":1365, + "from":{ + "last_name":"Test Lastname", + "id":1111111, + "first_name":"Test Firstname", + "username":"Testusername" + }, + "text":"Hello, I need your Help" + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal1_message_content2.json b/test/fixtures/telegram/personal1_message_content2.json new file mode 100644 index 000000000..3a930b7e1 --- /dev/null +++ b/test/fixtures/telegram/personal1_message_content2.json @@ -0,0 +1,21 @@ +{ + "update_id":10002, + "message":{ + "date":1441645535, + "chat":{ + "last_name":"Test Lastname", + "id":1111111, + "type": "private", + "first_name":"Test Firstname", + "username":"Testusername" + }, + "message_id":1366, + "from":{ + "last_name":"Test Lastname", + "id":1111111, + "first_name":"Test Firstname", + "username":"Testusername" + }, + "text":"Hello, I need your Help 2" + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal1_message_end.json b/test/fixtures/telegram/personal1_message_end.json new file mode 100644 index 000000000..2a459cf03 --- /dev/null +++ b/test/fixtures/telegram/personal1_message_end.json @@ -0,0 +1,21 @@ +{ + "update_id":10003, + "message":{ + "date":1441645532, + "chat":{ + "last_name":"Test Lastname", + "id":1111111, + "type": "private", + "first_name":"Test Firstname", + "username":"Testusername" + }, + "message_id":1367, + "from":{ + "last_name":"Test Lastname", + "id":1111111, + "first_name":"Test Firstname", + "username":"Testusername" + }, + "text":"/end" + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal1_message_start.json b/test/fixtures/telegram/personal1_message_start.json new file mode 100644 index 000000000..8c06a2387 --- /dev/null +++ b/test/fixtures/telegram/personal1_message_start.json @@ -0,0 +1,21 @@ +{ + "update_id":10000, + "message":{ + "date":1441645532, + "chat":{ + "last_name":"Test Lastname", + "id":1111111, + "type": "private", + "first_name":"Test Firstname", + "username":"Testusername" + }, + "message_id":1365, + "from":{ + "last_name":"Test Lastname", + "id":1111111, + "first_name":"Test Firstname", + "username":"Testusername" + }, + "text":"/start" + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal2_message_content1.json b/test/fixtures/telegram/personal2_message_content1.json new file mode 100644 index 000000000..f5d249f21 --- /dev/null +++ b/test/fixtures/telegram/personal2_message_content1.json @@ -0,0 +1,21 @@ +{ + "update_id":20001, + "message":{ + "date":1441645532, + "chat":{ + "last_name":"Test Lastname", + "id":1111111, + "type": "private", + "first_name":"Test Firstname", + "username":"Testusername" + }, + "message_id":2365, + "from":{ + "last_name":"Test Lastname", + "id":1111111, + "first_name":"Test Firstname", + "username":"Testusername" + }, + "text":"Can you help me with my feature?" + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal2_message_content2.json b/test/fixtures/telegram/personal2_message_content2.json new file mode 100644 index 000000000..92f09956a --- /dev/null +++ b/test/fixtures/telegram/personal2_message_content2.json @@ -0,0 +1,21 @@ +{ + "update_id":20002, + "message":{ + "date":1441645536, + "chat":{ + "last_name":"Test Lastname", + "id":1111111, + "type": "private", + "first_name":"Test Firstname", + "username":"Testusername" + }, + "message_id":2366, + "from":{ + "last_name":"Test Lastname", + "id":1111111, + "first_name":"Test Firstname", + "username":"Testusername" + }, + "text":"Yes of course! lalal" + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal2_message_start.json b/test/fixtures/telegram/personal2_message_start.json new file mode 100644 index 000000000..9851f1eee --- /dev/null +++ b/test/fixtures/telegram/personal2_message_start.json @@ -0,0 +1,21 @@ +{ + "update_id":20000, + "message":{ + "date":1441645532, + "chat":{ + "last_name":"Test Lastname", + "id":1111111, + "type": "private", + "first_name":"Test Firstname", + "username":"Testusername" + }, + "message_id":2364, + "from":{ + "last_name":"Test Lastname", + "id":1111111, + "first_name":"Test Firstname", + "username":"Testusername" + }, + "text":"/start" + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal3_message_content1.json b/test/fixtures/telegram/personal3_message_content1.json new file mode 100644 index 000000000..63f2b4090 --- /dev/null +++ b/test/fixtures/telegram/personal3_message_content1.json @@ -0,0 +1,21 @@ +{ + "update_id":30001, + "message":{ + "date":1441645532, + "chat":{ + "last_name":"Test Lastname2", + "id":1111112, + "type": "private", + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "message_id":3365, + "from":{ + "last_name":"Test Lastname2", + "id":1111112, + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "text":"Can you help me with my feature?" + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal3_message_content2.json b/test/fixtures/telegram/personal3_message_content2.json new file mode 100644 index 000000000..ba6f9feec --- /dev/null +++ b/test/fixtures/telegram/personal3_message_content2.json @@ -0,0 +1,42 @@ +{ + "update_id": 30002, + "message": { + "message_id": 3366, + "from": { + "last_name":"Test Lastname2", + "id":1111112, + "type": "private", + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "chat": { + "last_name":"Test Lastname2", + "id":1111112, + "first_name":"Test Firstname2", + "username":"Testusername2", + "type": "private" + }, + "date": 1486036832, + "photo": [ + { + "file_id": "ABC-123VabcOcv123w0ABBL_aoY-F849YYABC", + "file_size": 1016, + "width": 90, + "height": 82 + }, + { + "file_id": "ABC-123VabcOcv123w0ABPlhIiVSfO9TYoABC", + "file_size": 7378, + "width": 320, + "height": 291 + }, + { + "file_id": "ABC-123VabcOcv123w0ABHywrcPqfrbAYIABC", + "file_size": 16433, + "width": 720, + "height": 654 + } + ], + "caption": "caption 123abc" + } +} diff --git a/test/fixtures/telegram/personal3_message_content3.json b/test/fixtures/telegram/personal3_message_content3.json new file mode 100644 index 000000000..6aafb33ec --- /dev/null +++ b/test/fixtures/telegram/personal3_message_content3.json @@ -0,0 +1,33 @@ +{ + "update_id": 30003, + "message": { + "message_id": 3367, + "from": { + "last_name":"Test Lastname2", + "id":1111112, + "type": "private", + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "chat": { + "last_name":"Test Lastname2", + "id":1111112, + "first_name":"Test Firstname2", + "username":"Testusername2", + "type": "private" + }, + "date": 1486036832, + "document": { + "file_name": "blockposter-162412.pdf", + "mime_type": "application/pdf", + "thumb": { + "file_id": "AAQCABO0I4INAATATQAB5HWPq4XgxQACAg", + "file_size": 8752, + "width": 200, + "height": 200 + }, + "file_id": "BQADAgADDgAD7x6ZSC_-1LMkOEmoAg", + "file_size": 3622849 + } + } +} diff --git a/test/fixtures/telegram/personal3_message_content4.json b/test/fixtures/telegram/personal3_message_content4.json new file mode 100644 index 000000000..fc0606e16 --- /dev/null +++ b/test/fixtures/telegram/personal3_message_content4.json @@ -0,0 +1,22 @@ +{ + "update_id":30004, + "edited_message": { + "message_id":3365, + "from": { + "last_name":"Test Lastname2", + "id":1111112, + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "chat": { + "last_name":"Test Lastname2", + "id":1111112, + "type": "private", + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "date": 1487116688, + "edit_date": 1487116889, + "text": "UPDATE: 1231444" + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal3_message_content5.json b/test/fixtures/telegram/personal3_message_content5.json new file mode 100644 index 000000000..e1816ce3e --- /dev/null +++ b/test/fixtures/telegram/personal3_message_content5.json @@ -0,0 +1,26 @@ +{ + "update_id":30005, + "message":{ + "message_id":3368, + "from":{ + "last_name":"Test Lastname2", + "id":1111112, + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "chat":{ + "last_name":"Test Lastname2", + "id":1111112, + "type": "private", + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "date":1487119496, + "voice":{ + "duration":1, + "mime_type":"audio/ogg", + "file_id":"AwADAgADVQADCEIYSZwyOmSZK9iZAg", + "file_size":6030 + } + } +} \ No newline at end of file diff --git a/test/fixtures/telegram/personal3_message_start.json b/test/fixtures/telegram/personal3_message_start.json new file mode 100644 index 000000000..889635e1b --- /dev/null +++ b/test/fixtures/telegram/personal3_message_start.json @@ -0,0 +1,21 @@ +{ + "update_id":30000, + "message":{ + "date":1441645532, + "chat":{ + "last_name":"Test Lastname2", + "id":1111112, + "type": "private", + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "message_id":3364, + "from":{ + "last_name":"Test Lastname2", + "id":1111112, + "first_name":"Test Firstname2", + "username":"Testusername2" + }, + "text":"/start start" + } +} \ No newline at end of file diff --git a/test/integration/telegram_controller_test.rb b/test/integration/telegram_controller_test.rb new file mode 100644 index 000000000..45128bfcb --- /dev/null +++ b/test/integration/telegram_controller_test.rb @@ -0,0 +1,196 @@ +# encoding: utf-8 +require 'test_helper' +require 'rexml/document' + +class TelegramControllerTest < ActionDispatch::IntegrationTest + setup do + @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } + + # configure telegram channel + token = ENV['TELEGRAM_TOKEN'] + group_id = Group.find_by(name: 'Users').id + #bot = Telegram.check_token(token) + #Setting.set('http_type', 'http') + Setting.set('http_type', 'https') + Setting.set('fqdn', 'me.zammad.com') + Channel.where(area: 'Telegram::Bot').destroy_all + @channel = Telegram.create_or_update_channel(token, { group_id: group_id, welcome: 'hi!' }) + + groups = Group.where(name: 'Users') + roles = Role.where(name: %w(Agent)) + agent = User.create_or_update( + login: 'telegram-agent@example.com', + firstname: 'E', + lastname: 'S', + email: 'telegram-agent@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: groups, + updated_by_id: 1, + created_by_id: 1, + ) + + end + + test 'basic call' do + Ticket.destroy_all + + # start communication #1 + post '/api/v1/channels/telegram_webhook', read_messaage('personal1_message_start'), @headers + assert_response(404) + result = JSON.parse(@response.body) + + post '/api/v1/channels_telegram_webhook/not_existing', read_messaage('personal1_message_start'), @headers + assert_response(422) + result = JSON.parse(@response.body) + assert_equal('bot param missing', result['error']) + + callback_url = "/api/v1/channels_telegram_webhook/not_existing?bid=#{@channel.options[:bot][:id]}" + post callback_url, read_messaage('personal1_message_start'), @headers + assert_response(422) + result = JSON.parse(@response.body) + assert_equal('invalid callback token', result['error']) + + callback_url = "/api/v1/channels_telegram_webhook/#{@channel.options[:callback_token]}?bid=#{@channel.options[:bot][:id]}" + post callback_url, read_messaage('personal1_message_start'), @headers + assert_response(200) + + # send message1 + post callback_url, read_messaage('personal1_message_content1'), @headers + assert_response(200) + assert_equal(1, Ticket.count) + ticket = Ticket.last + assert_equal('Hello, I need your Help', ticket.title) + assert_equal('new', ticket.state.name) + assert_equal(1, ticket.articles.count) + assert_equal('Hello, I need your Help', ticket.articles.first.body) + assert_equal('text/plain', ticket.articles.first.content_type) + + # send same message again, ignore it + post callback_url, read_messaage('personal1_message_content1'), @headers + assert_response(200) + ticket = Ticket.last + assert_equal('Hello, I need your Help', ticket.title) + assert_equal('new', ticket.state.name) + assert_equal(1, ticket.articles.count) + assert_equal('Hello, I need your Help', ticket.articles.first.body) + assert_equal('text/plain', ticket.articles.first.content_type) + + # send message2 + post callback_url, read_messaage('personal1_message_content2'), @headers + assert_response(200) + ticket = Ticket.last + assert_equal('Hello, I need your Help', ticket.title) + assert_equal('new', ticket.state.name) + assert_equal(2, ticket.articles.count) + assert_equal('Hello, I need your Help 2', ticket.articles.last.body) + assert_equal('text/plain', ticket.articles.last.content_type) + + # send end message + post callback_url, read_messaage('personal1_message_end'), @headers + assert_response(200) + ticket = Ticket.last + assert_equal('Hello, I need your Help', ticket.title) + assert_equal('closed', ticket.state.name) + assert_equal(2, ticket.articles.count) + assert_equal('Hello, I need your Help 2', ticket.articles.last.body) + assert_equal('text/plain', ticket.articles.last.content_type) + + # start communication #2 + post callback_url, read_messaage('personal2_message_start'), @headers + assert_response(200) + + # send message1 + post callback_url, read_messaage('personal2_message_content1'), @headers + assert_response(200) + assert_equal(2, Ticket.count) + ticket = Ticket.last + assert_equal('Can you help me with my feature?', ticket.title) + assert_equal('new', ticket.state.name) + assert_equal(1, ticket.articles.count) + assert_equal('Can you help me with my feature?', ticket.articles.first.body) + assert_equal('text/plain', ticket.articles.first.content_type) + + # send message2 + post callback_url, read_messaage('personal2_message_content2'), @headers + assert_response(200) + assert_equal(2, Ticket.count) + ticket = Ticket.last + assert_equal('Can you help me with my feature?', ticket.title) + assert_equal('new', ticket.state.name) + assert_equal(2, ticket.articles.count) + assert_equal('Yes of course! lalal', ticket.articles.last.body) + assert_equal('text/plain', ticket.articles.last.content_type) + + # start communication #3 + post callback_url, read_messaage('personal3_message_start'), @headers + assert_response(200) + + # send message1 + post callback_url, read_messaage('personal3_message_content1'), @headers + assert_response(200) + assert_equal(3, Ticket.count) + ticket = Ticket.last + assert_equal('Can you help me with my feature?', ticket.title) + assert_equal('new', ticket.state.name) + assert_equal(1, ticket.articles.count) + assert_equal('Can you help me with my feature?', ticket.articles.last.body) + assert_equal('text/plain', ticket.articles.last.content_type) + + # send message2 + post callback_url, read_messaage('personal3_message_content2'), @headers + assert_response(200) + assert_equal(3, Ticket.count) + ticket = Ticket.last + assert_equal('Can you help me with my feature?', ticket.title) + assert_equal('new', ticket.state.name) + assert_equal(2, ticket.articles.count) + assert_match(/