ZReviewTender 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/ZReviewTender +96 -0
- data/lib/AndroidFetcher.rb +75 -0
- data/lib/AppleFetcher.rb +178 -0
- data/lib/Helper.rb +42 -0
- data/lib/Models/AndroidConfig.rb +23 -0
- data/lib/Models/AppleConfig.rb +25 -0
- data/lib/Models/Processor.rb +16 -0
- data/lib/Models/Review.rb +17 -0
- data/lib/Models/ReviewFetcher.rb +48 -0
- data/lib/Processors/GoogleTranslateProcessor.rb +53 -0
- data/lib/Processors/SlackProcessor.rb +142 -0
- metadata +111 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: f28c083172d2863eac2225b5dcd2f282252dc1ba549a367db473f9e6c0674550
         | 
| 4 | 
            +
              data.tar.gz: bb244ed194bf859f24f7952cae809ca9dac12048062c53acafb95e1baab71ca4
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 4baa4d768ee7c2c1e848a6ddec740cce709a94972c1157069fcd4601f364e09b32b75622b2f392facff383a14c775e3685b3226917e4c9322ebe8ebf8674c44e
         | 
| 7 | 
            +
              data.tar.gz: 678a9d03b6c4ca05a9d6d0555a50c4a3f3387ea9d2556f7b1a081ffafc4e335ea9251e09fd8714233ebd534ac06125a025197ae00547bb8ed3852471ec3c1bbc
         | 
    
        data/bin/ZReviewTender
    ADDED
    
    | @@ -0,0 +1,96 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            # -*- coding: utf-8 -*-
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            $lib = File.expand_path('../lib', File.dirname(__FILE__))
         | 
| 5 | 
            +
            $LOAD_PATH.unshift($lib)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require "Models/AppleConfig"
         | 
| 8 | 
            +
            require "Models/AndroidConfig"
         | 
| 9 | 
            +
            require "Models/Processor"
         | 
| 10 | 
            +
            require "Helper"
         | 
| 11 | 
            +
            require "AppleFetcher"
         | 
| 12 | 
            +
            require "AndroidFetcher"
         | 
| 13 | 
            +
            require "optparse"
         | 
| 14 | 
            +
            require "yaml"
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            class Main
         | 
| 17 | 
            +
                def initialize
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    ARGV << '-r' if ARGV.empty?
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    OptionParser.new do |opts|
         | 
| 22 | 
            +
                        opts.banner = "Usage: ZReviewTender [options]"
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                        basePath = ENV['PWD'] || ::Dir.pwd
         | 
| 25 | 
            +
                    
         | 
| 26 | 
            +
                        opts.on('-a', '--apple[=CONFIGYMLFILEPATH]', 'execute apple platform with config yml file') do |configYMLFilePath|
         | 
| 27 | 
            +
                            if configYMLFilePath.nil?
         | 
| 28 | 
            +
                                configYMLFilePath = "#{basePath}/config/apple.yml"
         | 
| 29 | 
            +
                            end
         | 
| 30 | 
            +
                            fetcher = parseConfigYMLFile(configYMLFilePath)
         | 
| 31 | 
            +
                            fetcher.execute()
         | 
| 32 | 
            +
                        end 
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                        opts.on('-g', '--googleAndroid[=CONFIGYMLFILEPATH]', 'execute apple platform with config yml file') do |configYMLFilePath|
         | 
| 35 | 
            +
                            if configYMLFilePath.nil?
         | 
| 36 | 
            +
                                configYMLFilePath = "#{basePath}/config/android.yml"
         | 
| 37 | 
            +
                            end
         | 
| 38 | 
            +
                            fetcher = parseConfigYMLFile(configYMLFilePath)
         | 
| 39 | 
            +
                            fetcher.execute()
         | 
| 40 | 
            +
                        end 
         | 
| 41 | 
            +
                        
         | 
| 42 | 
            +
                        opts.on('-r', '--run', 'execute with config yml file') do
         | 
| 43 | 
            +
                            appleConfigFilePath = "#{basePath}/config/android.yml"
         | 
| 44 | 
            +
                            fetcher = parseConfigYMLFile(appleConfigFilePath)
         | 
| 45 | 
            +
                            fetcher.execute()
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                            appleConfigFilePath = "#{basePath}/config/apple.yml"
         | 
| 48 | 
            +
                            fetcher = parseConfigYMLFile(appleConfigFilePath)
         | 
| 49 | 
            +
                            fetcher.execute()
         | 
| 50 | 
            +
                        end 
         | 
| 51 | 
            +
                    end.parse!
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                private
         | 
| 55 | 
            +
                def parseConfigYMLFile(configFilePath)
         | 
| 56 | 
            +
                    configYMLObj = YAML.load_file(configFilePath)
         | 
| 57 | 
            +
                    begin
         | 
| 58 | 
            +
                        platform = Helper.unwrapRequiredParameter(configYMLObj, 'platform')
         | 
| 59 | 
            +
                        
         | 
| 60 | 
            +
                        fetcher = nil
         | 
| 61 | 
            +
                        if platform.downcase == "apple"
         | 
| 62 | 
            +
                            fetcher = AppleFetcher.new(AppleConfig.new(configYMLObj, configFilePath, ENV['PWD'] || ::Dir.pwd))
         | 
| 63 | 
            +
                        elsif platform.downcase == "android"
         | 
| 64 | 
            +
                            fetcher = AndroidFetcher.new(AndroidConfig.new(configYMLObj, configFilePath, ENV['PWD'] || ::Dir.pwd))
         | 
| 65 | 
            +
                        else
         | 
| 66 | 
            +
                            raise "unknow platform #{platform} in yml file #{configFilePath}."
         | 
| 67 | 
            +
                        end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                        processors = Helper.unwrapRequiredParameter(configYMLObj, 'processors')
         | 
| 70 | 
            +
                        
         | 
| 71 | 
            +
                        if processors.length < 1
         | 
| 72 | 
            +
                            raise "must specify at least one processor."
         | 
| 73 | 
            +
                        end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                        processors.each do |processor|
         | 
| 76 | 
            +
                            processor.each do |key, value|
         | 
| 77 | 
            +
                                processorClass = Helper.unwrapRequiredParameter(value, "class")
         | 
| 78 | 
            +
                                require "Processors/#{processorClass}"
         | 
| 79 | 
            +
                                fetcher.registerProcessor(eval("#{processorClass}.new(#{value}, '#{configFilePath}', '#{ENV['PWD'] || ::Dir.pwd}')"))
         | 
| 80 | 
            +
                            end
         | 
| 81 | 
            +
                        end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
             | 
| 84 | 
            +
                        return fetcher
         | 
| 85 | 
            +
                    rescue => e
         | 
| 86 | 
            +
                        raise "#{e.message} in yml file #{configFilePath}."
         | 
| 87 | 
            +
                    end        
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
            end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
            begin 
         | 
| 92 | 
            +
                Main.new()
         | 
| 93 | 
            +
            rescue => e
         | 
| 94 | 
            +
                puts "#Error: #{e.class} #{e.message}\n"
         | 
| 95 | 
            +
                puts e.backtrace
         | 
| 96 | 
            +
            end
         | 
| @@ -0,0 +1,75 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../lib', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "Models/Review"
         | 
| 4 | 
            +
            require "Helper"
         | 
| 5 | 
            +
            require "Models/ReviewFetcher"
         | 
| 6 | 
            +
            require "time"
         | 
| 7 | 
            +
            require "google/apis/androidpublisher_v3"
         | 
| 8 | 
            +
            require "google/apis/content_v2"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            class AndroidFetcher < ReviewFetcher
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                attr_accessor :token, :client
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def initialize(config)
         | 
| 15 | 
            +
                    @processors = []
         | 
| 16 | 
            +
                    @config = config
         | 
| 17 | 
            +
                    @platform = 'Android'
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    @client = Google::Apis::AndroidpublisherV3::AndroidPublisherService.new
         | 
| 20 | 
            +
                    @client.authorization = Google::Auth::ServiceAccountCredentials.make_creds(json_key_io: config.keyContent, scope: 'https://www.googleapis.com/auth/androidpublisher')
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def execute()
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    latestCheckTimestamp = getPlatformLatestCheckTimestamp()
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    # init first time, send welcome message
         | 
| 28 | 
            +
                    if latestCheckTimestamp == 0 
         | 
| 29 | 
            +
                        sendWelcomMessage()
         | 
| 30 | 
            +
                        setPlatformLatestCheckTimestamp()
         | 
| 31 | 
            +
                        return
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    reviews = []
         | 
| 35 | 
            +
                    
         | 
| 36 | 
            +
                    # Google API Bug, couldn't specify limit/offse/pagination, google only return a few recent reviews.
         | 
| 37 | 
            +
                    customerReviews = client.list_reviews(config.packageName).reviews
         | 
| 38 | 
            +
                    customerReviews.each do |customerReview|
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                        customerReviewID = customerReview.review_id
         | 
| 41 | 
            +
                        customerReviewTitle = nil
         | 
| 42 | 
            +
                        customerReviewBody = customerReview.comments[0].user_comment.text.strip
         | 
| 43 | 
            +
                        customerReviewRating = customerReview.comments[0].user_comment.star_rating.to_i
         | 
| 44 | 
            +
                        customerReviewReviewerNickname = customerReview.author_name
         | 
| 45 | 
            +
                        customerReviewCreatedDateTimestamp = customerReview.comments[0].user_comment.last_modified.seconds.to_i
         | 
| 46 | 
            +
                        customerReviewTerritory = customerReview.comments[0].user_comment.reviewer_language
         | 
| 47 | 
            +
                        customerReviewVersionString = "unknown"
         | 
| 48 | 
            +
                        if !customerReview.comments[0].user_comment.app_version_name.nil?
         | 
| 49 | 
            +
                            customerReviewVersionString = "#{customerReview.comments[0].user_comment.app_version_name}"
         | 
| 50 | 
            +
                            if !customerReview.comments[0].user_comment.app_version_code.nil?
         | 
| 51 | 
            +
                                customerReviewVersionString = "#{customerReviewVersionString}(#{customerReview.comments[0].user_comment.app_version_code})"
         | 
| 52 | 
            +
                            end
         | 
| 53 | 
            +
                        end
         | 
| 54 | 
            +
                        customerReviewPlatform = "Android #{customerReview.comments[0].user_comment.android_os_version}"
         | 
| 55 | 
            +
                        
         | 
| 56 | 
            +
                        if latestCheckTimestamp > customerReviewCreatedDateTimestamp
         | 
| 57 | 
            +
                            break
         | 
| 58 | 
            +
                        else
         | 
| 59 | 
            +
                            url = "https://play.google.com/store/apps/details?id=#{config.packageName}&reviewId=#{customerReviewID}"
         | 
| 60 | 
            +
                            if !config.accountID.nil? && !config.appID.nil?
         | 
| 61 | 
            +
                                url = "https://play.google.com/console/developers/#{config.accountID}/app/#{config.appID}/user-feedback/review-details?reviewId=#{customerReviewID}"
         | 
| 62 | 
            +
                            end
         | 
| 63 | 
            +
                            reviews.append(Review.new(customerReviewPlatform, customerReviewID, customerReviewReviewerNickname, customerReviewRating, customerReviewTitle, customerReviewBody, customerReviewCreatedDateTimestamp, url, customerReviewVersionString, customerReviewTerritory))
         | 
| 64 | 
            +
                        end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                  
         | 
| 68 | 
            +
                    if reviews.length > 0
         | 
| 69 | 
            +
                        reviews.sort! { |a, b|  a.createdDateTimestamp <=> b.createdDateTimestamp }
         | 
| 70 | 
            +
                        processReviews(reviews, platform)
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    setPlatformLatestCheckTimestamp()
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
            end
         | 
    
        data/lib/AppleFetcher.rb
    ADDED
    
    | @@ -0,0 +1,178 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../lib', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "Models/Review"
         | 
| 4 | 
            +
            require "Helper"
         | 
| 5 | 
            +
            require "Models/ReviewFetcher"
         | 
| 6 | 
            +
            require "jwt"
         | 
| 7 | 
            +
            require "time"
         | 
| 8 | 
            +
            require "net/http"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            class AppleFetcher < ReviewFetcher
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                attr_accessor :token
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def initialize(config)
         | 
| 15 | 
            +
                    @processors = []
         | 
| 16 | 
            +
                    @config = config
         | 
| 17 | 
            +
                    @platform = 'Apple'
         | 
| 18 | 
            +
                    @token = generateJWT()
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def execute()
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    latestCheckTimestamp = getPlatformLatestCheckTimestamp()
         | 
| 24 | 
            +
             | 
| 25 | 
            +
             | 
| 26 | 
            +
                    # init first time, send welcome message
         | 
| 27 | 
            +
                    if latestCheckTimestamp == 0 
         | 
| 28 | 
            +
                        sendWelcomMessage()
         | 
| 29 | 
            +
                        setPlatformLatestCheckTimestamp()
         | 
| 30 | 
            +
                        return;
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    reviews = fetchReviews(latestCheckTimestamp)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    if reviews.length > 0
         | 
| 36 | 
            +
                        reviews.sort! { |a, b|  a.createdDateTimestamp <=> b.createdDateTimestamp }
         | 
| 37 | 
            +
                        reviews = fullfillAppInfo(reviews)
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                        processReviews(reviews, platform)
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    setPlatformLatestCheckTimestamp()
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                private
         | 
| 46 | 
            +
                def fetchReviews(latestCheckTimestamp)
         | 
| 47 | 
            +
                    customerReviewsLink = "https://api.appstoreconnect.apple.com/v1/apps/#{config.appID}/customerReviews?sort=-createdDate"
         | 
| 48 | 
            +
                    reviews = []
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    loop do
         | 
| 51 | 
            +
                        customerReviews = request(customerReviewsLink)
         | 
| 52 | 
            +
                        customerReviewsLink = customerReviews&.dig("links", "next")
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                        customerReviews&.dig("data").each do |customerReview|
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                            customerReviewID = customerReview&.dig("id")
         | 
| 57 | 
            +
                            customerReviewTitle = customerReview&.dig("attributes","title")
         | 
| 58 | 
            +
                            customerReviewBody= customerReview&.dig("attributes","body")
         | 
| 59 | 
            +
                            customerReviewRating = customerReview&.dig("attributes","rating").to_i
         | 
| 60 | 
            +
                            customerReviewReviewerNickname = customerReview&.dig("attributes","reviewerNickname")
         | 
| 61 | 
            +
                            customerReviewCreatedDate = customerReview&.dig("attributes","createdDate")
         | 
| 62 | 
            +
                            customerReviewTerritory = customerReview&.dig("attributes","territory")
         | 
| 63 | 
            +
                            
         | 
| 64 | 
            +
                            customerReviewCreatedDateTimestamp = 0
         | 
| 65 | 
            +
                            if !customerReviewCreatedDate.nil?
         | 
| 66 | 
            +
                                customerReviewCreatedDateTimestamp = Time.parse(customerReviewCreatedDate).to_i
         | 
| 67 | 
            +
                            end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                            if latestCheckTimestamp > customerReviewCreatedDateTimestamp
         | 
| 70 | 
            +
                                customerReviewsLink = nil
         | 
| 71 | 
            +
                                break
         | 
| 72 | 
            +
                            else
         | 
| 73 | 
            +
                                url = "https://appstoreconnect.apple.com/apps/#{config.appID}/appstore/activity/ios/ratingsResponses"
         | 
| 74 | 
            +
                                reviews.append(Review.new(nil, customerReviewID, customerReviewReviewerNickname, customerReviewRating, customerReviewTitle, customerReviewBody, customerReviewCreatedDateTimestamp, url, nil, customerReviewTerritory))
         | 
| 75 | 
            +
                            end
         | 
| 76 | 
            +
                        end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                        break if customerReviewsLink.nil?
         | 
| 79 | 
            +
                    end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                    return reviews
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                private
         | 
| 85 | 
            +
                def fullfillAppInfo(reviews)
         | 
| 86 | 
            +
                    customerReviewWhichAppVersionIsNil = reviews.select{ |review| review.appVersion.nil? }.map.with_index { |review, index| {"id":review.id, "index": index} }
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    appStoreVersionsLink = "https://api.appstoreconnect.apple.com/v1/apps/#{config.appID}/appStoreVersions"
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                    loop do
         | 
| 91 | 
            +
                        appStoreVersions = request(appStoreVersionsLink)
         | 
| 92 | 
            +
                        appStoreVersionsLink = appStoreVersions&.dig("links", "next")
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                        appStoreVersions&.dig("data").each do |appStoreVersion|
         | 
| 95 | 
            +
                            applePlatform = appStoreVersion&.dig("attributes","platform")
         | 
| 96 | 
            +
                            versionString = appStoreVersion&.dig("attributes","versionString")
         | 
| 97 | 
            +
                            customerReviewsLink = appStoreVersion&.dig("relationships","customerReviews","links","related")
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                            if !customerReviewsLink.nil?
         | 
| 100 | 
            +
                                customerReviewsLink = "#{customerReviewsLink}?sort=-createdDate&limit=200"
         | 
| 101 | 
            +
                                    
         | 
| 102 | 
            +
                                loop do
         | 
| 103 | 
            +
                                    customerReviews = request(customerReviewsLink)
         | 
| 104 | 
            +
                                    customerReviewsLink = customerReviews&.dig("links", "next")
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                                    customerReviews&.dig("data").each do |customerReview|
         | 
| 107 | 
            +
                                        customerReviewID = customerReview&.dig("id")
         | 
| 108 | 
            +
                                        if customerReviewID.nil?
         | 
| 109 | 
            +
                                            next
         | 
| 110 | 
            +
                                        end
         | 
| 111 | 
            +
                                        findIndex = customerReviewWhichAppVersionIsNil.find_index { |value| value[:id] == customerReviewID }
         | 
| 112 | 
            +
                                        if !findIndex.nil?
         | 
| 113 | 
            +
                                            findResult = customerReviewWhichAppVersionIsNil[findIndex]
         | 
| 114 | 
            +
                                            reviews[findResult[:index]].appVersion = versionString
         | 
| 115 | 
            +
                                            reviews[findResult[:index]].platform = applePlatform
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                                            customerReviewWhichAppVersionIsNil.delete_at(findIndex)
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                                            if customerReviewWhichAppVersionIsNil.length < 1
         | 
| 120 | 
            +
                                                customerReviewsLink = nil
         | 
| 121 | 
            +
                                                break
         | 
| 122 | 
            +
                                            end
         | 
| 123 | 
            +
                                        end
         | 
| 124 | 
            +
                                    end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                                    break if customerReviewsLink.nil?
         | 
| 127 | 
            +
                                end
         | 
| 128 | 
            +
                            end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                            if customerReviewWhichAppVersionIsNil.length < 1
         | 
| 131 | 
            +
                                appStoreVersionsLink = nil
         | 
| 132 | 
            +
                                break
         | 
| 133 | 
            +
                            end
         | 
| 134 | 
            +
                        end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                        break if appStoreVersionsLink.nil?
         | 
| 137 | 
            +
                    end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                    return reviews
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                private
         | 
| 143 | 
            +
                def generateJWT()
         | 
| 144 | 
            +
                    payload = {
         | 
| 145 | 
            +
                        iss: config.issueID,
         | 
| 146 | 
            +
                        iat: Time.now.to_i,
         | 
| 147 | 
            +
                        exp: Time.now.to_i + 60*20,
         | 
| 148 | 
            +
                        aud: 'appstoreconnect-v1'
         | 
| 149 | 
            +
                    }
         | 
| 150 | 
            +
                    token = JWT.encode payload, config.keyContent, 'ES256', header_fields={kid:config.keyID, typ:"JWT"}
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                private
         | 
| 154 | 
            +
                def request(url, retryCount = 0)
         | 
| 155 | 
            +
                    uri = URI(url)
         | 
| 156 | 
            +
                    https = Net::HTTP.new(uri.host, uri.port)
         | 
| 157 | 
            +
                    https.use_ssl = true
         | 
| 158 | 
            +
                    
         | 
| 159 | 
            +
                    request = Net::HTTP::Get.new(uri)
         | 
| 160 | 
            +
                    request['Authorization'] = "Bearer #{token}";
         | 
| 161 | 
            +
                    
         | 
| 162 | 
            +
                    response = https.request(request).read_body
         | 
| 163 | 
            +
                    
         | 
| 164 | 
            +
                    result = JSON.parse(response)
         | 
| 165 | 
            +
                    if !result["errors"].nil?
         | 
| 166 | 
            +
                        if retryCount >= 10
         | 
| 167 | 
            +
                            raise "Could not connect to api.appstoreconnect.apple.com, error message: #{response}"
         | 
| 168 | 
            +
                        else
         | 
| 169 | 
            +
                            @token = generateJWT()
         | 
| 170 | 
            +
                            Helper.logWarn("JWT Expired, refresh a new one. (#{retryCount + 1})")
         | 
| 171 | 
            +
                            return request(url, retryCount + 1)
         | 
| 172 | 
            +
                        end
         | 
| 173 | 
            +
                    else
         | 
| 174 | 
            +
                        return result
         | 
| 175 | 
            +
                    end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                end
         | 
| 178 | 
            +
            end
         | 
    
        data/lib/Helper.rb
    ADDED
    
    | @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "logger"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class Helper
         | 
| 6 | 
            +
                def self.unwrapRequiredParameter(obj, key)
         | 
| 7 | 
            +
                    if obj[key].nil?
         | 
| 8 | 
            +
                        raise "Required Parameter Not Found: #{key}"
         | 
| 9 | 
            +
                    else
         | 
| 10 | 
            +
                        if obj[key] == ''
         | 
| 11 | 
            +
                            raise "Required Parameter Is Empty: #{key}"
         | 
| 12 | 
            +
                        else
         | 
| 13 | 
            +
                            return obj[key]
         | 
| 14 | 
            +
                        end
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def self.createDirIfNotExist(dirPath)
         | 
| 19 | 
            +
                    dirs = dirPath.split("/")
         | 
| 20 | 
            +
                    currentDir = ""
         | 
| 21 | 
            +
                    begin
         | 
| 22 | 
            +
                        dir = dirs.shift
         | 
| 23 | 
            +
                        currentDir = "#{currentDir}/#{dir}"
         | 
| 24 | 
            +
                        Dir.mkdir(currentDir) unless File.exists?(currentDir)
         | 
| 25 | 
            +
                    end while dirs.length > 0
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def self.logError(message)
         | 
| 29 | 
            +
                    logger = Logger.new(STDOUT)
         | 
| 30 | 
            +
                    logger.error("#{caller[0]}: #{message}")
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def self.logWarn(message)
         | 
| 34 | 
            +
                    logger = Logger.new(STDOUT)
         | 
| 35 | 
            +
                    logger.warning("#{caller[0]}: #{message}")
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def self.logInfo(message)
         | 
| 39 | 
            +
                    logger = Logger.new(STDOUT)
         | 
| 40 | 
            +
                    logger.info("#{caller[0]}: #{message}")
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "pathname"
         | 
| 4 | 
            +
            require "Helper"
         | 
| 5 | 
            +
            require "time"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            class AndroidConfig
         | 
| 8 | 
            +
                attr_accessor :keyContent, :packageName, :accountID, :appID, :baseExecutePath
         | 
| 9 | 
            +
                def initialize(configYMLObj, configFilePath, baseExecutePath)
         | 
| 10 | 
            +
                    keyFilePath = Helper.unwrapRequiredParameter(configYMLObj,"keyFilePath")
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    if Pathname.new(keyFilePath).absolute?
         | 
| 13 | 
            +
                        configDir = File.dirname(configFilePath)
         | 
| 14 | 
            +
                        keyFilePath = "#{configDir}#{keyFilePath}"
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    @accountID = configYMLObj["playConsoleDeveloperAccountID"]
         | 
| 18 | 
            +
                    @appID = configYMLObj["playConsoleAppID"]
         | 
| 19 | 
            +
                    @keyContent = File.open(keyFilePath)
         | 
| 20 | 
            +
                    @baseExecutePath = baseExecutePath
         | 
| 21 | 
            +
                    @packageName = Helper.unwrapRequiredParameter(configYMLObj,"packageName")
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "pathname"
         | 
| 4 | 
            +
            require "Helper"
         | 
| 5 | 
            +
            require "openssl"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            class AppleConfig
         | 
| 8 | 
            +
                attr_accessor :keyContent, :keyID, :issueID, :appID, :baseExecutePath
         | 
| 9 | 
            +
                def initialize(configYMLObj, configFilePath, baseExecutePath)
         | 
| 10 | 
            +
                    keyFilePath = Helper.unwrapRequiredParameter(configYMLObj,"appStoreConnectP8PrivateKeyFilePath")
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    if Pathname.new(keyFilePath).absolute?
         | 
| 13 | 
            +
                        configDir = File.dirname(configFilePath)
         | 
| 14 | 
            +
                        keyFilePath = "#{configDir}#{keyFilePath}"
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    keyFile = File.read(keyFilePath)
         | 
| 18 | 
            +
                    @keyContent = OpenSSL::PKey::EC.new(keyFile)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    @baseExecutePath = baseExecutePath
         | 
| 21 | 
            +
                    @keyID = Helper.unwrapRequiredParameter(configYMLObj,"appStoreConnectP8PrivateKeyID")
         | 
| 22 | 
            +
                    @issueID = Helper.unwrapRequiredParameter(configYMLObj,"appStoreConnectIssueID")
         | 
| 23 | 
            +
                    @appID = Helper.unwrapRequiredParameter(configYMLObj,"appID")
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
            end
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class Processor
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                attr_accessor :config, :configFilePath, :baseExecutePath
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(config, configFilePath, baseExecutePath)
         | 
| 8 | 
            +
                    @config = config
         | 
| 9 | 
            +
                    @configFilePath = configFilePath
         | 
| 10 | 
            +
                    @baseExecutePath = baseExecutePath
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def processReviews(reviews, platform)
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class Review
         | 
| 4 | 
            +
                attr_accessor :platform, :id, :userName, :rating, :title, :body, :createdDateTimestamp, :url, :appVersion, :territory
         | 
| 5 | 
            +
                def initialize(platform, id, userName, rating, title, body, createdDateTimestamp, url, appVersion, territory)
         | 
| 6 | 
            +
                    @platform = platform
         | 
| 7 | 
            +
                    @id = id
         | 
| 8 | 
            +
                    @userName = userName
         | 
| 9 | 
            +
                    @rating = rating
         | 
| 10 | 
            +
                    @title = title
         | 
| 11 | 
            +
                    @body = body
         | 
| 12 | 
            +
                    @createdDateTimestamp = createdDateTimestamp
         | 
| 13 | 
            +
                    @url = url
         | 
| 14 | 
            +
                    @appVersion = appVersion
         | 
| 15 | 
            +
                    @territory = territory
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            $lib = File.expand_path('../lib', File.dirname(__FILE__))
         | 
| 3 | 
            +
             | 
| 4 | 
            +
             | 
| 5 | 
            +
            require "Processors/SlackProcessor"
         | 
| 6 | 
            +
            require "Models/Processor"
         | 
| 7 | 
            +
            require "time"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            class ReviewFetcher
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                attr_accessor :config, :platform, :processors
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def execute()
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def registerProcessor(processor)
         | 
| 18 | 
            +
                    processors.append(processor)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def processReviews(reviews, platform)
         | 
| 22 | 
            +
                    processors.each do |processor|
         | 
| 23 | 
            +
                        reviews = processor.processReviews(reviews, platform)
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def sendWelcomMessage()
         | 
| 28 | 
            +
                    slackProcessor = processors.find { |processor| processor.is_a?(SlackProcessor) }
         | 
| 29 | 
            +
                    if !slackProcessor.nil?
         | 
| 30 | 
            +
                        slackProcessor.sendWelcomMessage(platform)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def setPlatformLatestCheckTimestamp()
         | 
| 35 | 
            +
                    basePath = "#{config.baseExecutePath}/.cache"
         | 
| 36 | 
            +
                    Helper.createDirIfNotExist(basePath)
         | 
| 37 | 
            +
                    File.open("#{basePath}/#{platform}-latestCheckTimestamp", 'w') { |file| file.write(Time.now().to_i) }
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def getPlatformLatestCheckTimestamp()
         | 
| 41 | 
            +
                    filePath = "#{config.baseExecutePath}/.cache/#{platform}-latestCheckTimestamp"
         | 
| 42 | 
            +
                    if File.exists?(filePath)
         | 
| 43 | 
            +
                        return File.read(filePath).to_i
         | 
| 44 | 
            +
                    else
         | 
| 45 | 
            +
                        return 0
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
            end
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../lib', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "Models/Review"
         | 
| 4 | 
            +
            require "Models/Processor"
         | 
| 5 | 
            +
            require "Helper"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require "pathname"
         | 
| 8 | 
            +
            require "google/cloud/translate/v2"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            class GoogleTranslateProcessor < Processor
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                attr_accessor :client, :targetLang, :whiteListTerritories
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def initialize(config, configFilePath, baseExecutePath)
         | 
| 15 | 
            +
                    @config = config
         | 
| 16 | 
            +
                    @configFilePath = configFilePath
         | 
| 17 | 
            +
                    @baseExecutePath = baseExecutePath
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    keyFilePath = Helper.unwrapRequiredParameter(config, "googleTranslateAPIKeyFilePath")
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    if Pathname.new(keyFilePath).absolute?
         | 
| 22 | 
            +
                        configDir = File.dirname(configFilePath)
         | 
| 23 | 
            +
                        keyFilePath = "#{configDir}#{keyFilePath}"
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    ENV["TRANSLATE_CREDENTIALS"] = keyFilePath
         | 
| 27 | 
            +
                    @client = Google::Cloud::Translate::V2.new
         | 
| 28 | 
            +
                    @targetLang = Helper.unwrapRequiredParameter(config, "googleTranslateTargetLang")
         | 
| 29 | 
            +
                    @whiteListTerritories = []
         | 
| 30 | 
            +
                    if !config['googleTranslateWhiteListTerritories'].nil? && config['googleTranslateWhiteListTerritories'].length > 0
         | 
| 31 | 
            +
                        @whiteListTerritories = config['googleTranslateWhiteListTerritories']
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def processReviews(reviews, platform)
         | 
| 36 | 
            +
                    reviews.each_index do |index|
         | 
| 37 | 
            +
                        if whiteListTerritories.include? reviews[index].territory
         | 
| 38 | 
            +
                            next
         | 
| 39 | 
            +
                        end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                        if !reviews[index].title.nil?
         | 
| 42 | 
            +
                            reviews[index].title = "#{client.translate reviews[index].title, to: targetLang} (#{reviews[index].title})"
         | 
| 43 | 
            +
                        end
         | 
| 44 | 
            +
                        body = "#{client.translate reviews[index].body, to: targetLang}"
         | 
| 45 | 
            +
                        body += "\r\n===== Translate by Google =====\r\n"
         | 
| 46 | 
            +
                        body += reviews[index].body
         | 
| 47 | 
            +
                        reviews[index].body = body
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    return reviews
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            end
         | 
| @@ -0,0 +1,142 @@ | |
| 1 | 
            +
            $lib = File.expand_path('../lib', File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "Models/Review"
         | 
| 4 | 
            +
            require "Models/Processor"
         | 
| 5 | 
            +
            require "Helper"
         | 
| 6 | 
            +
            require "net/http"
         | 
| 7 | 
            +
            require "json"
         | 
| 8 | 
            +
            require "time"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            class SlackProcessor < Processor
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                attr_accessor :botToken, :inCommingWebHookURL, :targetChannel, :timeZoneOffset
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def initialize(config, configFilePath, baseExecutePath)
         | 
| 15 | 
            +
                    @config = config
         | 
| 16 | 
            +
                    @configFilePath = configFilePath
         | 
| 17 | 
            +
                    @baseExecutePath = baseExecutePath
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    @botToken = config["slackBotToken"]
         | 
| 20 | 
            +
                    @inCommingWebHookURL = config["slackInCommingWebHookURL"]
         | 
| 21 | 
            +
                    @targetChannel = config["slackBotTargetChannel"]
         | 
| 22 | 
            +
                    @timeZoneOffset = Helper.unwrapRequiredParameter(config, "slackTimeZoneOffset")
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    if (botToken.nil? && inCommingWebHookURL.nil?) || (botToken == "" && inCommingWebHookURL == "")
         | 
| 25 | 
            +
                        raise "must specify slackBotToken or slackInCommingWebHookURL in SlackProcessor."
         | 
| 26 | 
            +
                    elsif !botToken.nil? && botToken != "" && (targetChannel.nil? || targetChannel == "")
         | 
| 27 | 
            +
                        raise "must specify slackBotTargetChannel in SlackProcessor."
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def processReviews(reviews, platform)
         | 
| 32 | 
            +
                    reviews.each_slice(50) do |reviewGroup|
         | 
| 33 | 
            +
                        payload = Payload.new()
         | 
| 34 | 
            +
                        payload.attachments = []
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                        reviewGroup.each do |review|
         | 
| 37 | 
            +
                            attachment = Payload::Attachment.new()
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                            stars = "★" * review.rating + "☆" * (5 - review.rating)
         | 
| 40 | 
            +
                            color = review.rating >= 4 ? "good" : (review.rating > 2 ? "warning" : "danger")
         | 
| 41 | 
            +
                            date = Time.at(review.createdDateTimestamp).getlocal(timeZoneOffset)
         | 
| 42 | 
            +
                            
         | 
| 43 | 
            +
                            title = "#{stars}"
         | 
| 44 | 
            +
                            if !review.title.nil?
         | 
| 45 | 
            +
                                title = "#{review.title} - #{stars}"
         | 
| 46 | 
            +
                            end
         | 
| 47 | 
            +
                                
         | 
| 48 | 
            +
                            attachment.color = color
         | 
| 49 | 
            +
                            attachment.author_name = review.userName
         | 
| 50 | 
            +
                            attachment.fallback = title
         | 
| 51 | 
            +
                            attachment.title = title
         | 
| 52 | 
            +
                            attachment.text = review.body
         | 
| 53 | 
            +
                            attachment.footer = "#{platform} - #{review.platform} - #{review.appVersion} - #{review.territory} - <#{review.url}|#{date}>"
         | 
| 54 | 
            +
                            
         | 
| 55 | 
            +
                            payload.attachments.append(attachment)
         | 
| 56 | 
            +
                        end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                        result = request(payload)
         | 
| 59 | 
            +
                        if result["ok"] != true
         | 
| 60 | 
            +
                            Helper.logError(result)
         | 
| 61 | 
            +
                        end
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    return reviews
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def sendWelcomMessage(platform)
         | 
| 68 | 
            +
                    payload = Payload.new()
         | 
| 69 | 
            +
                    payload.attachments = []
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    attachment = Payload::Attachment.new()
         | 
| 72 | 
            +
                        
         | 
| 73 | 
            +
                    title = "ZReviewTender Standing By :astronaut:"
         | 
| 74 | 
            +
                    body = "#{platform} Init Success!, will resend App Review to this channel automatically."
         | 
| 75 | 
            +
                    
         | 
| 76 | 
            +
                    attachment.color = "good"
         | 
| 77 | 
            +
                    attachment.author_name = "<https://zhgchg.li|ZhgChgLi>"
         | 
| 78 | 
            +
                    attachment.fallback = title
         | 
| 79 | 
            +
                    attachment.title = title
         | 
| 80 | 
            +
                    attachment.text = body
         | 
| 81 | 
            +
                    attachment.footer = "Powered by "
         | 
| 82 | 
            +
                    
         | 
| 83 | 
            +
                    payload.attachments.append(attachment)
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    request(payload)
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                private
         | 
| 89 | 
            +
                def request(payload)
         | 
| 90 | 
            +
                    if !botToken.nil? && botToken != ""
         | 
| 91 | 
            +
                        uri = URI("https://slack.com/api/chat.postMessage")
         | 
| 92 | 
            +
                        payload.channel = targetChannel
         | 
| 93 | 
            +
                        headers = {'Content-Type': 'application/json; charset=utf-8', 'Authorization': "Bearer #{botToken}"}
         | 
| 94 | 
            +
                    else
         | 
| 95 | 
            +
                        uri = URI(inCommingWebHookURL)
         | 
| 96 | 
            +
                        payload.channel = nil
         | 
| 97 | 
            +
                        headers = {'Content-Type': 'application/json; charset=utf-8'}
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
                    
         | 
| 100 | 
            +
                    http = Net::HTTP.new(uri.host, uri.port)
         | 
| 101 | 
            +
                    http.use_ssl = true
         | 
| 102 | 
            +
                    req = Net::HTTP::Post.new(uri.request_uri, headers)
         | 
| 103 | 
            +
                    req.body = payload.to_json
         | 
| 104 | 
            +
                    res = http.request(req)
         | 
| 105 | 
            +
                    JSON.parse(res.body)
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                private
         | 
| 109 | 
            +
                class Payload
         | 
| 110 | 
            +
                    attr_accessor :channel, :attachments
         | 
| 111 | 
            +
                    class Attachment
         | 
| 112 | 
            +
                        attr_accessor :pretext, :color, :fallback, :title, :text, :author_name, :footer
         | 
| 113 | 
            +
                    
         | 
| 114 | 
            +
                        def as_json(options={})
         | 
| 115 | 
            +
                        {
         | 
| 116 | 
            +
                            pretext: @pretext,
         | 
| 117 | 
            +
                            color: @color,
         | 
| 118 | 
            +
                            fallback: @fallback,
         | 
| 119 | 
            +
                            title: @title,
         | 
| 120 | 
            +
                            text: @text,
         | 
| 121 | 
            +
                            author_name: @author_name,
         | 
| 122 | 
            +
                            footer: @footer
         | 
| 123 | 
            +
                        }
         | 
| 124 | 
            +
                        end
         | 
| 125 | 
            +
                
         | 
| 126 | 
            +
                        def to_json(*options)
         | 
| 127 | 
            +
                            as_json(*options).to_json(*options)
         | 
| 128 | 
            +
                        end
         | 
| 129 | 
            +
                    end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    def as_json(options={})
         | 
| 132 | 
            +
                    {
         | 
| 133 | 
            +
                        channel: @channel,
         | 
| 134 | 
            +
                        attachments: @attachments
         | 
| 135 | 
            +
                    }
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                    def to_json(*options)
         | 
| 139 | 
            +
                        as_json(*options).to_json(*options)
         | 
| 140 | 
            +
                    end
         | 
| 141 | 
            +
                end
         | 
| 142 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,111 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: ZReviewTender
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.0.1
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - ZhgChgLi
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2022-08-07 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: net-http
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - "~>"
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: 0.1.0
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - "~>"
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: 0.1.0
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: jwt
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - "~>"
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: 2.4.1
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - "~>"
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: 2.4.1
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: google-cloud-translate-v2
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - "~>"
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: 0.3.0
         | 
| 48 | 
            +
              type: :runtime
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - "~>"
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: 0.3.0
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: google-apis-androidpublisher_v3
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - "~>"
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: 0.25.0
         | 
| 62 | 
            +
              type: :runtime
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - "~>"
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: 0.25.0
         | 
| 69 | 
            +
            description: ZReviewTender - Monitor your App reviews in your Slack channel
         | 
| 70 | 
            +
            email: 
         | 
| 71 | 
            +
            executables:
         | 
| 72 | 
            +
            - ZReviewTender
         | 
| 73 | 
            +
            extensions: []
         | 
| 74 | 
            +
            extra_rdoc_files: []
         | 
| 75 | 
            +
            files:
         | 
| 76 | 
            +
            - bin/ZReviewTender
         | 
| 77 | 
            +
            - lib/AndroidFetcher.rb
         | 
| 78 | 
            +
            - lib/AppleFetcher.rb
         | 
| 79 | 
            +
            - lib/Helper.rb
         | 
| 80 | 
            +
            - lib/Models/AndroidConfig.rb
         | 
| 81 | 
            +
            - lib/Models/AppleConfig.rb
         | 
| 82 | 
            +
            - lib/Models/Processor.rb
         | 
| 83 | 
            +
            - lib/Models/Review.rb
         | 
| 84 | 
            +
            - lib/Models/ReviewFetcher.rb
         | 
| 85 | 
            +
            - lib/Processors/GoogleTranslateProcessor.rb
         | 
| 86 | 
            +
            - lib/Processors/SlackProcessor.rb
         | 
| 87 | 
            +
            homepage: https://github.com/ZhgChgLi/ZReviewTender
         | 
| 88 | 
            +
            licenses:
         | 
| 89 | 
            +
            - MIT
         | 
| 90 | 
            +
            metadata: {}
         | 
| 91 | 
            +
            post_install_message: 
         | 
| 92 | 
            +
            rdoc_options: []
         | 
| 93 | 
            +
            require_paths:
         | 
| 94 | 
            +
            - lib
         | 
| 95 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 96 | 
            +
              requirements:
         | 
| 97 | 
            +
              - - ">="
         | 
| 98 | 
            +
                - !ruby/object:Gem::Version
         | 
| 99 | 
            +
                  version: '0'
         | 
| 100 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 101 | 
            +
              requirements:
         | 
| 102 | 
            +
              - - ">="
         | 
| 103 | 
            +
                - !ruby/object:Gem::Version
         | 
| 104 | 
            +
                  version: '0'
         | 
| 105 | 
            +
            requirements: []
         | 
| 106 | 
            +
            rubygems_version: 3.0.3
         | 
| 107 | 
            +
            signing_key: 
         | 
| 108 | 
            +
            specification_version: 4
         | 
| 109 | 
            +
            summary: ZReviewTender uses brand new App Store & Google Play API to resend App reviews
         | 
| 110 | 
            +
              to your Slack channel automatically.
         | 
| 111 | 
            +
            test_files: []
         |