twitter_sync.rb 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'http/uri'
  3. class TwitterSync
  4. STATUS_URL_TEMPLATE = 'https://twitter.com/_/status/%s'.freeze
  5. DM_URL_TEMPLATE = 'https://twitter.com/messages/%s'.freeze
  6. MAX_TWEETS_PER_IMPORT = 120
  7. attr_accessor :client
  8. def initialize(auth, payload = nil)
  9. @client = Twitter::REST::Client.new(
  10. consumer_key: auth[:consumer_key],
  11. consumer_secret: auth[:consumer_secret],
  12. access_token: auth[:oauth_token] || auth[:access_token],
  13. access_token_secret: auth[:oauth_token_secret] || auth[:access_token_secret],
  14. )
  15. @payload = payload
  16. end
  17. def disconnect
  18. return if !@client
  19. @client = nil
  20. end
  21. def user(tweet)
  22. raise "Unknown tweet type '#{tweet.class}'" if tweet.class != Twitter::Tweet
  23. Rails.logger.debug { "Twitter sender for tweet (#{tweet.id}): found" }
  24. Rails.logger.debug { tweet.user.inspect }
  25. tweet.user
  26. end
  27. def to_user(tweet)
  28. Rails.logger.debug { 'Create user from tweet...' }
  29. Rails.logger.debug { tweet.inspect }
  30. # do tweet_user lookup
  31. tweet_user = user(tweet)
  32. auth = Authorization.find_by(uid: tweet_user.id, provider: 'twitter')
  33. # create or update user
  34. user_data = {
  35. image_source: tweet_user.profile_image_url.to_s,
  36. }
  37. if auth
  38. user = User.find(auth.user_id)
  39. map = {
  40. note: 'description',
  41. web: 'website',
  42. address: 'location',
  43. }
  44. # ignore if value is already set
  45. map.each do |target, source|
  46. next if user[target].present?
  47. new_value = tweet_user.send(source).to_s
  48. next if new_value.blank?
  49. user_data[target] = new_value
  50. end
  51. user.update!(user_data)
  52. else
  53. user_data[:login] = tweet_user.screen_name
  54. user_data[:firstname] = tweet_user.name
  55. user_data[:web] = tweet_user.website.to_s
  56. user_data[:note] = tweet_user.description
  57. user_data[:address] = tweet_user.location
  58. user_data[:active] = true
  59. user_data[:role_ids] = Role.signup_role_ids
  60. user = User.create!(user_data)
  61. end
  62. if user_data[:image_source]
  63. avatar = Avatar.add(
  64. object: 'User',
  65. o_id: user.id,
  66. url: user_data[:image_source],
  67. source: 'twitter',
  68. deletable: true,
  69. updated_by_id: user.id,
  70. created_by_id: user.id,
  71. )
  72. # update user link
  73. if avatar && user.image != avatar.store_hash
  74. user.image = avatar.store_hash
  75. user.save
  76. end
  77. end
  78. # create or update authorization
  79. auth_data = {
  80. uid: tweet_user.id,
  81. username: tweet_user.screen_name,
  82. user_id: user.id,
  83. provider: 'twitter'
  84. }
  85. if auth
  86. auth.update!(auth_data)
  87. else
  88. Authorization.create!(auth_data)
  89. end
  90. user
  91. end
  92. def to_ticket(tweet, user, group_id, channel)
  93. UserInfo.current_user_id = user.id
  94. Rails.logger.debug { 'Create ticket from tweet...' }
  95. Rails.logger.debug { tweet.inspect }
  96. Rails.logger.debug { user.inspect }
  97. Rails.logger.debug { group_id.inspect }
  98. # normalize message
  99. message = {}
  100. if tweet.instance_of?(Twitter::Tweet)
  101. message = {
  102. type: 'tweet',
  103. text: tweet.text,
  104. }
  105. state = get_state(channel, tweet)
  106. end
  107. if tweet.is_a?(Twitter::DirectMessage)
  108. message = {
  109. type: 'direct_message',
  110. text: tweet.text,
  111. }
  112. state = get_state(channel, tweet)
  113. end
  114. if tweet.is_a?(Hash) && tweet['type'] == 'message_create'
  115. message = {
  116. type: 'direct_message',
  117. text: tweet['message_create']['message_data']['text'],
  118. }
  119. state = get_state(channel, tweet)
  120. end
  121. if tweet.is_a?(Hash) && tweet['text'].present?
  122. message = {
  123. type: 'tweet',
  124. text: tweet['text'],
  125. }
  126. state = get_state(channel, tweet)
  127. end
  128. # process message
  129. if message[:type] == 'direct_message'
  130. ticket = Ticket.find_by(
  131. create_article_type: Ticket::Article::Type.lookup(name: 'twitter direct-message'),
  132. customer_id: user.id,
  133. state: Ticket::State.where.not(
  134. state_type_id: Ticket::StateType.where(
  135. name: %w[closed merged removed],
  136. )
  137. )
  138. )
  139. return ticket if ticket
  140. end
  141. # prepare title
  142. title = message[:text]
  143. if title.length > 80
  144. title = "#{title[0, 80]}..."
  145. end
  146. Ticket.create!(
  147. customer_id: user.id,
  148. title: title,
  149. group_id: group_id || Group.first.id,
  150. state: state,
  151. priority: Ticket::Priority.find_by(default_create: true),
  152. preferences: {
  153. channel_id: channel.id,
  154. channel_screen_name: channel.options['user']['screen_name'],
  155. },
  156. )
  157. end
  158. def to_article_webhook(item, user, ticket, channel)
  159. Rails.logger.debug { 'Create article from tweet...' }
  160. Rails.logger.debug { item.inspect }
  161. Rails.logger.debug { user.inspect }
  162. Rails.logger.debug { ticket.inspect }
  163. # import tweet
  164. to = nil
  165. from = nil
  166. text = nil
  167. message_id = nil
  168. article_type = nil
  169. in_reply_to = nil
  170. attachments = []
  171. if item['type'] == 'message_create'
  172. message_id = item['id']
  173. text = item['message_create']['message_data']['text']
  174. if item['message_create']['message_data']['entities'] && item['message_create']['message_data']['entities']['urls'].present?
  175. item['message_create']['message_data']['entities']['urls'].each do |local_url|
  176. next if local_url['url'].blank?
  177. if local_url['expanded_url'].present?
  178. text.gsub!(%r{#{Regexp.quote(local_url['url'])}}, local_url['expanded_url'])
  179. elsif local_url['display_url']
  180. text.gsub!(%r{#{Regexp.quote(local_url['url'])}}, local_url['display_url'])
  181. end
  182. end
  183. end
  184. app = get_app_webhook(item['message_create']['source_app_id'])
  185. article_type = 'twitter direct-message'
  186. recipient_id = item['message_create']['target']['recipient_id']
  187. recipient_screen_name = to_user_webhook_data(item['message_create']['target']['recipient_id'])['screen_name']
  188. sender_id = item['message_create']['sender_id']
  189. sender_screen_name = to_user_webhook_data(item['message_create']['sender_id'])['screen_name']
  190. to = "@#{recipient_screen_name}"
  191. from = "@#{sender_screen_name}"
  192. twitter_preferences = {
  193. created_at: item['created_timestamp'],
  194. recipient_id: recipient_id,
  195. recipient_screen_name: recipient_screen_name,
  196. sender_id: sender_id,
  197. sender_screen_name: sender_screen_name,
  198. app_id: app['app_id'],
  199. app_name: app['app_name'],
  200. }
  201. article_preferences = {
  202. twitter: self.class.preferences_cleanup(twitter_preferences),
  203. links: [
  204. {
  205. url: DM_URL_TEMPLATE % [recipient_id, sender_id].map(&:to_i).sort.join('-'),
  206. target: '_blank',
  207. name: 'on Twitter',
  208. },
  209. ],
  210. }
  211. elsif item['text'].present?
  212. message_id = item['id']
  213. text = item['text']
  214. if item['extended_tweet'] && item['extended_tweet']['full_text'].present?
  215. text = item['extended_tweet']['full_text']
  216. end
  217. article_type = 'twitter status'
  218. sender_screen_name = item['user']['screen_name']
  219. from = "@#{sender_screen_name}"
  220. mention_ids = []
  221. if item['entities']
  222. item['entities']['user_mentions']&.each do |local_user|
  223. if to
  224. to += ', '
  225. else
  226. to = ''
  227. end
  228. to += "@#{local_user['screen_name']}"
  229. mention_ids.push local_user['id']
  230. end
  231. item['entities']['urls']&.each do |local_media|
  232. if local_media['url'].present?
  233. if local_media['expanded_url'].present?
  234. text.gsub!(%r{#{Regexp.quote(local_media['url'])}}, local_media['expanded_url'])
  235. elsif local_media['display_url']
  236. text.gsub!(%r{#{Regexp.quote(local_media['url'])}}, local_media['display_url'])
  237. end
  238. end
  239. end
  240. item['entities']['media']&.each do |local_media|
  241. if local_media['url'].present?
  242. if local_media['expanded_url'].present?
  243. text.gsub!(%r{#{Regexp.quote(local_media['url'])}}, local_media['expanded_url'])
  244. elsif local_media['display_url']
  245. text.gsub!(%r{#{Regexp.quote(local_media['url'])}}, local_media['display_url'])
  246. end
  247. end
  248. url = local_media['media_url_https'] || local_media['media_url']
  249. next if url.blank?
  250. result = download_file(url)
  251. if !result.success? || !result.body
  252. Rails.logger.error "Unable for download image from twitter (#{url}): #{result.code}"
  253. next
  254. end
  255. attachment = {
  256. filename: url.sub(%r{^.*/(.+?)$}, '\1'),
  257. content: result.body,
  258. }
  259. attachments.push attachment
  260. end
  261. end
  262. in_reply_to = item['in_reply_to_status_id']
  263. twitter_preferences = {
  264. mention_ids: mention_ids,
  265. geo: item['geo'],
  266. retweeted: item['retweeted'],
  267. possibly_sensitive: item['possibly_sensitive'],
  268. in_reply_to_user_id: item['in_reply_to_user_id'],
  269. place: item['place'],
  270. retweet_count: item['retweet_count'],
  271. source: item['source'],
  272. favorited: item['favorited'],
  273. truncated: item['truncated'],
  274. }
  275. article_preferences = {
  276. twitter: self.class.preferences_cleanup(twitter_preferences),
  277. links: [
  278. {
  279. url: STATUS_URL_TEMPLATE % item['id'],
  280. target: '_blank',
  281. name: 'on Twitter',
  282. },
  283. ],
  284. }
  285. else
  286. raise "Unknown tweet type '#{item.class}'"
  287. end
  288. UserInfo.current_user_id = user.id
  289. # set ticket state to open if not new
  290. ticket_state = get_state(channel, item, ticket)
  291. if ticket_state.name != ticket.state.name
  292. ticket.state = ticket_state
  293. ticket.save!
  294. end
  295. article = Ticket::Article.create!(
  296. from: from,
  297. to: to,
  298. body: text,
  299. message_id: message_id,
  300. ticket_id: ticket.id,
  301. in_reply_to: in_reply_to,
  302. type_id: Ticket::Article::Type.find_by(name: article_type).id,
  303. sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
  304. internal: false,
  305. preferences: self.class.preferences_cleanup(article_preferences),
  306. )
  307. attachments.each do |attachment|
  308. Store.create!(
  309. object: 'Ticket::Article',
  310. o_id: article.id,
  311. data: attachment[:content],
  312. filename: attachment[:filename],
  313. preferences: {},
  314. )
  315. end
  316. end
  317. def to_article(tweet, user, ticket, channel)
  318. Rails.logger.debug { 'Create article from tweet...' }
  319. Rails.logger.debug { tweet.inspect }
  320. Rails.logger.debug { user.inspect }
  321. Rails.logger.debug { ticket.inspect }
  322. # import tweet
  323. to = nil
  324. raise "Unknown tweet type '#{tweet.class}'" if tweet.class != Twitter::Tweet
  325. article_type = 'twitter status'
  326. from = "@#{tweet.user.screen_name}"
  327. mention_ids = []
  328. tweet.user_mentions&.each do |local_user|
  329. if to
  330. to += ', '
  331. else
  332. to = ''
  333. end
  334. to += "@#{local_user.screen_name}"
  335. mention_ids.push local_user.id
  336. end
  337. in_reply_to = tweet.in_reply_to_status_id
  338. twitter_preferences = {
  339. mention_ids: mention_ids,
  340. geo: tweet.geo,
  341. retweeted: tweet.retweeted?,
  342. possibly_sensitive: tweet.possibly_sensitive?,
  343. in_reply_to_user_id: tweet.in_reply_to_user_id,
  344. place: tweet.place,
  345. retweet_count: tweet.retweet_count,
  346. source: tweet.source,
  347. favorited: tweet.favorited?,
  348. truncated: tweet.truncated?,
  349. }
  350. UserInfo.current_user_id = user.id
  351. # set ticket state to open if not new
  352. ticket_state = get_state(channel, tweet, ticket)
  353. if ticket_state.name != ticket.state.name
  354. ticket.state = ticket_state
  355. ticket.save!
  356. end
  357. article_preferences = {
  358. twitter: self.class.preferences_cleanup(twitter_preferences),
  359. links: [
  360. {
  361. url: STATUS_URL_TEMPLATE % tweet.id,
  362. target: '_blank',
  363. name: 'on Twitter',
  364. },
  365. ],
  366. }
  367. Ticket::Article.create!(
  368. from: from,
  369. to: to,
  370. body: tweet.text,
  371. message_id: tweet.id,
  372. ticket_id: ticket.id,
  373. in_reply_to: in_reply_to,
  374. type_id: Ticket::Article::Type.find_by(name: article_type).id,
  375. sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
  376. internal: false,
  377. preferences: self.class.preferences_cleanup(article_preferences),
  378. )
  379. end
  380. def to_group(tweet, group_id, channel)
  381. Rails.logger.debug { 'import tweet' }
  382. ticket = nil
  383. Transaction.execute(reset_user_id: true, context: 'twitter') do
  384. # check if parent exists
  385. user = to_user(tweet)
  386. raise "Unknown tweet type '#{tweet.class}'" if tweet.class != Twitter::Tweet
  387. if tweet.in_reply_to_status_id && tweet.in_reply_to_status_id.to_s != ''
  388. existing_article = Ticket::Article.find_by(message_id: tweet.in_reply_to_status_id)
  389. if existing_article
  390. ticket = existing_article.ticket
  391. else
  392. begin
  393. parent_tweet = @client.status(tweet.in_reply_to_status_id)
  394. ticket = to_group(parent_tweet, group_id, channel)
  395. rescue Twitter::Error::NotFound, Twitter::Error::Forbidden => e
  396. # just ignore if tweet has already gone
  397. Rails.logger.info "Can't import tweet (#{tweet.in_reply_to_status_id}), #{e.message}"
  398. end
  399. end
  400. end
  401. if !ticket
  402. ticket = to_ticket(tweet, user, group_id, channel)
  403. end
  404. to_article(tweet, user, ticket, channel)
  405. end
  406. ticket
  407. end
  408. =begin
  409. create a tweet or direct message from an article
  410. =end
  411. def from_article(article)
  412. tweet = nil
  413. case article[:type]
  414. when 'twitter direct-message'
  415. Rails.logger.debug { "Create twitter direct message from article to '#{article[:to]}'..." }
  416. article[:to].delete!('@')
  417. authorization = Authorization.find_by(provider: 'twitter', username: article[:to])
  418. raise "Unable to lookup user_id for @#{article[:to]}" if !authorization
  419. tweet = @client.create_direct_message(authorization.uid.to_i, article[:body])
  420. when 'twitter status'
  421. Rails.logger.debug { 'Create tweet from article...' }
  422. # workaround for https://github.com/sferik/twitter/issues/677
  423. # https://github.com/zammad/zammad/issues/2873 - unable to post
  424. # tweets with * - replace `*` with the wide-asterisk `*`.
  425. article[:body].tr!('*', '*') if article[:body].present?
  426. tweet = @client.update(
  427. article[:body],
  428. {
  429. in_reply_to_status_id: article[:in_reply_to]
  430. }
  431. )
  432. else
  433. raise "Can't handle unknown twitter article type '#{article[:type]}'."
  434. end
  435. Rails.logger.debug { tweet.inspect }
  436. tweet
  437. end
  438. def get_state(channel, tweet, ticket = nil)
  439. user_id = if tweet.is_a?(Hash)
  440. if tweet['user'] && tweet['user']['id']
  441. tweet['user']['id']
  442. else
  443. tweet['message_create']['sender_id']
  444. end
  445. else
  446. user(tweet).id
  447. end
  448. # no changes in post is from page user itself
  449. if channel.options[:user][:id].to_s == user_id.to_s
  450. if !ticket
  451. return Ticket::State.find_by(name: 'closed')
  452. end
  453. return ticket.state
  454. end
  455. state = Ticket::State.find_by(default_create: true)
  456. return state if !ticket
  457. return ticket.state if ticket.state_id == state.id
  458. Ticket::State.find_by(default_follow_up: true)
  459. end
  460. def tweet_limit_reached(tweet, factor = 1)
  461. max_count = MAX_TWEETS_PER_IMPORT
  462. max_count *= factor
  463. type_id = Ticket::Article::Type.lookup(name: 'twitter status').id
  464. created_at = 15.minutes.ago
  465. created_count = Ticket::Article.where('created_at > ? AND type_id = ?', created_at, type_id).count
  466. if created_count > max_count
  467. Rails.logger.info "Tweet limit of #{created_count}/#{max_count} reached, ignored tweed id (#{tweet.id})"
  468. return true
  469. end
  470. false
  471. end
  472. =begin
  473. replace Twitter::Place and Twitter::Geo as hash and replace Twitter::NullObject with nil
  474. preferences = TwitterSync.preferences_cleanup(
  475. twitter: twitter_preferences,
  476. links: [
  477. {
  478. url: 'https://twitter.com/_/status/123',
  479. target: '_blank',
  480. name: 'on Twitter',
  481. },
  482. ],
  483. )
  484. or
  485. preferences = {
  486. twitter: TwitterSync.preferences_cleanup(twitter_preferences),
  487. links: [
  488. {
  489. url: 'https://twitter.com/_/status/123',
  490. target: '_blank',
  491. name: 'on Twitter',
  492. },
  493. ],
  494. }
  495. =end
  496. def self.preferences_cleanup(preferences)
  497. # replace Twitter::NullObject with nill to prevent elasticsearch index issue
  498. preferences.each do |key, value|
  499. if value.instance_of?(Twitter::Place) || value.instance_of?(Twitter::Geo)
  500. preferences[key] = value.to_h
  501. next
  502. end
  503. if value.instance_of?(Twitter::NullObject)
  504. preferences[key] = nil
  505. next
  506. end
  507. next if !value.is_a?(Hash)
  508. value.each do |sub_key, sub_level|
  509. if sub_level.instance_of?(NilClass)
  510. value[sub_key] = nil
  511. next
  512. end
  513. if sub_level.instance_of?(Twitter::Place) || sub_level.instance_of?(Twitter::Geo)
  514. value[sub_key] = sub_level.to_h
  515. next
  516. end
  517. next if sub_level.class != Twitter::NullObject
  518. value[sub_key] = nil
  519. end
  520. end
  521. if preferences[:twitter]
  522. if preferences[:twitter][:geo].blank?
  523. preferences[:twitter][:geo] = {}
  524. end
  525. if preferences[:twitter][:place].blank?
  526. preferences[:twitter][:place] = {}
  527. end
  528. else
  529. if preferences[:geo].blank?
  530. preferences[:geo] = {}
  531. end
  532. if preferences[:place].blank?
  533. preferences[:place] = {}
  534. end
  535. end
  536. preferences
  537. end
  538. =begin
  539. check if tweet is from local sender
  540. client = TwitterSync.new
  541. client.locale_sender?(tweet)
  542. =end
  543. def locale_sender?(tweet)
  544. tweet_user = user(tweet)
  545. Channel.where(area: 'Twitter::Account').each do |local_channel|
  546. next if !local_channel.options
  547. next if !local_channel.options[:user]
  548. next if !local_channel.options[:user][:id]
  549. next if local_channel.options[:user][:id].to_s != tweet_user.id.to_s
  550. Rails.logger.debug { "Tweet is sent by local account with user id #{tweet_user.id} and tweet.id #{tweet.id}" }
  551. return true
  552. end
  553. false
  554. end
  555. =begin
  556. process webhook messages from twitter
  557. client = TwitterSync.new
  558. client.process_webhook(channel)
  559. =end
  560. def process_webhook(channel)
  561. Rails.logger.debug { 'import tweet' }
  562. ticket = nil
  563. if @payload['direct_message_events'].present? && channel.options[:sync][:direct_messages][:group_id].present?
  564. @payload['direct_message_events'].each do |item|
  565. next if item['type'] != 'message_create'
  566. next if Ticket::Article.exists?(message_id: item['id'])
  567. user = to_user_webhook(item['message_create']['sender_id'])
  568. ticket = to_ticket(item, user, channel.options[:sync][:direct_messages][:group_id], channel)
  569. to_article_webhook(item, user, ticket, channel)
  570. end
  571. end
  572. if @payload['tweet_create_events'].present?
  573. @payload['tweet_create_events'].each do |item|
  574. next if Ticket::Article.exists?(message_id: item['id'])
  575. next if item.key?('retweeted_status') && !channel.options.dig('sync', 'track_retweets')
  576. # check if it's mention
  577. group_id = nil
  578. if channel.options[:sync][:mentions][:group_id].present? && item['entities']['user_mentions']
  579. item['entities']['user_mentions'].each do |local_user|
  580. next if channel.options[:user][:id].to_s != local_user['id'].to_s
  581. group_id = channel.options[:sync][:mentions][:group_id]
  582. break
  583. end
  584. end
  585. # check if it's search term
  586. if !group_id && channel.options[:sync][:search].present?
  587. channel.options[:sync][:search].each do |local_search|
  588. next if local_search[:term].blank?
  589. next if local_search[:group_id].blank?
  590. next if !item['text'].match?(%r{#{Regexp.quote(local_search[:term])}}i)
  591. group_id = local_search[:group_id]
  592. break
  593. end
  594. end
  595. next if !group_id
  596. user = to_user_webhook(item['user']['id'], item['user'])
  597. if item['in_reply_to_status_id'].present?
  598. existing_article = Ticket::Article.find_by(message_id: item['in_reply_to_status_id'])
  599. if existing_article
  600. ticket = existing_article.ticket
  601. else
  602. begin
  603. parent_tweet = @client.status(item['in_reply_to_status_id'])
  604. ticket = to_group(parent_tweet, group_id, channel)
  605. rescue Twitter::Error::NotFound, Twitter::Error::Forbidden => e
  606. # just ignore if tweet has already gone
  607. Rails.logger.info "Can't import tweet (#{item['in_reply_to_status_id']}), #{e.message}"
  608. end
  609. end
  610. end
  611. if !ticket
  612. ticket = to_ticket(item, user, group_id, channel)
  613. end
  614. to_article_webhook(item, user, ticket, channel)
  615. end
  616. end
  617. ticket
  618. end
  619. def get_app_webhook(app_id)
  620. return {} if !@payload['apps']
  621. return {} if !@payload['apps'][app_id]
  622. @payload['apps'][app_id]
  623. end
  624. def to_user_webhook_data(user_id)
  625. if @payload['user'] && @payload['user']['id'].to_s == user_id.to_s
  626. return @payload['user']
  627. end
  628. raise 'no users in payload' if !@payload['users']
  629. raise 'no users in payload' if !@payload['users'][user_id]
  630. @payload['users'][user_id]
  631. end
  632. =begin
  633. download public media file from twitter
  634. client = TwitterSync.new
  635. result = client.download_file(url)
  636. result.body
  637. =end
  638. def download_file(url)
  639. UserAgent.get(
  640. url,
  641. {},
  642. {
  643. open_timeout: 20,
  644. read_timeout: 40,
  645. verify_ssl: true,
  646. },
  647. )
  648. end
  649. def to_user_webhook(user_id, payload_user = nil)
  650. user_payload = if payload_user && payload_user['id'].to_s == user_id.to_s
  651. payload_user
  652. else
  653. to_user_webhook_data(user_id)
  654. end
  655. auth = Authorization.find_by(uid: user_payload['id'], provider: 'twitter')
  656. # create or update user
  657. user_data = {
  658. image_source: user_payload['profile_image_url'],
  659. }
  660. if auth
  661. user = User.find(auth.user_id)
  662. map = {
  663. note: 'description',
  664. web: 'url',
  665. address: 'location',
  666. }
  667. # ignore if value is already set
  668. map.each do |target, source|
  669. next if user[target].present?
  670. new_value = user_payload[source].to_s
  671. next if new_value.blank?
  672. user_data[target] = new_value
  673. end
  674. user.update!(user_data)
  675. else
  676. user_data[:login] = user_payload['screen_name']
  677. user_data[:firstname] = user_payload['name']
  678. user_data[:web] = user_payload['url']
  679. user_data[:note] = user_payload['description']
  680. user_data[:address] = user_payload['location']
  681. user_data[:active] = true
  682. user_data[:role_ids] = Role.signup_role_ids
  683. user = User.create!(user_data)
  684. end
  685. if user_data[:image_source].present?
  686. avatar = Avatar.add(
  687. object: 'User',
  688. o_id: user.id,
  689. url: user_data[:image_source],
  690. source: 'twitter',
  691. deletable: true,
  692. updated_by_id: user.id,
  693. created_by_id: user.id,
  694. )
  695. # update user link
  696. if avatar && user.image != avatar.store_hash
  697. user.image = avatar.store_hash
  698. user.save
  699. end
  700. end
  701. # create or update authorization
  702. auth_data = {
  703. uid: user_payload['id'],
  704. username: user_payload['screen_name'],
  705. user_id: user.id,
  706. provider: 'twitter'
  707. }
  708. if auth
  709. auth.update!(auth_data)
  710. else
  711. Authorization.create!(auth_data)
  712. end
  713. user
  714. end
  715. =begin
  716. get the user of current twitter client
  717. client = TwitterSync.new
  718. user_hash = client.who_am_i
  719. =end
  720. def who_am_i
  721. @client.user
  722. end
  723. =begin
  724. request a new webhook verification request from twitter
  725. client = TwitterSync.new
  726. webhook_request_verification(webhook_id, env_name, webhook_url)
  727. =end
  728. def webhook_request_verification(webhook_id, env_name, webhook_url)
  729. Twitter::REST::Request.new(@client, :put, "/1.1/account_activity/all/#{env_name}/webhooks/#{webhook_id}.json", {}).perform
  730. rescue => e
  731. raise "Webhook registered but not valid (#{webhook_url}). Unable to set webhook to valid: #{e.message}"
  732. end
  733. =begin
  734. get webhooks by env_name
  735. client = TwitterSync.new
  736. webhooks = webhooks_by_env_name(env_name)
  737. =end
  738. def webhooks_by_env_name(env_name)
  739. Twitter::REST::Request.new(@client, :get, "/1.1/account_activity/all/#{env_name}/webhooks.json", {}).perform
  740. end
  741. =begin
  742. get all webhooks
  743. client = TwitterSync.new
  744. webhooks = webhooks(env_name)
  745. =end
  746. def webhooks
  747. Twitter::REST::Request.new(@client, :get, '/1.1/account_activity/all/webhooks.json', {}).perform
  748. end
  749. =begin
  750. delete a webhooks
  751. client = TwitterSync.new
  752. webhook_delete(webhook_id, env_name)
  753. =end
  754. def webhook_delete(webhook_id, env_name)
  755. Twitter::REST::Request.new(@client, :delete, "/1.1/account_activity/all/#{env_name}/webhooks/#{webhook_id}.json", {}).perform
  756. end
  757. =begin
  758. register a new webhooks at twitter
  759. client = TwitterSync.new
  760. webhook_register(env_name, webhook_url)
  761. =end
  762. def webhook_register(env_name, webhook_url)
  763. options = {
  764. url: webhook_url,
  765. }
  766. begin
  767. response = Twitter::REST::Request.new(@client, :post, "/1.1/account_activity/all/#{env_name}/webhooks.json", options).perform
  768. rescue => e
  769. message = "Unable to register webhook: #{e.message}"
  770. if webhook_url.include?('http://')
  771. message += ' Only https webhooks possible to register.'
  772. elsif webhooks.count.positive?
  773. message += " Already #{webhooks.count} webhooks registered. Maybe you need to delete one first."
  774. end
  775. raise message
  776. end
  777. response
  778. end
  779. =begin
  780. subscribe a user to a webhooks at twitter
  781. client = TwitterSync.new
  782. webhook_subscribe(env_name)
  783. =end
  784. def webhook_subscribe(env_name)
  785. Twitter::REST::Request.new(@client, :post, "/1.1/account_activity/all/#{env_name}/subscriptions.json", {}).perform
  786. rescue => e
  787. raise "Unable to subscriptions with via webhook: #{e.message}"
  788. end
  789. end