├── .github ├── images │ ├── og.png │ ├── preview.png │ └── preview_ko.png ├── FUNDING.yml └── workflows │ └── fetch.yml ├── Gemfile ├── locales ├── ja.json ├── ko.json └── en.json ├── package.json ├── .gitignore ├── README-JAPANESE.md ├── README-KOREAN.md ├── Sources ├── fetch_app_status.rb ├── check_status.js ├── discord.js └── slack.js ├── README.md └── Gemfile.lock /.github/images/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techinpark/appstore-status-bot/HEAD/.github/images/og.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | ruby "~> 2.6" 3 | 4 | gem "fastlane" 5 | gem "rubocop", require: false -------------------------------------------------------------------------------- /.github/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techinpark/appstore-status-bot/HEAD/.github/images/preview.png -------------------------------------------------------------------------------- /.github/images/preview_ko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techinpark/appstore-status-bot/HEAD/.github/images/preview_ko.png -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "Prepare for submission": "提出準備中", 3 | "Waiting for review": "審査待ち", 4 | "In review": "審査中", 5 | "Pending contract": "契約保留中", 6 | "Waiting for export compliance": "輸出コンプライアンス待ち", 7 | "Pending developer release": "デベロッパのリリース保留", 8 | "Processing for app store": "App Storeで処理中", 9 | "Pending apple release": "Appleのリリース待ち", 10 | "Ready for sale": "配信準備完了", 11 | "Rejected": "却下済み", 12 | "Metadata rejected": "メタデータ却下済み", 13 | "Removed from sale": "ストアから削除済み", 14 | "Developer rejected": "デベロッパにより却下済み", 15 | "Developer removed from sale": "デベロッパによりストアから削除済み", 16 | "Invalid binary": "Invalid Binary", 17 | "Status": "状態", 18 | "Version": "バージョン", 19 | "Message": "*{{appname}}*の状態が*{{status}}*に変更されました。 🚀" 20 | } 21 | -------------------------------------------------------------------------------- /locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "Prepare for submission": "제출 준비 중", 3 | "Waiting for review": "심사 대기 중", 4 | "In review": "심사 중", 5 | "Pending contract": "대기 중인 계약", 6 | "Waiting for export compliance": "수출 규정 관련 문서 승인 대기중", 7 | "Pending developer release": "개발자 출시 대기 중", 8 | "Processing for app store": "App Store 판매 준비중", 9 | "Pending apple release": "대기중인 앱 이전", 10 | "Ready for sale": "판매 준비됨", 11 | "Rejected": "거부됨", 12 | "Metadata rejected": "메타데이터가 거부됨", 13 | "Removed from sale": "판매가 중단됨", 14 | "Developer rejected": "개발자가 취소함", 15 | "Developer removed from sale": "개발자가 판매를 중단함", 16 | "Invalid binary": "Invalid Binary", 17 | "Status": "상태", 18 | "Version": "버전", 19 | "Message": "*{{appname}}* 앱의 상태가 *{{status}}* 으로 변경 되었습니다. 🚀" 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [techinpark] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appstore-status-bot", 3 | "version": "1.0.3", 4 | "description": "", 5 | "main": "Sources/check_status.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/techinpark/appstore-status-bot.git" 12 | }, 13 | "author": "Fernando", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/techinpark/appstore-status-bot/issues" 17 | }, 18 | "homepage": "https://github.com/techinpark/appstore-status-bot#readme", 19 | "dependencies": { 20 | "@slack/webhook": "^5.0.3", 21 | "child_process": "^1.0.2", 22 | "dirty": "^1.1.0", 23 | "i18n": "^0.13.2", 24 | "moment": "^2.29.2", 25 | "octokit": "^1.7.1", 26 | "request": "^2.88.2", 27 | "request-promise-native": "^1.0.9" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Prepare for submission": "Prepare for Submission", 3 | "Waiting for review": "Waiting For Review", 4 | "In review": "In Review", 5 | "Pending contract": "Pending Contract", 6 | "Waiting for export compliance": "Waiting For Export Compliance", 7 | "Pending developer release": "Pending Developer Release", 8 | "Processing for app store": "Processing for App Store", 9 | "Pending apple release": "Pending Apple Release", 10 | "Ready for sale": "Ready for Sale", 11 | "Rejected": "Rejected", 12 | "Metadata rejected": "Metadata Rejected", 13 | "Removed from sale": "Removed From Sale", 14 | "Developer rejected": "Developer Rejected", 15 | "Developer removed from sale": "Developer Removed From Sale", 16 | "Invalid binary": "Invalid Binary", 17 | "Status": "Status", 18 | "Version": "Version", 19 | "Message": "The status of your app *{{appname}}* has been changed to *{{status}}* 🚀" 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/fetch.yml: -------------------------------------------------------------------------------- 1 | name: Fetch Appstore Info 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0/15 * * * *" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2.4.1 14 | with: 15 | node-version: "16.x" 16 | env: 17 | ACTIONS_ALLOW_UNSECURE_COMMANDS: "true" 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: "2.7" 21 | - run: gem install bundler:2.1.4 22 | - run: bundle install 23 | - run: npm install 24 | - run: gem install fastlane -v '2.219.0' 25 | - run: node Sources/check_status.js 26 | env: 27 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 28 | KEY_ID: ${{ secrets.KEY_ID }} 29 | ISSUER_ID: ${{ secrets.ISSUER_ID }} 30 | BUNDLE_ID: ${{ secrets.BUNDLE_ID }} 31 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 32 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 33 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 34 | GIST_ID: ${{ secrets.GIST_ID }} 35 | LANGUAGE: "ko" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # End of https://www.toptal.com/developers/gitignore/api/node -------------------------------------------------------------------------------- /README-JAPANESE.md: -------------------------------------------------------------------------------- 1 |

20 |
21 |
22 | # 使用 👨🏻💻
23 |
24 | ## 1. APIをコールするためにはトークンをまず作ります。
25 | `KEY ID` を得るために [App Store Connect](https://appstoreconnect.apple.com/)へ接続します。
26 |
27 | 1. `ユーザとアクセス`をクリック、 `キー` タブをクリックします。
28 | 2. 新しいAPIキーを作成します。
29 | 3. `キー ID` をコピーしておきます。
30 | 4. `Issuer ID` もコピーしておきます。
31 | 5. 作られた `API Key file (.p8)` をダウンロードします。
32 | > ⚠️ ページを再読み込みすると二度とダウンロードが出来なるなるのでご注意を!
33 |
34 | ## 2. 事前準備
35 | 6. SlackのWebhook URLを発行します。
36 | 7. このリポジトリをForkします。
37 |
38 |
39 | ## 3. `Secrets`の設定
40 |
41 | - リポジトリの設定から `Settings` - `Secrets and variables` - `New repository secret` 順番にコピーした項目を設定します。
42 |
43 | ### コピーした項目の設定
44 |
45 | > PRIVATE_KEY: ダウンロードした `key file(.p8)`をテキストに開いて全部コピペして入れます。
46 | > KEY_ID : `キー ID`をここに入力します。
47 | > ISSUER_ID : `Issuer ID`もここに入力します。
48 | > BUNDLE_ID : 状態の確認したいアプリの `bundle identifier`を入力します。 (2個以上のアプリの場合は、「 」 スペースを入れずに、「,」記号を使うと動作します。)
49 | > 2個以上のアプリの場合は、カンマ記号を使い、スペースを入れずに入力してください
50 | > SLACK_WEBHOOK : SlackのWebhook URLを入力します。
51 | > DISCORD_WEBHOOK : DiscordのWebhook URLを入力します。 (optional)
52 | > GH_TOKEN: Githubのトークンを入力します。 (`gists`と `repo` 権限が必要です。 )
53 | > GIST_ID: gistファイルを作成し、 URLに存在するキーをコピーして入力します。
54 | - https://gist.github.com/techinpark/**9842e074b8ee46aef76fd0d493bae0ed**
55 |
56 | ## 4. 言語設定、インターバル設定
57 |
58 | - [fetch.yml](./.github/workflows/fetch.yml)
59 |
60 | `workflow` ファイルに言語設定、スケジュールの設定ができます。基本 `15分`で動いてます。
61 |
62 |
63 | # レファレンス 🙇🏻♂️
64 |
65 | - https://github.com/fastlane/fastlane/tree/master/spaceship
66 | - https://github.com/erikvillegas/itunes-connect-slack
67 | - https://github.com/rogerluan/app-store-connect-notifier
68 |
69 |
70 | # コントリビュート
71 | - オープンソースなので全てのPR大歓迎です。 🤩
72 |
--------------------------------------------------------------------------------
/README-KOREAN.md:
--------------------------------------------------------------------------------
1 | 
20 |
21 |
22 | # 사용법 👨🏻💻
23 |
24 | ## 1. API를 호출하기 위해서는 토큰을 먼저 생성합니다.
25 | `KEY ID` 를 얻기 위해서는 먼저 [App Store Connect](https://appstoreconnect.apple.com/) 에 접속합니다.
26 |
27 | 1. `사용자 및 액세스`를 선택하고, `키` 탭을 선택합니다.
28 | 2. 새로운 API키를 생성합니다.
29 | 3. `키 ID` 를 선택해서 복사 해둡니다.
30 | 4. `Issuer ID` 도 선택해서 복사를 해둡니다.
31 | 5. 생성된 `API Key file (.p8)` 을 다운로드 합니다.
32 | > ⚠️ 페이지를 새로고침하면 다시 다운로드 할 수 없으니 주의해주세요!
33 |
34 | ## 2. 사전 준비
35 | 6. 슬랙 Webhook URL 발급 받습니다.
36 | 7. 해당 레포지토리를 Fork 합니다.
37 |
38 |
39 | ## 3. `Secrets` 설정하기
40 |
41 | - 깃헙 레포 페이지에서 `Settings` - `Secrets and variables` - `New repository secret` 로 들어가서 위에서 복사한 정보들을 세팅해줍니다.
42 |
43 | ### 복사해야하는 정보들
44 |
45 | > PRIVATE_KEY: 다운로드한 `key file(.p8)`을 텍스트로 열어서 복사한후 넣어주시면 됩니다.
46 | > KEY_ID : `키 ID`를 이곳에 입력합니다.
47 | > ISSUER_ID : `Issuer ID`도 이곳에 입력합니다.
48 | > BUNDLE_ID : 상태를 확인하고 싶은 앱의 `bundle identifier` 을 입력해줍니다. (공백 없이 콤마로 구분하시면 2개이상의 앱도 가능합니다.)
49 | > SLACK_WEBHOOK : 슬랙 Webhook URL을 넣어줍니다.
50 | > DISCORD_WEBHOOK : 디스코드 Webhook URL을 넣어줍니다. (optional)
51 | > GH_TOKEN: 깃헙 토큰을 넣어줍니다 (`gists` 와 `repo` 권한이 필요합니다 )
52 | > GIST_ID: gist파일을 생성하고 URL에 존재하는 키값을 복사해서 넣어줍니다.
53 | - https://gist.github.com/techinpark/**9842e074b8ee46aef76fd0d493bae0ed**
54 |
55 | ## 4. 언어 설정 및 탐색 주기 설정
56 |
57 | - [fetch.yml](./.github/workflows/fetch.yml)
58 |
59 | `workflow` 파일 내부에서 언어 설정 및 스케줄 시간을 설정 하실 수 있습니다. 기본값은 `15분` 단위로 되어있습니다.
60 |
61 |
62 | # 레퍼런스 🙇🏻♂️
63 |
64 | - https://github.com/fastlane/fastlane/tree/master/spaceship
65 | - https://github.com/erikvillegas/itunes-connect-slack
66 | - https://github.com/rogerluan/app-store-connect-notifier
67 |
68 |
69 | # 기여하기
70 | - 오픈소스이므로 모든 PR은 환영합니다. 🤩
71 |
--------------------------------------------------------------------------------
/Sources/fetch_app_status.rb:
--------------------------------------------------------------------------------
1 | require "spaceship"
2 | require "json"
3 | require 'tempfile'
4 |
5 | def get_app_state(app)
6 |
7 | edit_version_info = app.get_edit_app_store_version
8 | in_review_version_info = app.get_in_review_app_store_version
9 | pending_version_info = app.get_pending_release_app_store_version
10 | latest_version_info = app.get_latest_app_store_version
11 |
12 | version_string = ""
13 | app_store_state = ""
14 |
15 | if edit_version_info.nil? == false
16 | version_string = edit_version_info.version_string
17 | app_store_state = edit_version_info.app_store_state.gsub("_", " ").capitalize
18 | elsif in_review_version_info.nil? == false
19 | version_string = in_review_version_info.version_string
20 | app_store_state = in_review_version_info.app_store_state.gsub("_", " ").capitalize
21 | elsif pending_version_info.nil? == false
22 | version_string = pending_version_info.version_string
23 | app_store_state = pending_version_info.app_store_state.gsub("_", " ").capitalize
24 | elsif latest_version_info.nil? == false
25 | version_string = latest_version_info.version_string
26 | app_store_state = latest_version_info.app_store_state.gsub("_", " ").capitalize
27 | end
28 |
29 | icon_url = ""
30 | live_version_info = app.get_live_app_store_version
31 | if live_version_info.nil? == false
32 | icon_url = live_version_info.build.icon_asset_token["templateUrl"]
33 | icon_url["{w}"] = "340"
34 | icon_url["{h}"] = "340"
35 | icon_url["{f}"] = "png"
36 | end
37 |
38 | {
39 | "name" => app.name,
40 | "version" => version_string,
41 | "status" => app_store_state,
42 | "appID" => app.id,
43 | "iconURL" => icon_url
44 | }
45 |
46 | end
47 |
48 | def get_app_version_from(bundle_id)
49 | apps = []
50 | if bundle_id
51 | apps.push(Spaceship::ConnectAPI::App.find(bundle_id))
52 | else
53 | apps = Spaceship::ConnectAPI::App.all
54 | end
55 | apps.compact.map { |app| get_app_state(app) }
56 | end
57 |
58 |
59 | # Create temp file.
60 | p8 = ENV['PRIVATE_KEY']
61 | p8_file = Tempfile.new('AuthKey')
62 | p8_file.write(p8)
63 | p8_file.rewind
64 |
65 | bundle_id = ENV['BUNDLE_ID']
66 | versions = []
67 |
68 | token = Spaceship::ConnectAPI::Token.create(
69 | key_id: ENV['KEY_ID'],
70 | issuer_id: ENV['ISSUER_ID'],
71 | filepath: File.absolute_path(p8_file.path)
72 | )
73 |
74 |
75 | Spaceship::ConnectAPI.token = token
76 |
77 | bundle_id_array = bundle_id.to_s.split(",")
78 |
79 | if bundle_id_array.length.zero?
80 | versions += get_app_version_from(nil)
81 | else
82 | bundle_id_array.each do |bundle_id|
83 | versions += get_app_version_from(bundle_id)
84 | end
85 | end
86 |
87 | puts JSON.dump versions
88 | p8_file.unlink
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
22 |
23 |
24 | # Usage 👨🏻💻
25 |
26 | ## 1. Generating Tokens for API Requests
27 | To get your Key ID, copy it from App Store Connect by logging in to [App Store Connect](https://appstoreconnect.apple.com/), then:
28 |
29 | 1. Select Users and Access, then select the API Keys tab.
30 | 2. The key IDs appear in a column under the Active heading. Hover the cursor next to a key ID to display the Copy Key ID link.
31 | 3. Click Copy Key ID and paste it.
32 | 4. Click Copy Issuer ID and paste it.
33 | 5. Download the newly created API Key file (.p8)
34 | > ⚠️ This file cannot be downloaded again after the page has been refreshed
35 |
36 | 6. Generate Slack Webhook token.
37 | 7. Fork this repository.
38 |
39 | ## 3. Setting Secrets with your keys.
40 |
41 | - Go to `Settings` - `Secrets and variables` - `New repository secret`
42 |
43 | ### Secret Values
44 |
45 | > PRIVATE_KEY: Input raw data about your API Key file (.p8)
46 | > KEY_ID : Input Appstore connect `key_id`
47 | > ISSUER_ID : Input Appstore connect `issuer_id`
48 | > BUNDLE_ID : Input your bundle_identifier of application you can input multiple bundle_id with comma and no whitespace
49 | > SLACK_WEBHOOK : Input your slack webhook url
50 | > DISCORD_WEBHOOK : Input your discord webhook url (optional)
51 | > GH_TOKEN: Input your github token, (need `gists` and `repo` scope).
52 | > GIST_ID: Input portion from your gist url:
53 | - https://gist.github.com/techinpark/**9842e074b8ee46aef76fd0d493bae0ed**
54 |
55 |
56 | ## 4. Configure fetch timing or languages
57 |
58 | - [fetch.yml](./.github/workflows/fetch.yml)
59 |
60 | In `workflow` file, can change lanauges and fetch schedule default `schedule` is every 15 minutes.
61 |
62 |
63 | # References 🙇🏻♂️
64 |
65 | - https://github.com/fastlane/fastlane/tree/master/spaceship
66 | - https://github.com/erikvillegas/itunes-connect-slack
67 | - https://github.com/rogerluan/app-store-connect-notifier
68 |
69 |
70 | # Contribution
71 | - Feel free to contribution for this project.
72 | - Every `PR`, `Issues` is wellcome. 🤩
73 |
--------------------------------------------------------------------------------
/Sources/check_status.js:
--------------------------------------------------------------------------------
1 | const slack = require("./slack.js");
2 | const discord = require("./discord.js");
3 | const exec = require("child_process").exec;
4 | const dirty = require("dirty");
5 | const { Octokit, App } = require("octokit");
6 | const request = require("request-promise-native");
7 | const { prependOnceListener } = require("process");
8 | const fs = require("fs").promises;
9 | const env = Object.create(process.env);
10 | const octokit = new Octokit({ auth: `token ${process.env.GH_TOKEN}` });
11 |
12 | const main = async () => {
13 | await getGist();
14 |
15 | exec(
16 | "ruby Sources/fetch_app_status.rb",
17 | { env: env },
18 | function (err, stdout, stderr) {
19 | if (stdout) {
20 | var apps = JSON.parse(stdout);
21 | console.log(apps);
22 | for (let app of apps) {
23 | checkVersion(app);
24 | }
25 | } else {
26 | console.log("There was a problem fetching the status of the app!");
27 | console.log(stderr);
28 | }
29 | }
30 | );
31 | };
32 |
33 | const checkVersion = async (app) => {
34 | var appInfoKey = "appInfo-" + app.appID;
35 | var submissionStartKey = "submissionStart" + app.appID;
36 |
37 | const db = dirty("store.db");
38 | db.on("load", async function () {
39 | var lastAppInfo = db.get(appInfoKey);
40 | if (!lastAppInfo || lastAppInfo.status != app.status) {
41 | console.log("[*] status is different");
42 |
43 | slack.post(app, db.get(submissionStartKey));
44 | discord.post(app, db.get(submissionStartKey));
45 |
46 | if (app.status == "Waiting For Review") {
47 | db.set(submissionStartKey, new Date());
48 | }
49 | } else {
50 | console.log("[*] status is same");
51 | }
52 |
53 | db.set(appInfoKey, app);
54 |
55 | try {
56 | const data = await fs.readFile("store.db", "utf-8");
57 | await updateGist(data);
58 | } catch (error) {
59 | console.log(error);
60 | }
61 | });
62 | };
63 |
64 | const getGist = async () => {
65 | const gist = await octokit.rest.gists
66 | .get({
67 | gist_id: process.env.GIST_ID,
68 | })
69 | .catch((error) => console.error(`[*] Unable to update gist\n${error}`));
70 | if (!gist) return;
71 |
72 | const filename = Object.keys(gist.data.files)[0];
73 | const rawdataURL = gist.data.files[filename].raw_url;
74 |
75 | const options = {
76 | url: rawdataURL,
77 | };
78 |
79 | const result = await request.get(options);
80 | try {
81 | await fs.writeFile("store.db", result);
82 | console.log("[*] file saved!");
83 | } catch (error) {
84 | console.log(error);
85 | }
86 | };
87 |
88 | const updateGist = async (content) => {
89 | const gist = await octokit.rest.gists
90 | .get({
91 | gist_id: process.env.GIST_ID,
92 | })
93 | .catch((error) => console.error(`[*] Unable to update gist\n${error}`));
94 | if (!gist) return;
95 |
96 | const filename = Object.keys(gist.data.files)[0];
97 | await octokit.rest.gists.update({
98 | gist_id: process.env.GIST_ID,
99 | files: {
100 | [filename]: {
101 | content: content,
102 | },
103 | },
104 | });
105 | };
106 |
107 | main();
108 |
--------------------------------------------------------------------------------
/Sources/discord.js:
--------------------------------------------------------------------------------
1 | const moment = require("moment");
2 | const path = require("path");
3 | const fetch = require("node-fetch");
4 | const { I18n } = require("i18n");
5 |
6 | const webhookURL = process.env.DISCORD_WEBHOOK;
7 | const language = process.env.LANGUAGE;
8 | const i18n = new I18n();
9 |
10 | i18n.configure({
11 | locales: ['en', 'ko', 'ja'],
12 | directory: path.join(__dirname, '../locales'),
13 | defaultLocale: 'en'
14 | });
15 |
16 | i18n.setLocale(language || 'en');
17 |
18 | function post(appInfo, submissionStartDate) {
19 | if (!webhookURL) return;
20 | const status = i18n.__(appInfo.status);
21 | const message = i18n.__("Message", { appname: appInfo.name, status: status });
22 | const embed = discordEmbed(appInfo, submissionStartDate);
23 |
24 | hook(message, embed);
25 | }
26 |
27 | async function hook(message, embed) {
28 | const payload = {
29 | content: message,
30 | embeds: [embed]
31 | };
32 |
33 | await fetch(webhookURL, {
34 | method: 'POST',
35 | headers: {
36 | 'Content-Type': 'application/json',
37 | },
38 | body: JSON.stringify(payload),
39 | });
40 | }
41 |
42 | function discordEmbed(appInfo, submissionStartDate) {
43 | const embed = {
44 | title: "App Store Connect",
45 | author: {
46 | name: appInfo.name,
47 | icon_url: appInfo.iconURL
48 | },
49 | url: `https://appstoreconnect.apple.com/apps/${appInfo.appID}/appstore`,
50 | fields: [
51 | {
52 | name: i18n.__("Version"),
53 | value: appInfo.version,
54 | inline: true,
55 | },
56 | {
57 | name: i18n.__("Status"),
58 | value: i18n.__(appInfo.status),
59 | inline: true,
60 | }
61 | ],
62 | footer: {
63 | text: "appstore-status-bot",
64 | icon_url: "https://icons-for-free.com/iconfiles/png/512/app+store+apple+apps+game+games+store+icon-1320085881005897327.png"
65 | },
66 | timestamp: new Date()
67 | };
68 |
69 | // Set elapsed time since "Waiting For Review" start
70 | if (
71 | submissionStartDate &&
72 | appInfo.status !== "Prepare for Submission" &&
73 | appInfo.status !== "Waiting For Review"
74 | ) {
75 | const elapsedHours = moment().diff(moment(submissionStartDate), "hours");
76 | embed.fields.push({
77 | name: "Elapsed Time",
78 | value: `${elapsedHours} hours`,
79 | inline: true,
80 | });
81 | }
82 |
83 | embed.color = colorForStatus(appInfo.status);
84 |
85 | return embed;
86 | }
87 |
88 | function colorForStatus(status) {
89 | const infoColor = 0x8e8e8e;
90 | const warningColor = 0xf4f124;
91 | const successColor1 = 0x1eb6fc;
92 | const successColor2 = 0x14ba40;
93 | const failureColor = 0xe0143d;
94 | const colorMapping = {
95 | "Prepare for Submission": infoColor,
96 | "Waiting For Review": infoColor,
97 | "In Review": successColor1,
98 | "Pending Contract": warningColor,
99 | "Waiting For Export Compliance": warningColor,
100 | "Pending Developer Release": successColor2,
101 | "Processing for App Store": successColor2,
102 | "Pending Apple Release": successColor2,
103 | "Ready for Sale": successColor2,
104 | Rejected: failureColor,
105 | "Metadata Rejected": failureColor,
106 | "Removed From Sale": failureColor,
107 | "Developer Rejected": failureColor,
108 | "Developer Removed From Sale": failureColor,
109 | "Invalid Binary": failureColor,
110 | };
111 |
112 | return colorMapping[status];
113 | }
114 |
115 | module.exports = {
116 | post: post,
117 | };
118 |
--------------------------------------------------------------------------------
/Sources/slack.js:
--------------------------------------------------------------------------------
1 | const moment = require("moment");
2 | const path = require("path");
3 | const { IncomingWebhook } = require("@slack/webhook");
4 | const { I18n } = require("i18n");
5 |
6 | const webhookURL = process.env.SLACK_WEBHOOK;
7 | const language = process.env.LANGUAGE;
8 | const i18n = new I18n();
9 |
10 | i18n.configure({
11 | locales: ['en','ko', 'ja'],
12 | directory: path.join(__dirname, '../locales'),
13 | defaultLocale: 'en'
14 | });
15 |
16 | i18n.setLocale(language || 'en');
17 |
18 | function post(appInfo, submissionStartDate) {
19 | const status = i18n.__(appInfo.status);
20 | const message = i18n.__("Message", { appname: appInfo.name, status: status });
21 | const attachment = slackAttachment(appInfo, submissionStartDate);
22 |
23 | const params = {
24 | attachments: [attachment],
25 | as_user: "true",
26 | };
27 |
28 | hook(message, attachment);
29 | }
30 |
31 | async function hook(message, attachment) {
32 |
33 | if (!webhookURL) {
34 | console.log("No Slack webhook URL provided.");
35 | return;
36 | }
37 |
38 | const webhook = new IncomingWebhook(webhookURL, {});
39 | await webhook.send({
40 | text: message,
41 | attachments: [attachment],
42 | });
43 | }
44 |
45 | function slackAttachment(appInfo, submissionStartDate) {
46 | const attachment = {
47 | fallback: `The status of your app ${appInfo.name} has been changed to ${appInfo.status}`,
48 | color: colorForStatus(appInfo.status),
49 | title: "App Store Connect",
50 | author_name: appInfo.name,
51 | author_icon: appInfo.iconURL,
52 | title_link: `https://appstoreconnect.apple.com/apps/${appInfo.appID}/appstore`,
53 | fields: [
54 | {
55 | title: i18n.__("Version"),
56 | value: appInfo.version,
57 | short: true,
58 | },
59 | {
60 | title: i18n.__("Status"),
61 | value: i18n.__(appInfo.status),
62 | short: true,
63 | },
64 | ],
65 | footer: "appstore-status-bot",
66 | footer_icon:
67 | "https://icons-for-free.com/iconfiles/png/512/app+store+apple+apps+game+games+store+icon-1320085881005897327.png",
68 | ts: new Date().getTime() / 1000,
69 | };
70 |
71 | // Set elapsed time since "Waiting For Review" start
72 | if (
73 | submissionStartDate &&
74 | appInfo.status != "Prepare for Submission" &&
75 | appInfo.status != "Waiting For Review"
76 | ) {
77 | const elapsedHours = moment().diff(moment(submissionStartDate), "hours");
78 | attachment["fields"].push({
79 | title: "Elapsed Time",
80 | value: `${elapsedHours} hours`,
81 | short: true,
82 | });
83 | }
84 | return attachment;
85 | }
86 |
87 | function colorForStatus(status) {
88 | const infoColor = "#8e8e8e";
89 | const warningColor = "#f4f124";
90 | const successColor1 = "#1eb6fc";
91 | const successColor2 = "#14ba40";
92 | const failureColor = "#e0143d";
93 | const colorMapping = {
94 | "Prepare for Submission": infoColor,
95 | "Waiting For Review": infoColor,
96 | "In Review": successColor1,
97 | "Pending Contract": warningColor,
98 | "Waiting For Export Compliance": warningColor,
99 | "Pending Developer Release": successColor2,
100 | "Processing for App Store": successColor2,
101 | "Pending Apple Release": successColor2,
102 | "Ready for Sale": successColor2,
103 | Rejected: failureColor,
104 | "Metadata Rejected": failureColor,
105 | "Removed From Sale": failureColor,
106 | "Developer Rejected": failureColor,
107 | "Developer Removed From Sale": failureColor,
108 | "Invalid Binary": failureColor,
109 | };
110 |
111 | return colorMapping[status];
112 | }
113 |
114 | module.exports = {
115 | post: post,
116 | };
117 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.2)
5 | addressable (2.7.0)
6 | public_suffix (>= 2.0.2, < 5.0)
7 | ast (2.4.1)
8 | atomos (0.1.3)
9 | aws-eventstream (1.1.0)
10 | aws-partitions (1.374.0)
11 | aws-sdk-core (3.107.0)
12 | aws-eventstream (~> 1, >= 1.0.2)
13 | aws-partitions (~> 1, >= 1.239.0)
14 | aws-sigv4 (~> 1.1)
15 | jmespath (~> 1.0)
16 | aws-sdk-kms (1.38.0)
17 | aws-sdk-core (~> 3, >= 3.99.0)
18 | aws-sigv4 (~> 1.1)
19 | aws-sdk-s3 (1.81.0)
20 | aws-sdk-core (~> 3, >= 3.104.3)
21 | aws-sdk-kms (~> 1)
22 | aws-sigv4 (~> 1.1)
23 | aws-sigv4 (1.2.2)
24 | aws-eventstream (~> 1, >= 1.0.2)
25 | babosa (1.0.3)
26 | claide (1.0.3)
27 | colored (1.2)
28 | colored2 (3.1.2)
29 | commander-fastlane (4.4.6)
30 | highline (~> 1.7.2)
31 | declarative (0.0.20)
32 | declarative-option (0.1.0)
33 | digest-crc (0.6.1)
34 | rake (~> 13.0)
35 | domain_name (0.5.20190701)
36 | unf (>= 0.0.5, < 1.0.0)
37 | dotenv (2.7.6)
38 | emoji_regex (3.0.0)
39 | excon (0.76.0)
40 | faraday (1.0.1)
41 | multipart-post (>= 1.2, < 3)
42 | faraday-cookie_jar (0.0.7)
43 | faraday (>= 0.8.0)
44 | http-cookie (~> 1.0.0)
45 | faraday_middleware (1.0.0)
46 | faraday (~> 1.0)
47 | fastimage (2.2.0)
48 | fastlane (2.160.0)
49 | CFPropertyList (>= 2.3, < 4.0.0)
50 | addressable (>= 2.3, < 3.0.0)
51 | aws-sdk-s3 (~> 1.0)
52 | babosa (>= 1.0.3, < 2.0.0)
53 | bundler (>= 1.12.0, < 3.0.0)
54 | colored
55 | commander-fastlane (>= 4.4.6, < 5.0.0)
56 | dotenv (>= 2.1.1, < 3.0.0)
57 | emoji_regex (>= 0.1, < 4.0)
58 | excon (>= 0.71.0, < 1.0.0)
59 | faraday (~> 1.0)
60 | faraday-cookie_jar (~> 0.0.6)
61 | faraday_middleware (~> 1.0)
62 | fastimage (>= 2.1.0, < 3.0.0)
63 | gh_inspector (>= 1.1.2, < 2.0.0)
64 | google-api-client (>= 0.37.0, < 0.39.0)
65 | google-cloud-storage (>= 1.15.0, < 2.0.0)
66 | highline (>= 1.7.2, < 2.0.0)
67 | json (< 3.0.0)
68 | jwt (>= 2.1.0, < 3)
69 | mini_magick (>= 4.9.4, < 5.0.0)
70 | multipart-post (~> 2.0.0)
71 | plist (>= 3.1.0, < 4.0.0)
72 | rubyzip (>= 2.0.0, < 3.0.0)
73 | security (= 0.1.3)
74 | simctl (~> 1.6.3)
75 | slack-notifier (>= 2.0.0, < 3.0.0)
76 | terminal-notifier (>= 2.0.0, < 3.0.0)
77 | terminal-table (>= 1.4.5, < 2.0.0)
78 | tty-screen (>= 0.6.3, < 1.0.0)
79 | tty-spinner (>= 0.8.0, < 1.0.0)
80 | word_wrap (~> 1.0.0)
81 | xcodeproj (>= 1.13.0, < 2.0.0)
82 | xcpretty (~> 0.3.0)
83 | xcpretty-travis-formatter (>= 0.0.3)
84 | gh_inspector (1.1.3)
85 | google-api-client (0.38.0)
86 | addressable (~> 2.5, >= 2.5.1)
87 | googleauth (~> 0.9)
88 | httpclient (>= 2.8.1, < 3.0)
89 | mini_mime (~> 1.0)
90 | representable (~> 3.0)
91 | retriable (>= 2.0, < 4.0)
92 | signet (~> 0.12)
93 | google-cloud-core (1.5.0)
94 | google-cloud-env (~> 1.0)
95 | google-cloud-errors (~> 1.0)
96 | google-cloud-env (1.3.3)
97 | faraday (>= 0.17.3, < 2.0)
98 | google-cloud-errors (1.0.1)
99 | google-cloud-storage (1.29.0)
100 | addressable (~> 2.5)
101 | digest-crc (~> 0.4)
102 | google-api-client (~> 0.33)
103 | google-cloud-core (~> 1.2)
104 | googleauth (~> 0.9)
105 | mini_mime (~> 1.0)
106 | googleauth (0.13.1)
107 | faraday (>= 0.17.3, < 2.0)
108 | jwt (>= 1.4, < 3.0)
109 | memoist (~> 0.16)
110 | multi_json (~> 1.11)
111 | os (>= 0.9, < 2.0)
112 | signet (~> 0.14)
113 | highline (1.7.10)
114 | http-cookie (1.0.3)
115 | domain_name (~> 0.5)
116 | httpclient (2.8.3)
117 | jmespath (1.4.0)
118 | json (2.3.1)
119 | jwt (2.2.2)
120 | memoist (0.16.2)
121 | mini_magick (4.10.1)
122 | mini_mime (1.0.2)
123 | multi_json (1.15.0)
124 | multipart-post (2.0.0)
125 | nanaimo (0.3.0)
126 | naturally (2.2.0)
127 | os (1.1.1)
128 | parallel (1.19.2)
129 | parser (2.7.1.4)
130 | ast (~> 2.4.1)
131 | plist (3.5.0)
132 | public_suffix (4.0.6)
133 | rainbow (3.0.0)
134 | rake (13.0.1)
135 | regexp_parser (1.8.0)
136 | representable (3.0.4)
137 | declarative (< 0.1.0)
138 | declarative-option (< 0.2.0)
139 | uber (< 0.2.0)
140 | retriable (3.1.2)
141 | rexml (3.2.4)
142 | rouge (2.0.7)
143 | rubocop (0.91.1)
144 | parallel (~> 1.10)
145 | parser (>= 2.7.1.1)
146 | rainbow (>= 2.2.2, < 4.0)
147 | regexp_parser (>= 1.7)
148 | rexml
149 | rubocop-ast (>= 0.4.0, < 1.0)
150 | ruby-progressbar (~> 1.7)
151 | unicode-display_width (>= 1.4.0, < 2.0)
152 | rubocop-ast (0.4.2)
153 | parser (>= 2.7.1.4)
154 | ruby-progressbar (1.10.1)
155 | rubyzip (2.3.0)
156 | security (0.1.3)
157 | signet (0.14.0)
158 | addressable (~> 2.3)
159 | faraday (>= 0.17.3, < 2.0)
160 | jwt (>= 1.5, < 3.0)
161 | multi_json (~> 1.10)
162 | simctl (1.6.8)
163 | CFPropertyList
164 | naturally
165 | slack-notifier (2.3.2)
166 | terminal-notifier (2.0.0)
167 | terminal-table (1.8.0)
168 | unicode-display_width (~> 1.1, >= 1.1.1)
169 | tty-cursor (0.7.1)
170 | tty-screen (0.8.1)
171 | tty-spinner (0.9.3)
172 | tty-cursor (~> 0.7)
173 | uber (0.1.0)
174 | unf (0.1.4)
175 | unf_ext
176 | unf_ext (0.0.7.7)
177 | unicode-display_width (1.7.0)
178 | word_wrap (1.0.0)
179 | xcodeproj (1.18.0)
180 | CFPropertyList (>= 2.3.3, < 4.0)
181 | atomos (~> 0.1.3)
182 | claide (>= 1.0.2, < 2.0)
183 | colored2 (~> 3.1)
184 | nanaimo (~> 0.3.0)
185 | xcpretty (0.3.0)
186 | rouge (~> 2.0.7)
187 | xcpretty-travis-formatter (1.0.0)
188 | xcpretty (~> 0.2, >= 0.0.7)
189 |
190 | PLATFORMS
191 | ruby
192 |
193 | DEPENDENCIES
194 | fastlane
195 | rubocop
196 |
197 | RUBY VERSION
198 | ruby 2.6.4p104
199 |
200 | BUNDLED WITH
201 | 2.1.4
202 |
--------------------------------------------------------------------------------