├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── gempush.yml │ └── ruby.yml ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── .yardopts ├── CONTRIBUTING.md ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── twitter-ads ├── error-hierarchy.png ├── examples ├── active_entities.rb ├── analytics.rb ├── audience_estimate.rb ├── audience_permissions.rb ├── batch_request.rb ├── cards.rb ├── cards_fetch.rb ├── custom_audience.rb ├── draft_tweet.rb ├── manual_request.rb ├── metric_filtering.rb ├── poll_cards.rb ├── promoted_tweet.rb ├── quick_start.rb └── tweet_previews.rb ├── features ├── step_definitions │ └── .gitkeep └── support │ └── env.rb ├── lib ├── twitter-ads.rb └── twitter-ads │ ├── account.rb │ ├── audiences │ └── custom_audience.rb │ ├── campaign │ ├── advertiser_business_categories.rb │ ├── app_list.rb │ ├── campaign.rb │ ├── content_categories.rb │ ├── funding_instrument.rb │ ├── iab_category.rb │ ├── line_item.rb │ ├── organic_tweet.rb │ ├── promotable_user.rb │ ├── targeting_criteria.rb │ ├── tracking_tags.rb │ └── tweet.rb │ ├── client.rb │ ├── creative │ ├── account_media.rb │ ├── cards.rb │ ├── cards_fetch.rb │ ├── draft_tweet.rb │ ├── image_conversation_card.rb │ ├── media_creative.rb │ ├── media_library.rb │ ├── poll_cards.rb │ ├── promoted_account.rb │ ├── promoted_tweet.rb │ ├── scheduled_tweet.rb │ ├── tweet_previews.rb │ ├── tweets.rb │ └── video_conversation_card.rb │ ├── cursor.rb │ ├── enum.rb │ ├── error.rb │ ├── http │ ├── request.rb │ └── response.rb │ ├── legacy.rb │ ├── measurement │ ├── app_event_tag.rb │ └── web_event_tag.rb │ ├── resources │ ├── analytics.rb │ ├── batch.rb │ ├── dsl.rb │ ├── persistence.rb │ └── resource.rb │ ├── restapi.rb │ ├── settings │ ├── tax.rb │ └── user.rb │ ├── targeting │ └── audience_estimate.rb │ ├── targeting_criteria │ ├── app_store_category.rb │ ├── conversation.rb │ ├── device.rb │ ├── event.rb │ ├── interest.rb │ ├── language.rb │ ├── location.rb │ ├── network_operator.rb │ ├── platform.rb │ ├── platform_version.rb │ ├── tv_market.rb │ └── tv_show.rb │ ├── utils.rb │ └── version.rb ├── public.pem ├── spec ├── fixtures │ ├── accounts_all.json │ ├── accounts_features.json │ ├── accounts_load.json │ ├── app_lists_all.json │ ├── app_lists_load.json │ ├── audience_estimate.json │ ├── campaigns_all.json │ ├── campaigns_load.json │ ├── cards_all.json │ ├── cards_load.json │ ├── custom_audiences_all.json │ ├── custom_audiences_load.json │ ├── funding_instruments_all.json │ ├── funding_instruments_load.json │ ├── line_items_all.json │ ├── line_items_load.json │ ├── no_content.json │ ├── placements.json │ ├── promotable_users_all.json │ ├── promotable_users_load.json │ ├── promoted_tweets_all.json │ ├── promoted_tweets_load.json │ ├── reach_estimate.json │ ├── tailored_audiences_all.json │ ├── targeted_audiences.json │ ├── tracking_tags_load.json │ ├── tweet_previews.json │ ├── videos_all.json │ └── videos_load.json ├── quality_spec.rb ├── shared │ └── properties.rb ├── spec_helper.rb ├── support │ └── helpers.rb ├── tmp │ └── .keep └── twitter-ads │ ├── account_spec.rb │ ├── audiences │ └── custom_audience_spec.rb │ ├── campaign │ ├── app_list_spec.rb │ ├── line_item_spec.rb │ ├── targeting_criteria_spec.rb │ ├── tracking_tag_spec.rb │ └── tweet_spec.rb │ ├── client_spec.rb │ ├── creative │ ├── cards_spec.rb │ ├── media_creative_spec.rb │ ├── promoted_account_spec.rb │ ├── promoted_tweet_spec.rb │ └── tweet_previews_spec.rb │ ├── cursor_spec.rb │ ├── placements_spec.rb │ ├── rate_limit_spec.rb │ ├── retry_count_spec.rb │ ├── targeting │ └── audience_estimate_spec.rb │ └── utils_spec.rb └── twitter-ads.gemspec /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Issue Type:** Bug, Question, Improvement or Feature (Select One) 2 | 3 | **Description:** 4 | 5 | A brief overview of the issue. Please make sure to include detailed information and a clearly written assessment of the impact or priority of the issue. 6 | 7 | **Expected Result:** 8 | ```ruby 9 | # expected output here 10 | ``` 11 | 12 | **Actual Result:** 13 | ```ruby 14 | # actual output here 15 | ``` 16 | 17 | **Steps to Reproduce:** 18 | 19 | - 20 | - 21 | - 22 | 23 | 24 | **Relates To:** #123 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Issue Type:** Bug, Improvement or Feature (Select One) 2 | 3 | **Fixes / Relates To:** #123 4 | 5 | **Changes Included:** 6 | 7 | - 8 | - 9 | - 10 | 11 | **Check List:** 12 | 13 | - [ ] Includes adequate test [coverage](https://github.com/twitterdev/twitter-ruby-ads-sdk/tree/master/spec) for changes made. 14 | - [ ] Includes new or updated [documentation](http://twitterdev.github.io/twitter-ruby-ads-sdk/reference/index.html). 15 | - [ ] Includes new or updated usage [examples](https://github.com/twitterdev/twitter-ruby-ads-sdk/tree/master/examples). 16 | 17 | _For more information on check list items, please see the [Contributors Guide](https://github.com/twitterdev/twitter-ruby-ads-sdk/tree/master/CONTRIBUTING.md)._ 18 | -------------------------------------------------------------------------------- /.github/workflows/gempush.yml: -------------------------------------------------------------------------------- 1 | name: Ruby Gems 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | name: Build + Publish 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Ruby 2.7 15 | uses: actions/setup-ruby@v1 16 | with: 17 | ruby-version: '2.7' 18 | 19 | - name: Publish to RubyGems 20 | run: | 21 | mkdir -p $HOME/.gem 22 | touch $HOME/.gem/credentials 23 | chmod 0600 $HOME/.gem/credentials 24 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 25 | gem build *.gemspec 26 | gem push *.gem 27 | env: 28 | GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}} 29 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ master ] 13 | pull_request: 14 | branches: [ master ] 15 | 16 | jobs: 17 | test: 18 | name: Test on ruby ${{ matrix.ruby_version }} and ${{ matrix.os }} 19 | runs-on: ${{ matrix.os }}-latest 20 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | ruby_version: [3.0.0, 2.7, 2.6.4, 2.5.6, 2.4.7] 25 | os: [ubuntu, macos] 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Set up Ruby 30 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 31 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 32 | # uses: ruby/setup-ruby@v1 33 | uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: ${{ matrix.ruby_version}} 36 | - name: Install dependencies 37 | run: | 38 | bundle config without 'development' 39 | bundle install 40 | - name: Run tests 41 | run: bundle exec rake 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | /.config 3 | /coverage/ 4 | /pkg/ 5 | /spec/reports/ 6 | /test/tmp/ 7 | /test/version_tmp/ 8 | /tmp/ 9 | /features/shared 10 | .DS_Store 11 | 12 | # Documentation 13 | /.yardoc/ 14 | /_yardoc/ 15 | /doc/ 16 | /rdoc/ 17 | 18 | # Environment 19 | /.bundle/ 20 | /vendor/bundle 21 | /lib/bundler/man/ 22 | 23 | Gemfile.lock 24 | .ruby-version 25 | .ruby-gemset 26 | 27 | private.pem 28 | 29 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Style & Quality Overrides 2 | AllCops: 3 | TargetRubyVersion: 2.4 4 | Exclude: 5 | - 'bin/**/*' 6 | - 'vendor/bundle/**/*' 7 | - '**/*/.irbrc' 8 | 9 | Lint/AssignmentInCondition: 10 | Enabled: false 11 | 12 | Lint/Void: 13 | Enabled: false 14 | 15 | # Disable for now 16 | Lint/UriEscapeUnescape: 17 | Enabled: false 18 | 19 | Metrics/LineLength: 20 | Max: 100 21 | 22 | Metrics/MethodLength: 23 | Enabled: false 24 | 25 | Metrics/ClassLength: 26 | Enabled: false 27 | 28 | Metrics/AbcSize: 29 | Enabled: false 30 | 31 | Metrics/CyclomaticComplexity: 32 | Max: 15 33 | 34 | Metrics/PerceivedComplexity: 35 | Max: 20 36 | 37 | Metrics/ParameterLists: 38 | Max: 10 39 | CountKeywordArgs: true 40 | 41 | Performance/Casecmp: 42 | Enabled: false 43 | 44 | Style/LineEndConcatenation: 45 | Enabled: false 46 | # Disable for now 47 | Performance/HashEachMethods: 48 | Enabled: false 49 | 50 | Style/MixinGrouping: 51 | Enabled: false 52 | 53 | Layout/MultilineMethodCallBraceLayout: 54 | Enabled: false 55 | 56 | Style/FrozenStringLiteralComment: 57 | EnforcedStyle: always 58 | 59 | Style/StringLiterals: 60 | Enabled: true 61 | EnforcedStyle: single_quotes 62 | SupportedStyles: 63 | - single_quotes 64 | - double_quotes 65 | 66 | Style/NumericLiterals: 67 | Enabled: false 68 | 69 | Style/FormatStringToken: 70 | Enabled: false 71 | 72 | Style/Copyright: 73 | Enabled: true 74 | Notice: 'Copyright (\(C\) )?2019 Twitter, Inc.' 75 | AutocorrectNotice: "# Copyright (C) 2019 Twitter, Inc.\n" 76 | Exclude: 77 | - Gemfile 78 | 79 | Style/NegatedIf: 80 | Enabled: false 81 | 82 | Style/RaiseArgs: 83 | Enabled: false 84 | 85 | Style/GuardClause: 86 | Enabled: false 87 | 88 | Style/BlockDelimiters: 89 | Enabled: false 90 | 91 | Style/DoubleNegation: 92 | Enabled: false 93 | 94 | Style/RegexpLiteral: 95 | Enabled: false 96 | 97 | Style/Documentation: 98 | Enabled: false 99 | 100 | Style/SignalException: 101 | Enabled: false 102 | 103 | Layout/EmptyLinesAroundModuleBody: 104 | Enabled: false 105 | 106 | Layout/EmptyLinesAroundClassBody: 107 | Enabled: false 108 | 109 | Layout/EmptyLinesAroundBlockBody: 110 | Enabled: false 111 | 112 | Style/FormatString: 113 | Enabled: false 114 | 115 | Style/SafeNavigation: 116 | Enabled: false 117 | 118 | Style/SymbolArray: 119 | Enabled: false 120 | 121 | Style/TernaryParentheses: 122 | Enabled: false 123 | 124 | Style/PercentLiteralDelimiters: 125 | Enabled: false 126 | 127 | Style/BracesAroundHashParameters: 128 | Enabled: false 129 | 130 | Naming/VariableNumber: 131 | Enabled: false 132 | 133 | Style/MultilineIfModifier: 134 | Enabled: false 135 | 136 | Style/IdenticalConditionalBranches: 137 | Enabled: false 138 | 139 | Layout/EmptyLineAfterMagicComment: 140 | Enabled: false 141 | 142 | Metrics/BlockLength: 143 | Enabled: false 144 | 145 | Performance/RegexpMatch: 146 | Enabled: false 147 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: ruby 3 | 4 | before_install: 5 | - bundle config without 'development' 6 | 7 | install: bundle install 8 | script: bundle exec rake 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | matrix: 15 | fast_finish: true 16 | allow_failures: 17 | - rvm: ruby-head 18 | - rvm: rbx-2 19 | include: 20 | - rvm: 2.6.4 21 | - rvm: 2.5.6 22 | - rvm: 2.4.7 23 | - rvm: ruby-head 24 | - rvm: rbx-2 25 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --quiet 2 | --no-private 3 | 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | We love pull requests from everyone! 4 | 5 | We encourage community contributions for all kinds of changes 6 | both big and small, but we ask that you adhere to the following guidelines for contributing code. 7 | 8 | ##### Proposing Changes 9 | 10 | As a starting point for all changes, we recommend [reporting an issue][report-issue] 11 | before you begin making any changes. Make sure to search the issues on this repository 12 | first to check and see the issue has already been previously discussed and whether or 13 | not it's already being worked on. 14 | 15 | - For small changes, improvements and bug fixes please feel free to send us a pull 16 | request with proposed changes along-side the issue you report. 17 | 18 | - For larger more involved or design related changes, please open an issue and discuss 19 | the changes with the other contributors before submitting any pull requests. 20 | 21 | [report-issue]: https://github.com/twitterdev/twitter-ruby-ads-sdk/issues?q=is%3Aopen+is%3Aissue 22 | 23 | ##### Submitting A Pull Request 24 | 25 | 1) Fork us and clone the repository locally. 26 | 27 | ```bash 28 | git clone git@github.com:twitterdev/twitter-ruby-ads-sdk.git 29 | ``` 30 | 31 | 2) Install development dependencies: 32 | 33 | ```bash 34 | # install bundler (if you don't have it already) 35 | gem install bundler 36 | 37 | # install all development dependencies 38 | bundle install 39 | ``` 40 | 41 | 3) Make sure all tests pass before you start: 42 | 43 | ```bash 44 | rake 45 | ``` 46 | 47 | 4) Make your changes! (Don't forget tests and documentation) 48 | 49 | 5) Test your changes again and make sure everything passes: 50 | 51 | ```bash 52 | rake 53 | ``` 54 | 55 | The test suite will automatically enforce test coverage and code style. 56 | For the most part, we follow [this][style] community style guide with a 57 | [few exceptions](https://github.com/twitterdev/twitter-ruby-ads-sdk/blob/master/.rubocop.yml). 58 | 59 | [style]: https://github.com/bbatsov/ruby-style-guide 60 | 61 | 6) Submit your changes! 62 | 63 | - [Squash](http://eli.thegreenplace.net/2014/02/19/squashing-github-pull-requests-into-a-single-commit) your development commits. Put features in a single clean commit whenever possible or logically split it into a few commits (no development commits). Test coverage can be included in a separate commit if preferred. 64 | - Write a [good commit message][commit] for your change. 65 | - Push to your fork. 66 | - Submit a [pull request][pr]. 67 | 68 | [commit]: http://chris.beams.io/posts/git-commit/ 69 | [pr]: https://github.com/thoughtbot/suspenders/compare/ 70 | 71 | We try to at least comment on pull requests within one business day and may suggest changes. 72 | 73 | ##### Release Schedule and Versioning 74 | 75 | We have a regular release cadence and adhere to [semantic versioning](http://semver.org/). When 76 | exactly your change ships will depend on the scope of your changes and what type of upcoming release 77 | its best suited for. 78 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source 'https://rubygems.org' 3 | 4 | gemspec 5 | gem 'rake' 6 | 7 | group :development, :test do 8 | gem 'cucumber' 9 | gem 'faker', '~> 1.0' # fix for breaking change in faker 2.x 10 | gem 'pry-nav', require: true 11 | gem 'pry-rescue', require: true 12 | gem 'rexml' 13 | gem 'rspec' 14 | gem 'rubocop', '~> 0.50.0' 15 | gem 'simplecov', '~> 0.13' # fix for breaking change in simplecov 16 | gem 'webmock' 17 | end 18 | 19 | group :development do 20 | gem 'guard-bundler', platforms: :mri 21 | gem 'guard-rspec', platforms: :mri 22 | 23 | gem 'rb-fchange', require: false # Windows 24 | gem 'rb-fsevent', require: false # OS X 25 | gem 'rb-inotify', require: false # Linux 26 | gem 'terminal-notifier-guard' 27 | 28 | gem 'ruby-prof', platforms: :mri 29 | end 30 | 31 | group :release do 32 | gem 'colorize' 33 | gem 'git' 34 | gem 'redcarpet', platforms: :mri 35 | gem 'yard' 36 | end 37 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | guard :bundler do 5 | watch('Gemfile') 6 | watch(/^.+\.gemspec/) 7 | end 8 | 9 | guard :rspec, cmd: 'bundle exec rspec' do 10 | watch(%r{^spec/.+_spec\.rb$}) 11 | watch(%r{^lib/(.+)\.rb$}) { |match| "spec/#{match[1]}_spec.rb" } 12 | watch('spec/spec_helper.rb') { 'spec' } 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2020 Twitter, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Getting Started [![Build Status](https://travis-ci.org/twitterdev/twitter-ruby-ads-sdk.svg?branch=master)](https://travis-ci.org/twitterdev/twitter-ruby-ads-sdk)[![Gem Version](https://badge.fury.io/rb/twitter-ads.svg)](http://badge.fury.io/rb/twitter-ads) 2 | ------ 3 | 4 | ##### Installation 5 | ```bash 6 | # installing the latest signed release 7 | gem install twitter-ads 8 | ``` 9 | 10 | ##### Quick Start 11 | ```ruby 12 | require 'twitter-ads' 13 | 14 | # initialize the client 15 | client = TwitterAds::Client.new( 16 | CONSUMER_KEY, 17 | CONSUMER_SECRET, 18 | ACCESS_TOKEN, 19 | ACCESS_TOKEN_SECRET 20 | ) 21 | 22 | # load the advertiser account instance 23 | account = client.accounts('c3won9gy') 24 | 25 | # load and update a specific campaign 26 | campaign = account.campaigns('4m0gjms') 27 | campaign.entity_status = EntityStatus::PAUSED 28 | campaign.save 29 | 30 | # iterate through campaigns 31 | account.campaigns.each do |camp| 32 | # do stuff 33 | end 34 | ``` 35 | 36 | ##### Command Line Helper 37 | ```bash 38 | # The twitter-ads command launches an interactive session for testing purposes 39 | # with a client instance automatically loaded from your .twurlrc file. 40 | 41 | ~ ❯ twitter-ads 42 | twitter-ads vX.X.X >> CLIENT 43 | => # 44 | twitter-ads vX.X.X >> CLIENT.accounts.first 45 | => # 46 | ``` 47 | For more help please see our [Examples and Guides](https://github.com/twitterdev/twitter-ruby-ads-sdk/tree/master/examples) or check the online [Reference Documentation](http://twitterdev.github.io/twitter-ruby-ads-sdk/reference/index.html). 48 | 49 | ## Compatibility & Versioning 50 | 51 | This project is designed to work with Ruby 2.4.0 or greater. While it may work on other version of Ruby, below are the platform and runtime versions we officially support and regularly test against. 52 | 53 | Platform | Versions 54 | -------- | -------- 55 | MRI | 2.4.x, 2.5.x, 2.6.x, 2.7, 3.0 56 | Rubinius | 2.4.x, 2.5.x 57 | 58 | All releases adhere to strict [semantic versioning](http://semver.org). For Example, major.minor.patch-pre (aka. stick.carrot.oops-peek). 59 | 60 | ## Development 61 | If you'd like to contribute to the project or try an unreleased development version of this project locally, you can do so quite easily by following the examples below. 62 | ```bash 63 | # clone the repository 64 | git clone git@github.com:twitterdev/twitter-ruby-ads-sdk.git 65 | cd twitter-ruby-ads-sdk 66 | 67 | # install dependencies 68 | bundle install 69 | 70 | rake docs # building documentation 71 | rake spec # run all tests 72 | 73 | # installing a local unsigned release 74 | gem build twitter-ads.gemspec & gem install twitter-ads-X.X.X.gem 75 | ``` 76 | We love community contributions! If you're planning to send us a pull request, please make sure read our [Contributing Guidelines](https://github.com/twitterdev/twitter-ruby-ads-sdk/blob/master/CONTRIBUTING.md) first. 77 | 78 | ## Feedback and Bug Reports 79 | Found an issue? Please open up a [GitHub issue](https://github.com/twitterdev/twitter-ruby-ads-sdk/issues) or even better yet [send us](https://github.com/twitterdev/twitter-ruby-ads-sdk/blob/master/CONTRIBUTING.md) a pull request.
80 | Have a question? Want to discuss a new feature? Come chat with us in the [Twitter Community Forums](https://twittercommunity.com/c/advertiser-api). 81 | 82 | ## Error Handling 83 | 84 | Like the [Response](https://github.com/twitterdev/twitter-ruby-ads-sdk/blob/master/lib/twitter-ads/http/response.rb) and [Request](https://github.com/twitterdev/twitter-ruby-ads-sdk/blob/master/lib/twitter-ads/http/request.rb) classes, the Ads API SDK fully models all [error objects](https://github.com/twitterdev/twitter-ruby-ads-sdk/blob/master/lib/twitter-ads/error.rb) for easy error handling. 85 | 86 | Error Hierarchy 87 | 88 | ## License 89 | 90 | The MIT License (MIT) 91 | 92 | Copyright (C) 2021 Twitter, Inc. 93 | 94 | Permission is hereby granted, free of charge, to any person obtaining a copy 95 | of this software and associated documentation files (the "Software"), to deal 96 | in the Software without restriction, including without limitation the rights 97 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 98 | copies of the Software, and to permit persons to whom the Software is 99 | furnished to do so, subject to the following conditions: 100 | 101 | The above copyright notice and this permission notice shall be included in all 102 | copies or substantial portions of the Software. 103 | 104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 105 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 106 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 107 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 108 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 109 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 110 | SOFTWARE. 111 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Copyright (C) 2019 Twitter, Inc. 5 | 6 | require 'rubygems' 7 | 8 | begin 9 | require 'bundler' 10 | require 'bundler/gem_tasks' 11 | rescue LoadError 12 | raise '[FAIL] Bundler not found! Install it with ' \ 13 | '`gem install bundler; bundle install`.' 14 | end 15 | 16 | # Helper for making a bit of log noise. 17 | def announce(type, message, &block) 18 | print "[#{type.to_s.upcase}] #{message}... " 19 | begin 20 | yield block 21 | print "[DONE]\n".colorize(color: :green).bold 22 | rescue StandardError 23 | print "[FAILED]\n".colorize(color: :red).bold 24 | abort 25 | end 26 | end 27 | 28 | # Default Rake Task 29 | if ENV['CI'] 30 | task default: ['spec'] # 'features' 31 | Bundler.require(:default, :test) 32 | else 33 | task default: ['spec'] # 'features' 34 | Bundler.require(:default, :test, :development, :release) 35 | end 36 | 37 | # Cucumber Setup 38 | require 'cucumber' 39 | require 'cucumber/rake/task' 40 | Cucumber::Rake::Task.new(:features) do |t| 41 | t.cucumber_opts = 'features --format progress --strict' 42 | end 43 | 44 | # RSpec Setup 45 | require 'rspec/core/rake_task' 46 | RSpec::Core::RakeTask.new(:spec) 47 | 48 | unless ENV['CI'] 49 | # Release and Deployment 50 | desc 'Builds the latest SDK docs locally' 51 | task :docs do 52 | FileUtils.rm_rf('doc') 53 | YARD::CLI::CommandParser.run('--quiet') 54 | end 55 | 56 | namespace :docs do 57 | git = Git.open(File.expand_path('../', __FILE__)) 58 | 59 | desc 'Builds and deploys the latest SDK docs' 60 | task :deploy do 61 | unless git.status.changed.empty? 62 | puts 'Error! Cannot proceeed, you have pending changes.'.colorize(color: :red).bold 63 | abort 64 | end 65 | FileUtils.rm_rf('doc') 66 | YARD::CLI::CommandParser.run 67 | 68 | current_branch = git.branch.name 69 | announce(:build, "updating v#{TwitterAds::VERSION} documentation") do 70 | git.branch('gh-pages').checkout 71 | FileUtils.rm_rf('reference') 72 | FileUtils.mv('doc', 'reference') 73 | git.add('reference') 74 | git.commit("[docs] updating v#{TwitterAds::VERSION} documentation") 75 | end 76 | 77 | announce(:push, "publishing v#{TwitterAds::VERSION} documentation") do 78 | git.push('origin', 'gh-pages') 79 | end 80 | 81 | git.branch(current_branch).checkout 82 | end 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /bin/twitter-ads: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | 6 | begin 7 | require 'twitter' 8 | rescue LoadError 9 | end 10 | 11 | $LOAD_PATH.push File.join(File.dirname(__FILE__), '..', 'lib') 12 | require 'twitter-ads' 13 | 14 | include TwitterAds 15 | include TwitterAds::Enum 16 | 17 | # load up a ready-to-go client instance if .twurlrc is setup 18 | if File.exists?(File.expand_path('~/.twurlrc')) 19 | require 'yaml' 20 | twurl_config = YAML.load(File.read(File.expand_path('~/.twurlrc'))) 21 | profile_name = twurl_config['configuration']['default_profile'][0] 22 | profile_key = twurl_config['configuration']['default_profile'][1] 23 | default_profile = twurl_config['profiles'][profile_name][profile_key] 24 | CLIENT = TwitterAds::Client.new( 25 | default_profile['consumer_key'], 26 | default_profile['consumer_secret'], 27 | default_profile['token'], 28 | default_profile['secret'], 29 | ) 30 | end 31 | 32 | begin 33 | require 'pry' 34 | KLASS = Pry 35 | version_info = "twitter-ads v#{TwitterAds::VERSION}" 36 | KLASS.config.prompt = [ 37 | proc { "#{version_info} >> " }, 38 | proc { "#{version_info} ... " } 39 | ] 40 | rescue LoadError 41 | require 'irb' 42 | KLASS = IRB 43 | end 44 | 45 | KLASS.start(__FILE__) 46 | -------------------------------------------------------------------------------- /error-hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/twitter-ruby-ads-sdk/0b6759a21236617ba5c4f39178e7a73fc1a4ae4e/error-hierarchy.png -------------------------------------------------------------------------------- /examples/active_entities.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | # note: the following is just a simple example. before making any stats calls, make 5 | # sure to read our best practices for analytics which can be found here: 6 | # 7 | # https://dev.twitter.com/ads/analytics/best-practices 8 | # https://dev.twitter.com/ads/analytics/metrics-and-segmentation 9 | # https://dev.twitter.com/ads/analytics/metrics-derived 10 | 11 | require 'time' 12 | require 'twitter-ads' 13 | 14 | include TwitterAds::Enum 15 | 16 | CONSUMER_KEY = 'your consumer key' 17 | CONSUMER_SECRET = 'your consumer secret' 18 | ACCESS_TOKEN = 'user access token' 19 | ACCESS_TOKEN_SECRET = 'user access token secret' 20 | ADS_ACCOUNT = 'ads account id' 21 | 22 | # initialize the twitter ads api client 23 | client = TwitterAds::Client.new( 24 | CONSUMER_KEY, 25 | CONSUMER_SECRET, 26 | ACCESS_TOKEN, 27 | ACCESS_TOKEN_SECRET 28 | ) 29 | 30 | # load up the account instance 31 | account = client.accounts(ADS_ACCOUNT) 32 | 33 | # for checking the active entities endpoint for the last day 34 | time = Time.now 35 | utc_offset = '+09:00' 36 | start_time = time - (60 * 60 * 24) # -1 day 37 | end_time = time 38 | 39 | # active entities for line items 40 | active_entities = TwitterAds::LineItem.active_entities( 41 | account, 42 | line_item_ids: %w(exrfs), 43 | start_time: start_time, 44 | end_time: end_time, 45 | utc_offset: utc_offset, 46 | granularity: :day) 47 | 48 | puts(active_entities) 49 | 50 | # entity IDs to fetch analytics data for 51 | # note: analytics endpoints support a 52 | # maximum of 20 entity IDs per request 53 | ids = active_entities.map { |entity| entity[:entity_id] } 54 | 55 | # function for determining the start and end time 56 | # to be used in the subsequent analytics request 57 | # note: if `active_entities` is empty, `date_range` will error 58 | def date_range(data) 59 | # Returns the minimum activity start time and the maximum activity end time 60 | # from the active entities response. These dates are modified in the following 61 | # way. The hours (and minutes and so on) are removed from the start and end 62 | # times and a *day* is added to the end time. These are the dates that should 63 | # be used in the subsequent analytics request. 64 | 65 | start_time = data.map { |entity| Time.parse(entity[:activity_start_time]) }.min 66 | end_time = data.map { |entity| Time.parse(entity[:activity_end_time]) }.max 67 | 68 | start_time = Time.new( 69 | start_time.year, 70 | start_time.month, 71 | start_time.day, 72 | start_time.hour, 73 | 0, 74 | 0, 75 | '+00:00') 76 | 77 | end_time = Time.new( 78 | end_time.year, 79 | end_time.month, 80 | end_time.day, 81 | end_time.hour, 82 | 0, 83 | 0, 84 | '+00:00') + (60 * 60 * 24) # +1 day 85 | 86 | [start_time, end_time] 87 | end 88 | 89 | start_time, end_time = date_range(active_entities) 90 | 91 | # analytics request parameters 92 | metric_groups = %W(#{MetricGroup::ENGAGEMENT}) 93 | granularity = Granularity::HOUR 94 | placement = Placement::ALL_ON_TWITTER 95 | 96 | # analytics request (synchronous) 97 | data = TwitterAds::LineItem.stats( 98 | account, 99 | ids, 100 | metric_groups, 101 | granularity: granularity, 102 | placement: placement, 103 | start_time: start_time, 104 | end_time: end_time) 105 | 106 | puts(data) 107 | -------------------------------------------------------------------------------- /examples/analytics.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | # note: the following is just a simple example. before making any stats calls, make 5 | # sure to read our best practices for analytics which can be found here: 6 | # 7 | # https://dev.twitter.com/ads/analytics/best-practices 8 | # https://dev.twitter.com/ads/analytics/metrics-and-segmentation 9 | # https://dev.twitter.com/ads/analytics/metrics-derived 10 | 11 | require 'twitter-ads' 12 | 13 | CONSUMER_KEY = 'your consumer key' 14 | CONSUMER_SECRET = 'your consumer secret' 15 | ACCESS_TOKEN = 'user access token' 16 | ACCESS_TOKEN_SECRET = 'user access token secret' 17 | ADS_ACCOUNT = 'ads account id' 18 | 19 | # initialize the twitter ads api client 20 | client = TwitterAds::Client.new( 21 | CONSUMER_KEY, 22 | CONSUMER_SECRET, 23 | ACCESS_TOKEN, 24 | ACCESS_TOKEN_SECRET 25 | ) 26 | 27 | # load up the account instance 28 | account = client.accounts(ADS_ACCOUNT) 29 | 30 | # limit request count and grab the first 10 line items from TwitterAds::Cursor 31 | line_items = account.line_items(nil, count: 10)[0..9] 32 | 33 | # the list of metric groups we want to fetch, for a full list of possible metrics 34 | # see: https://dev.twitter.com/ads/analytics/metrics-and-segmentation 35 | metrics = %w(ENGAGEMENT VIDEO) 36 | 37 | # fetching stats on the instance 38 | line_items.first.stats(metrics) 39 | 40 | # fetching stats for multiple line items 41 | ids = line_items.map(&:id) 42 | TwitterAds::LineItem.stats(account, ids, metrics) 43 | -------------------------------------------------------------------------------- /examples/audience_estimate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | require 'twitter-ads' 4 | 5 | CONSUMER_KEY = 'your consumer key' 6 | CONSUMER_SECRET = 'your consumer secret' 7 | ACCESS_TOKEN = 'user access token' 8 | ACCESS_TOKEN_SECRET = 'user access token secret' 9 | ADS_ACCOUNT = 'ads account id' 10 | 11 | # initialize the twitter ads api client 12 | client = TwitterAds::Client.new( 13 | CONSUMER_KEY, 14 | CONSUMER_SECRET, 15 | ACCESS_TOKEN, 16 | ACCESS_TOKEN_SECRET 17 | ) 18 | 19 | # load up the account instance 20 | account = client.accounts(ADS_ACCOUNT) 21 | 22 | # targeting criteria params 23 | params = { 24 | targeting_criteria: [ 25 | { 26 | targeting_type: 'LOCATION', 27 | targeting_value: '96683cc9126741d1' 28 | }, 29 | { 30 | targeting_type: 'BROAD_KEYWORD', 31 | targeting_value: 'cats' 32 | }, 33 | { 34 | targeting_type: 'SIMILAR_TO_FOLLOWERS_OF_USER', 35 | targeting_value: '14230524' 36 | }, 37 | { 38 | targeting_type: 'SIMILAR_TO_FOLLOWERS_OF_USER', 39 | targeting_value: '90420314' 40 | } 41 | ] 42 | } 43 | 44 | audience_estimate = TwitterAds::AudienceEstimate.fetch(account, params) 45 | puts audience_estimate 46 | -------------------------------------------------------------------------------- /examples/audience_permissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | require 'digest' 5 | require 'twitter-ads' 6 | include TwitterAds::Enum 7 | 8 | CONSUMER_KEY = 'your consumer key' 9 | CONSUMER_SECRET = 'your consumer secret' 10 | ACCESS_TOKEN = 'user access token' 11 | ACCESS_TOKEN_SECRET = 'user access token secret' 12 | ADS_ACCOUNT = 'ads account id' 13 | 14 | # initialize the twitter ads api client 15 | client = TwitterAds::Client.new( 16 | CONSUMER_KEY, 17 | CONSUMER_SECRET, 18 | ACCESS_TOKEN, 19 | ACCESS_TOKEN_SECRET 20 | ) 21 | 22 | # load up the account instance 23 | account = client.accounts(ADS_ACCOUNT) 24 | 25 | custom_audience_id = '36n4f' 26 | 27 | # fetch all permissions 28 | permissions = TwitterAds::CustomAudiencePermission.all(account, custom_audience_id) 29 | 30 | permissions.each { |data| 31 | p data.id 32 | p data.permission_level 33 | p data.granted_account_id 34 | } 35 | 36 | # create instance 37 | permission = TwitterAds::CustomAudiencePermission.new(account) 38 | 39 | # set required params 40 | permission.custom_audience_id = custom_audience_id 41 | permission.granted_account_id = '18ce54uvbwu' 42 | permission.permission_level = PermissionLevel::READ_ONLY 43 | 44 | # set permission 45 | response = permission.save 46 | 47 | # delete permission 48 | permission.id = response.id 49 | permission.delete! 50 | -------------------------------------------------------------------------------- /examples/batch_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | require 'twitter-ads' 5 | include TwitterAds::Enum 6 | 7 | CONSUMER_KEY = 'your consumer key' 8 | CONSUMER_SECRET = 'your consumer secret' 9 | ACCESS_TOKEN = 'user access token' 10 | ACCESS_TOKEN_SECRET = 'user access token secret' 11 | ACCOUNT_ID = 'ads account id' 12 | 13 | # initialize the client 14 | client = TwitterAds::Client.new(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET) 15 | 16 | # load the advertiser account instance 17 | account = client.accounts(ACCOUNT_ID) 18 | 19 | # create two campaigns 20 | campaign_1 = TwitterAds::Campaign.new(account) 21 | campaign_1.funding_instrument_id = account.funding_instruments.first.id 22 | campaign_1.daily_budget_amount_local_micro = 1_000_000 23 | campaign_1.name = 'my first campaign' 24 | campaign_1.entity_status = EntityStatus::PAUSED 25 | campaign_1.start_time = Time.now.utc 26 | 27 | campaign_2 = TwitterAds::Campaign.new(account) 28 | campaign_2.funding_instrument_id = account.funding_instruments.first.id 29 | campaign_2.daily_budget_amount_local_micro = 2_000_000 30 | campaign_2.name = 'my second campaign' 31 | campaign_2.entity_status = EntityStatus::PAUSED 32 | campaign_2.start_time = Time.now.utc 33 | 34 | campaigns_list = [campaign_1, campaign_2] 35 | TwitterAds::Campaign.batch_save(account, campaigns_list) 36 | 37 | # modify the created campaigns 38 | campaign_1.name = 'my modified first campaign' 39 | campaign_2.name = 'my modified second campaign' 40 | 41 | TwitterAds::Campaign.batch_save(account, campaigns_list) 42 | 43 | # create line items for campaign_1 44 | line_item_1 = TwitterAds::LineItem.new(account) 45 | line_item_1.campaign_id = campaign_1.id 46 | line_item_1.name = 'my first ad' 47 | line_item_1.product_type = TwitterAds::Product::PROMOTED_TWEETS 48 | line_item_1.placements = [TwitterAds::Placement::ALL_ON_TWITTER] 49 | line_item_1.objective = TwitterAds::Objective::ENGAGEMENTS 50 | line_item_1.bid_amount_local_micro = 10_000 51 | line_item_1.entity_status = EntityStatus::PAUSED 52 | 53 | line_item_2 = TwitterAds::LineItem.new(account) 54 | line_item_2.campaign_id = campaign_1.id 55 | line_item_2.name = 'my second ad' 56 | line_item_2.product_type = TwitterAds::Product::PROMOTED_TWEETS 57 | line_item_2.placements = [TwitterAds::Placement::ALL_ON_TWITTER] 58 | line_item_2.objective = TwitterAds::Objective::ENGAGEMENTS 59 | line_item_2.bid_amount_local_micro = 20_000 60 | line_item_2.entity_status = EntityStatus::PAUSED 61 | 62 | line_items_list = [line_item_1, line_item_2] 63 | TwitterAds::LineItem.batch_save(account, line_items_list) 64 | 65 | # create targeting criteria for line_item_1 66 | targeting_criterion_1 = TwitterAds::TargetingCriteria.new(account) 67 | targeting_criterion_1.line_item_id = line_item_1.id 68 | targeting_criterion_1.targeting_type = 'LOCATION' 69 | targeting_criterion_1.targeting_value = '00a8b25e420adc94' 70 | 71 | targeting_criterion_2 = TwitterAds::TargetingCriteria.new(account) 72 | targeting_criterion_2.line_item_id = line_item_1.id 73 | targeting_criterion_2.targeting_type = 'PHRASE_KEYWORD' 74 | targeting_criterion_2.targeting_value = 'righteous dude' 75 | 76 | targeting_criteria_list = [targeting_criterion_1, targeting_criterion_2] 77 | TwitterAds::TargetingCriteria.batch_save(account, targeting_criteria_list) 78 | 79 | targeting_criterion_1.to_delete = true 80 | targeting_criterion_2.to_delete = true 81 | 82 | TwitterAds::TargetingCriteria.batch_save(account, targeting_criteria_list) 83 | 84 | line_item_1.to_delete = true 85 | line_item_2.to_delete = true 86 | 87 | TwitterAds::LineItem.batch_save(account, line_items_list) 88 | 89 | campaign_1.to_delete = true 90 | campaign_2.to_delete = true 91 | 92 | TwitterAds::Campaign.batch_save(account, campaigns_list) 93 | -------------------------------------------------------------------------------- /examples/cards.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | require 'twitter-ads' 4 | 5 | CONSUMER_KEY = 'your consumer key' 6 | CONSUMER_SECRET = 'your consumer secret' 7 | ACCESS_TOKEN = 'user access token' 8 | ACCESS_TOKEN_SECRET = 'user access token secret' 9 | ADS_ACCOUNT = 'ads account id' 10 | 11 | # initialize the twitter ads api client 12 | client = TwitterAds::Client.new( 13 | CONSUMER_KEY, 14 | CONSUMER_SECRET, 15 | ACCESS_TOKEN, 16 | ACCESS_TOKEN_SECRET 17 | ) 18 | 19 | # load up the account instance, campaign and line item 20 | account = client.accounts(ADS_ACCOUNT) 21 | 22 | # create the card 23 | name = 'video website card' 24 | components = [ 25 | { 26 | type: 'MEDIA', 27 | media_key: '13_794652834998325248' 28 | }, 29 | { 30 | type: 'DETAILS', 31 | title: 'Twitter', 32 | destination: { 33 | type: 'WEBSITE', 34 | url: 'http://twitter.com/' 35 | } 36 | } 37 | ] 38 | 39 | vwc = TwitterAds::Creative::Cards.new(account) 40 | vwc.name = name 41 | vwc.components = components 42 | vwc.save 43 | vwc.name = 'vwc - ruby sdk' 44 | vwc.save 45 | puts vwc.name # vwc - ruby sdk 46 | 47 | # fetch all 48 | # card = TwitterAds::Creative::Cards.all(account, card_ids: '1508693734346485761').first 49 | 50 | # fetch by card-id 51 | # card = TwitterAds::Creative::Cards.load(account, '1508693734346485761') 52 | 53 | # get user_id for as_user_id parameter 54 | user_id = TwitterRestApi::UserIdLookup.load(account, screen_name: 'your_screen_name').id 55 | 56 | # create a draft tweet using this new card 57 | tweet = TwitterAds::Creative::DraftTweet.new(account) 58 | tweet.text = 'Created from SDK' 59 | tweet.as_user_id = user_id 60 | tweet.card_uri = vwc.card_uri 61 | tweet.save 62 | -------------------------------------------------------------------------------- /examples/cards_fetch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | require 'twitter-ads' 4 | 5 | CONSUMER_KEY = 'your consumer key' 6 | CONSUMER_SECRET = 'your consumer secret' 7 | ACCESS_TOKEN = 'user access token' 8 | ACCESS_TOKEN_SECRET = 'user access token secret' 9 | ADS_ACCOUNT = 'ads account id' 10 | 11 | # initialize the twitter ads api client 12 | client = TwitterAds::Client.new( 13 | CONSUMER_KEY, 14 | CONSUMER_SECRET, 15 | ACCESS_TOKEN, 16 | ACCESS_TOKEN_SECRET 17 | ) 18 | 19 | # load up the account instance, campaign and line item 20 | account = client.accounts(ADS_ACCOUNT) 21 | 22 | # retrieve a tweet 23 | tweet_id = '859263438396153856' # use one of your own tweets with card_uri in response 24 | resource = "/1.1/statuses/show/#{tweet_id}.json" 25 | domain = 'https://api.twitter.com' 26 | params = { include_card_uri: 'true' } 27 | response = TwitterAds::Request.new(client, :get, resource, domain: domain, params: params).perform 28 | uri = response.body[:card_uri] # 54ytn 29 | 30 | # fetch by card_uri 31 | cf = TwitterAds::Creative::CardsFetch.new(account) 32 | card = cf.load(account, uri).first 33 | puts card.card_type # IMAGE_APP_DOWNLOAD 34 | puts card.id # '54ytn' 35 | 36 | # fetch by card_id 37 | cf = TwitterAds::Creative::CardsFetch.new(account) 38 | same_card = cf.load(account, nil, card.id) 39 | puts same_card.card_type # IMAGE_APP_DOWNLOAD 40 | puts same_card.card_uri # 'card://942628629552406533' 41 | -------------------------------------------------------------------------------- /examples/custom_audience.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | require 'digest' 5 | require 'twitter-ads' 6 | include TwitterAds::Enum 7 | 8 | CONSUMER_KEY = 'your consumer key' 9 | CONSUMER_SECRET = 'your consumer secret' 10 | ACCESS_TOKEN = 'user access token' 11 | ACCESS_TOKEN_SECRET = 'user access token secret' 12 | ADS_ACCOUNT = 'ads account id' 13 | 14 | # initialize the twitter ads api client 15 | client = TwitterAds::Client.new( 16 | CONSUMER_KEY, 17 | CONSUMER_SECRET, 18 | ACCESS_TOKEN, 19 | ACCESS_TOKEN_SECRET 20 | ) 21 | 22 | # load up the account instance 23 | account = client.accounts(ADS_ACCOUNT) 24 | 25 | # create a new placeholder custom audience 26 | audience = 27 | TwitterAds::CustomAudience.create(account, 'Test TA') 28 | 29 | # sample user 30 | # all values musth be sha256 hashede except 'partner_user_id' 31 | email_hash = Digest::SHA256.hexdigest 'test-email@test.com' 32 | 33 | # create payload 34 | user = [{ 35 | operation_type: 'Update', 36 | params: { 37 | users: [{ 38 | email: [ 39 | email_hash 40 | ] 41 | }] 42 | } 43 | }] 44 | 45 | # update the custom audience 46 | success_count, total_count = audience.users(user) 47 | print "Successfully added #{total_count} users" if success_count == total_count 48 | -------------------------------------------------------------------------------- /examples/draft_tweet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | require 'twitter-ads' 4 | include TwitterAds::Enum 5 | 6 | CONSUMER_KEY = 'your consumer key' 7 | CONSUMER_SECRET = 'your consumer secret' 8 | ACCESS_TOKEN = 'user access token' 9 | ACCESS_TOKEN_SECRET = 'user access token secret' 10 | ADS_ACCOUNT = 'ads account id' 11 | 12 | # initialize the twitter ads api client 13 | client = TwitterAds::Client.new( 14 | CONSUMER_KEY, 15 | CONSUMER_SECRET, 16 | ACCESS_TOKEN, 17 | ACCESS_TOKEN_SECRET 18 | ) 19 | 20 | # load up the account instance, campaign and line item 21 | account = client.accounts(ADS_ACCOUNT) 22 | 23 | # get user_id for as_user_id parameter 24 | user_id = TwitterRestApi::UserIdLookup.load(account, screen_name: 'your_twitter_handle_name').id 25 | 26 | # fetch draft tweets from a given account 27 | tweets = TwitterAds::Creative::DraftTweet.all(account) 28 | tweets.each { |tweet| 29 | p tweet.id 30 | p tweet.text 31 | } 32 | 33 | # create a new draft tweet 34 | draft_tweet = TwitterAds::Creative::DraftTweet.new(account) 35 | draft_tweet.text = 'draft tweet - new' 36 | draft_tweet.as_user_id = user_id 37 | draft_tweet.save 38 | p draft_tweet.id 39 | p draft_tweet.text 40 | 41 | # fetch single draft tweet metadata 42 | tweet_id = draft_tweet.id 43 | draft_tweet = TwitterAds::Creative::DraftTweet.load(account, tweet_id) 44 | p draft_tweet.id 45 | p draft_tweet.text 46 | 47 | # update (PUT) metadata 48 | draft_tweet.text = 'draft tweet - update' 49 | draft_tweet = draft_tweet.save 50 | p draft_tweet.id 51 | p draft_tweet.text 52 | 53 | # preview draft tweet of current instance (send notification) 54 | draft_tweet.preview 55 | # or, specify any draft_tweet_id 56 | # draft_tweet.preview(draft_tweet_id: '1142048306194862080') 57 | 58 | # create a nullcasted tweet using draft tweet metadata 59 | tweet = TwitterAds::Tweet.create(account, text: draft_tweet.text, as_user_id: user_id) 60 | p tweet 61 | 62 | # delete draft tweet 63 | # draft_tweet.delete! 64 | -------------------------------------------------------------------------------- /examples/manual_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | require 'twitter-ads' 5 | 6 | CONSUMER_KEY = 'your consumer key' 7 | CONSUMER_SECRET = 'your consumer secret' 8 | ACCESS_TOKEN = 'user access token' 9 | ACCESS_TOKEN_SECRET = 'user access token secret' 10 | ADS_ACCOUNT = 'ads account id' 11 | 12 | # initialize the twitter ads api client 13 | client = TwitterAds::Client.new( 14 | CONSUMER_KEY, 15 | CONSUMER_SECRET, 16 | ACCESS_TOKEN, 17 | ACCESS_TOKEN_SECRET 18 | ) 19 | 20 | # load up the account instance 21 | account = client.accounts(ADS_ACCOUNT) 22 | 23 | # using the TwitterAds::Request object you can manually request any 24 | # twitter ads api resource that you want. 25 | 26 | resource = "/#{TwitterAds::API_VERSION}/accounts/#{account.id}/features" 27 | params = { feature_keys: 'AGE_TARGETING,CPI_CHARGING' } 28 | 29 | # build and execute the request 30 | response = TwitterAds::Request.new(client, :get, resource, params: params).perform 31 | response.body[:data].first 32 | 33 | # you can also manually construct requests to be 34 | # used in TwitterAds::Cursor object. 35 | 36 | resource = "/#{TwitterAds::API_VERSION}/targeting_criteria/locations" 37 | params = { location_type: 'CITY', q: 'port' } 38 | request = TwitterAds::Request.new(client, :get, resource, params: params) 39 | cursor = TwitterAds::Cursor.new(nil, request) 40 | 41 | # execute requests and iterate cursor until exhausted 42 | cursor.each do |item| 43 | puts item['name'] 44 | end 45 | -------------------------------------------------------------------------------- /examples/poll_cards.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | require 'twitter-ads' 5 | 6 | CONSUMER_KEY = 'your consumer key' 7 | CONSUMER_SECRET = 'your consumer secret' 8 | ACCESS_TOKEN = 'user access token' 9 | ACCESS_TOKEN_SECRET = 'user access token secret' 10 | ADS_ACCOUNT = 'ads account id' 11 | 12 | # initialize the twitter ads api client 13 | client = TwitterAds::Client.new( 14 | CONSUMER_KEY, 15 | CONSUMER_SECRET, 16 | ACCESS_TOKEN, 17 | ACCESS_TOKEN_SECRET 18 | ) 19 | 20 | # load up the account 21 | account = client.accounts(ADS_ACCOUNT) 22 | 23 | # get video object 24 | ml = TwitterAds::Creative::MediaLibrary.all(account, media_type: TwitterAds::Enum::MediaType::VIDEO) 25 | media_key = ml.first.media_key 26 | 27 | # create poll card with video 28 | pc = TwitterAds::Creative::PollCard.new(account) 29 | pc.duration_in_minutes = 10080 # one week 30 | pc.first_choice = 'Northern' 31 | pc.second_choice = 'Southern' 32 | pc.name = '%{media_name} poll card from SDK' % { media_name: ml.first.name } 33 | pc.media_key = media_key 34 | pc.save 35 | 36 | # create Tweet 37 | TwitterAds::Tweet.create(account, text: 'Which hemisphere do you prefer?', card_uri: pc.card_uri) 38 | -------------------------------------------------------------------------------- /examples/promoted_tweet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | require 'twitter-ads' 5 | 6 | CONSUMER_KEY = 'your consumer key' 7 | CONSUMER_SECRET = 'your consumer secret' 8 | ACCESS_TOKEN = 'user access token' 9 | ACCESS_TOKEN_SECRET = 'user access token secret' 10 | ADS_ACCOUNT = 'ads account id' 11 | 12 | # initialize the twitter ads api client 13 | client = TwitterAds::Client.new( 14 | CONSUMER_KEY, 15 | CONSUMER_SECRET, 16 | ACCESS_TOKEN, 17 | ACCESS_TOKEN_SECRET 18 | ) 19 | 20 | # load up the account instance, campaign and line item 21 | account = client.accounts(ADS_ACCOUNT) 22 | campaign = account.campaigns.first 23 | line_item = account.line_items(nil, campaign_ids: campaign.id).first 24 | 25 | # get user_id for as_user_id parameter 26 | user_id = TwitterRestApi::UserIdLookup.load(account, screen_name: 'your_twitter_handle_name').id 27 | 28 | # create request for a simple nullcasted tweet 29 | tweet1 = TwitterAds::Tweet.create(account, text: 'There can be only one...', as_user_id: user_id) 30 | 31 | # promote the tweet using our line item 32 | promoted_tweet = TwitterAds::Creative::PromotedTweet.new(account) 33 | promoted_tweet.line_item_id = line_item.id 34 | promoted_tweet.tweet_id = tweet1[:id] 35 | promoted_tweet.save 36 | 37 | # create request for a nullcasted tweet with a website card 38 | website_card = TwitterAds::Creative::WebsiteCard.all(account).first 39 | tweet2 = TwitterAds::Tweet.create( 40 | account, 41 | text: 'Fine. There can be two.', 42 | card_uri: website_card.card_uri, 43 | as_user_id: user_id 44 | ) 45 | 46 | # promote the tweet using our line item 47 | promoted_tweet = TwitterAds::Creative::PromotedTweet.new(account) 48 | promoted_tweet.line_item_id = line_item.id 49 | promoted_tweet.tweet_id = tweet2[:id] 50 | promoted_tweet.save 51 | -------------------------------------------------------------------------------- /examples/quick_start.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | require 'twitter-ads' 5 | include TwitterAds::Enum 6 | 7 | CONSUMER_KEY = 'your consumer key' 8 | CONSUMER_SECRET = 'your consumer secret' 9 | ACCESS_TOKEN = 'user access token' 10 | ACCESS_TOKEN_SECRET = 'user access token secret' 11 | ADS_ACCOUNT = 'ads account id' 12 | 13 | # initialize the twitter ads api client 14 | client = TwitterAds::Client.new( 15 | CONSUMER_KEY, 16 | CONSUMER_SECRET, 17 | ACCESS_TOKEN, 18 | ACCESS_TOKEN_SECRET 19 | ) 20 | 21 | # load up the account instance 22 | account = client.accounts(ADS_ACCOUNT) 23 | 24 | # create your campaign 25 | campaign = TwitterAds::Campaign.new(account) 26 | campaign.funding_instrument_id = account.funding_instruments.first.id 27 | campaign.daily_budget_amount_local_micro = 1_000_000 28 | campaign.name = 'my first campaign' 29 | campaign.entity_status = EntityStatus::PAUSED 30 | campaign.start_time = Time.now.utc 31 | campaign.save 32 | 33 | # create a line item for the campaign 34 | line_item = TwitterAds::LineItem.new(account) 35 | line_item.campaign_id = campaign.id 36 | line_item.name = 'my first ad' 37 | line_item.product_type = Product::PROMOTED_TWEETS 38 | line_item.placements = [Placement::ALL_ON_TWITTER] 39 | line_item.objective = Objective::ENGAGEMENTS 40 | line_item.bid_amount_local_micro = 10_000 41 | line_item.entity_status = EntityStatus::PAUSED 42 | line_item.save 43 | 44 | # add targeting criteria 45 | targeting_criteria = TwitterAds::TargetingCriteria.new(account) 46 | targeting_criteria.line_item_id = line_item.id 47 | targeting_criteria.targeting_type = 'LOCATION' 48 | targeting_criteria.targeting_value = '00a8b25e420adc94' 49 | targeting_criteria.save 50 | -------------------------------------------------------------------------------- /examples/tweet_previews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | require 'twitter-ads' 4 | include TwitterAds::Enum 5 | 6 | CONSUMER_KEY = 'your consumer key' 7 | CONSUMER_SECRET = 'your consumer secret' 8 | ACCESS_TOKEN = 'user access token' 9 | ACCESS_TOKEN_SECRET = 'user access token secret' 10 | ADS_ACCOUNT = 'ads account id' 11 | 12 | # initialize the twitter ads api client 13 | client = TwitterAds::Client.new( 14 | CONSUMER_KEY, 15 | CONSUMER_SECRET, 16 | ACCESS_TOKEN, 17 | ACCESS_TOKEN_SECRET 18 | ) 19 | 20 | # load up the account instance, campaign and line item 21 | account = client.accounts(ADS_ACCOUNT) 22 | 23 | preview = TwitterAds::Creative::TweetPreview.new(account) 24 | tweets = preview.load( 25 | account, 26 | tweet_ids: %w(1130942781109596160 1101254234031370240), 27 | tweet_type: TweetType::PUBLISHED) 28 | 29 | tweets.each { |tweet| 30 | puts(tweet.tweet_id) 31 | puts(tweet.preview) 32 | } 33 | -------------------------------------------------------------------------------- /features/step_definitions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/twitter-ruby-ads-sdk/0b6759a21236617ba5c4f39178e7a73fc1a4ae4e/features/step_definitions/.gitkeep -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | -------------------------------------------------------------------------------- /lib/twitter-ads.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Naming/FileName 2 | # frozen_string_literal: true 3 | # Copyright (C) 2019 Twitter, Inc. 4 | # rubocop:enable Naming/FileName 5 | 6 | require 'time' 7 | require 'oauth' 8 | require 'multi_json' 9 | require 'forwardable' 10 | require 'logger' 11 | 12 | require 'twitter-ads/version' 13 | require 'twitter-ads/utils' 14 | require 'twitter-ads/error' 15 | require 'twitter-ads/enum' 16 | require 'twitter-ads/resources/dsl' 17 | 18 | require 'twitter-ads/client' 19 | require 'twitter-ads/cursor' 20 | require 'twitter-ads/account' 21 | 22 | require 'twitter-ads/resources/resource' 23 | require 'twitter-ads/resources/persistence' 24 | require 'twitter-ads/resources/analytics' 25 | require 'twitter-ads/resources/batch' 26 | 27 | require 'twitter-ads/http/request' 28 | require 'twitter-ads/http/response' 29 | 30 | require 'twitter-ads/restapi.rb' 31 | 32 | require 'twitter-ads/audiences/custom_audience' 33 | 34 | require 'twitter-ads/campaign/app_list' 35 | require 'twitter-ads/campaign/campaign' 36 | require 'twitter-ads/campaign/funding_instrument' 37 | require 'twitter-ads/campaign/line_item' 38 | require 'twitter-ads/campaign/promotable_user' 39 | require 'twitter-ads/campaign/targeting_criteria' 40 | require 'twitter-ads/campaign/tracking_tags' 41 | require 'twitter-ads/campaign/tweet' 42 | require 'twitter-ads/campaign/organic_tweet' 43 | require 'twitter-ads/campaign/iab_category' 44 | require 'twitter-ads/campaign/advertiser_business_categories' 45 | require 'twitter-ads/campaign/content_categories' 46 | 47 | require 'twitter-ads/targeting_criteria/tv_market' 48 | require 'twitter-ads/targeting_criteria/tv_show' 49 | require 'twitter-ads/targeting_criteria/event' 50 | require 'twitter-ads/targeting_criteria/device' 51 | require 'twitter-ads/targeting_criteria/conversation' 52 | require 'twitter-ads/targeting_criteria/platform' 53 | require 'twitter-ads/targeting_criteria/platform_version' 54 | require 'twitter-ads/targeting_criteria/network_operator' 55 | require 'twitter-ads/targeting_criteria/location' 56 | require 'twitter-ads/targeting_criteria/interest' 57 | require 'twitter-ads/targeting_criteria/language' 58 | require 'twitter-ads/targeting_criteria/app_store_category' 59 | 60 | require 'twitter-ads/creative/account_media' 61 | require 'twitter-ads/creative/cards_fetch' 62 | require 'twitter-ads/creative/cards' 63 | require 'twitter-ads/creative/image_conversation_card' 64 | require 'twitter-ads/creative/media_creative' 65 | require 'twitter-ads/creative/media_library' 66 | require 'twitter-ads/creative/promoted_account' 67 | require 'twitter-ads/creative/promoted_tweet' 68 | require 'twitter-ads/creative/scheduled_tweet' 69 | require 'twitter-ads/creative/draft_tweet' 70 | require 'twitter-ads/creative/video_conversation_card' 71 | require 'twitter-ads/creative/poll_cards' 72 | require 'twitter-ads/creative/tweet_previews' 73 | require 'twitter-ads/creative/tweets' 74 | 75 | require 'twitter-ads/targeting/audience_estimate' 76 | 77 | require 'twitter-ads/measurement/web_event_tag' 78 | require 'twitter-ads/measurement/app_event_tag' 79 | 80 | require 'twitter-ads/settings/user' 81 | require 'twitter-ads/settings/tax' 82 | 83 | require 'twitter-ads/legacy.rb' 84 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/advertiser_business_categories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class AdvertiserBusinessCategories 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | attr_reader :account 11 | 12 | property :id, read_only: true 13 | property :name, read_only: true 14 | property :iab_categories, read_only: true 15 | 16 | # @api private 17 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/advertiser_business_categories" 18 | 19 | def initialize(account) 20 | @account = account 21 | self 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/app_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class AppList 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | attr_reader :account 11 | 12 | property :id, read_only: true 13 | property :apps, read_only: true 14 | property :name, read_only: true 15 | 16 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 17 | 'accounts/%{account_id}/app_lists' # @api private 18 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 19 | 'accounts/%{account_id}/app_lists/%{id}' # @api private 20 | 21 | def initialize(account) 22 | @account = account 23 | self 24 | end 25 | 26 | # Creates a new App List 27 | # 28 | # @param name [String] The name for the app list to be created. 29 | # @param ids [String] String or String Array of app IDs. 30 | # 31 | # @return [self] Returns the instance refreshed from the API 32 | def create(name, *ids) 33 | resource = self.class::RESOURCE_COLLECTION % { account_id: account.id } 34 | params = to_params.merge!(app_store_identifiers: ids.join(','), name: name) 35 | response = Request.new(account.client, :post, resource, params: params).perform 36 | from_response(response.body[:data]) 37 | end 38 | 39 | def apps 40 | reload! if @id && !@apps 41 | @apps 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/campaign.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Campaign < Analytics 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | include TwitterAds::Persistence 10 | include TwitterAds::Batch 11 | 12 | attr_reader :account 13 | 14 | property :id, read_only: true 15 | property :reasons_not_servable, read_only: true 16 | property :servable, read_only: true 17 | property :deleted, type: :bool, read_only: true 18 | property :created_at, type: :time, read_only: true 19 | property :updated_at, type: :time, read_only: true 20 | 21 | property :name 22 | property :funding_instrument_id 23 | property :entity_status 24 | property :effective_status 25 | property :currency 26 | property :standard_delivery 27 | property :daily_budget_amount_local_micro 28 | property :total_budget_amount_local_micro 29 | property :budget_optimization 30 | 31 | # sdk only 32 | property :to_delete, type: :bool 33 | 34 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 35 | 'accounts/%{account_id}/campaigns' # @api private 36 | RESOURCE_BATCH = "/#{TwitterAds::API_VERSION}/" \ 37 | 'batch/accounts/%{account_id}/campaigns' # @api private 38 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 39 | 'accounts/%{account_id}/campaigns/%{id}' # @api private 40 | 41 | def initialize(account) 42 | @account = account 43 | self 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/content_categories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class ContentCategories 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | attr_reader :account 11 | 12 | property :id, read_only: true 13 | property :name, read_only: true 14 | property :iab_categories, read_only: true 15 | 16 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/content_categories" # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/funding_instrument.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class FundingInstrument < Analytics 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | attr_reader :account 11 | 12 | property :id, read_only: true 13 | property :name, read_only: true 14 | property :credit_limit_local_micro, read_only: true 15 | property :currency, read_only: true 16 | property :description, read_only: true 17 | property :funded_amount_local_micro, read_only: true 18 | property :type, read_only: true 19 | property :created_at, type: :time, read_only: true 20 | property :updated_at, type: :time, read_only: true 21 | property :deleted, type: :bool, read_only: true 22 | property :able_to_fund, type: :bool, read_only: true 23 | property :entity_status, read_only: true 24 | property :io_header, read_only: true 25 | property :reasons_not_able_to_fund, read_only: true 26 | 27 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 28 | 'accounts/%{account_id}/funding_instruments' # @api private 29 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 30 | 'accounts/%{account_id}/funding_instruments/%{id}' # @api private 31 | 32 | def initialize(account) 33 | @account = account 34 | self 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/iab_category.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class IABCategory 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | attr_reader :account 11 | 12 | property :id, read_only: true 13 | property :name, read_only: true 14 | property :parent_id, read_only: true 15 | 16 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/iab_categories" # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/line_item.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class LineItem < Analytics 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | include TwitterAds::Persistence 10 | include TwitterAds::Batch 11 | 12 | attr_reader :account 13 | 14 | property :id, read_only: true 15 | property :deleted, type: :bool, read_only: true 16 | property :created_at, type: :time, read_only: true 17 | property :updated_at, type: :time, read_only: true 18 | 19 | property :advertiser_domain 20 | property :android_app_store_identifier 21 | property :audience_expansion 22 | property :bid_amount_local_micro 23 | property :bid_strategy 24 | property :campaign_id 25 | property :categories 26 | property :end_time, type: :time 27 | property :entity_status 28 | property :goal 29 | property :ios_app_store_identifier 30 | property :name 31 | property :objective 32 | property :pay_by 33 | property :placements 34 | property :primary_web_event_tag 35 | property :product_type 36 | property :start_time, type: :time 37 | property :standard_delivery, type: :bool 38 | property :total_budget_amount_local_micro 39 | property :advertiser_user_id 40 | 41 | # sdk only 42 | property :to_delete, type: :bool 43 | 44 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 45 | 'accounts/%{account_id}/line_items' # @api private 46 | RESOURCE_BATCH = "/#{TwitterAds::API_VERSION}/" \ 47 | 'batch/accounts/%{account_id}/line_items' # @api private 48 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 49 | 'accounts/%{account_id}/line_items/%{id}' # @api private 50 | RESOURCE_PLACEMENTS = "/#{TwitterAds::API_VERSION}/" \ 51 | 'line_items/placements' # @api private 52 | 53 | def initialize(account) 54 | @account = account 55 | self 56 | end 57 | 58 | class << self 59 | 60 | # Helper method to return a list a valid placement combinations by Product. 61 | # 62 | # @example 63 | # LineItem.placements(Product::PROMOTED_TWEETS) 64 | # 65 | # @param product_type [Product] The enum value for the Product type being targeted. 66 | # 67 | # @return [Array] An array of valid placement combinations. 68 | # 69 | # @since 0.3.2 70 | # @see https://dev.twitter.com/ads/reference/get/line_items/placements 71 | def placements(client, product_type = nil) 72 | params = { product_type: product_type } if product_type 73 | response = TwitterAds::Request.new(client, :get, 74 | RESOURCE_PLACEMENTS, params: params).perform 75 | response.body[:data][0][:placements] 76 | end 77 | 78 | end 79 | 80 | # Returns a collection of targeting criteria available to the current line item. 81 | # 82 | # @param id [String] The TargetingCriteria ID value. 83 | # @param opts [Hash] A Hash of extended options. 84 | # @option opts [Boolean] :with_deleted Indicates if deleted items should be included. 85 | # @option opts [String] :sort_by The object param to sort the API response by. 86 | # 87 | # @since 0.3.1 88 | # 89 | # @return A Cursor or object instance. 90 | def targeting_criteria(id = nil, opts = {}) 91 | id ? TargetingCriteria.load(account, id, opts) : TargetingCriteria.all(account, @id, opts) 92 | end 93 | 94 | def tracking_tags(id = nil, opts = {}) 95 | id ? TrackingTag.load(account, id, opts) : TrackingTag.all(account, @id, opts) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/organic_tweet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | # Author Bob, Nugit 4 | 5 | module TwitterAds 6 | class OrganicTweet < Analytics 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/promotable_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class PromotableUser 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | attr_reader :account 11 | 12 | property :id, read_only: true 13 | property :promotable_user_type, read_only: true 14 | property :user_id, read_only: true 15 | property :created_at, type: :time, read_only: true 16 | property :updated_at, type: :time, read_only: true 17 | property :deleted, type: :bool, read_only: true 18 | 19 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 20 | 'accounts/%{account_id}/promotable_users' # @api private 21 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 22 | 'accounts/%{account_id}/promotable_users/%{id}' # @api private 23 | 24 | def initialize(account) 25 | @account = account 26 | self 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/targeting_criteria.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class TargetingCriteria 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Persistence 9 | include TwitterAds::Resource::InstanceMethods 10 | include TwitterAds::Batch 11 | 12 | attr_reader :account 13 | 14 | property :id, read_only: true 15 | property :name, read_only: true 16 | property :localized_name, read_only: true 17 | property :created_at, type: :time, read_only: true 18 | property :updated_at, type: :time, read_only: true 19 | property :deleted, type: :bool, read_only: true 20 | 21 | property :line_item_id 22 | property :targeting_type 23 | property :targeting_value 24 | property :operator_type 25 | property :tailored_audience_expansion, type: :bool 26 | property :location_type 27 | 28 | # sdk only 29 | property :to_delete, type: :bool 30 | 31 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 32 | 'accounts/%{account_id}/targeting_criteria' # @api private 33 | RESOURCE_BATCH = "/#{TwitterAds::API_VERSION}/" \ 34 | 'batch/accounts/%{account_id}/targeting_criteria' # @api private 35 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 36 | 'accounts/%{account_id}/targeting_criteria/%{id}' # @api private 37 | 38 | def initialize(account) 39 | @account = account 40 | self 41 | end 42 | 43 | class << self 44 | 45 | # Returns a Cursor instance for a given resource. 46 | # 47 | # @param account [Account] The Account object instance. 48 | # @param line_item_ids [String] A String or String array of Line Item IDs. 49 | # @param opts [Hash] An optional Hash of extended options. 50 | # @option opts [Boolean] :with_deleted Indicates if deleted items should be included. 51 | # @option opts [String] :sort_by The object param to sort the API response by. 52 | # 53 | # @return [Cursor] A Cusor object ready to iterate through the API response. 54 | # 55 | # @since 0.3.1 56 | # @see Cursor 57 | # @see https://dev.twitter.com/ads/basics/sorting Sorting 58 | def all(account, line_item_ids, opts = {}) 59 | params = { line_item_ids: Array(line_item_ids).join(',') }.merge!(opts) 60 | resource = RESOURCE_COLLECTION % { account_id: account.id } 61 | request = Request.new(account.client, :get, resource, params: params) 62 | Cursor.new(self, request, init_with: [account]) 63 | end 64 | 65 | # Returns an object instance for a given resource. 66 | # 67 | # @param account [Account] The Account object instance. 68 | # @param id [String] The ID of the specific object to be loaded. 69 | # @param opts [Hash] An optional Hash of extended options. 70 | # @option opts [Boolean] :with_deleted Indicates if deleted items should be included. 71 | # @option opts [String] :sort_by The object param to sort the API response by. 72 | # 73 | # @return [self] The object instance for the specified resource. 74 | # 75 | # @since 0.3.1 76 | def load(account, id, opts = {}) 77 | params = { with_deleted: true }.merge!(opts) 78 | resource = RESOURCE % { account_id: account.id, id: id } 79 | response = Request.new(account.client, :get, resource, params: params).perform 80 | new(account).from_response(response.body[:data]) 81 | end 82 | 83 | end 84 | 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/tracking_tags.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class TrackingTag 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | include TwitterAds::Persistence 10 | 11 | attr_reader :account 12 | 13 | property :id, read_only: true 14 | property :deleted, type: :bool, read_only: true 15 | property :created_at, type: :time, read_only: true 16 | property :updated_at, type: :time, read_only: true 17 | 18 | property :line_item_id 19 | property :tracking_tag_type 20 | property :tracking_tag_url 21 | 22 | # sdk only 23 | property :to_delete, type: :bool 24 | 25 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 26 | 'accounts/%{account_id}/tracking_tags' # @api private 27 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 28 | 'accounts/%{account_id}/tracking_tags/%{id}' # @api private 29 | 30 | def initialize(account) 31 | @account = account 32 | self 33 | end 34 | 35 | # Creates a new Tracking Tag 36 | # 37 | # @param line_item_id [String] The line item id to create tags for. 38 | # @param tracking_tag_url [String] tracking tag URL. 39 | # 40 | # @return [self] Returns the instance refreshed from the API 41 | def create(line_item_id, tracking_tag_url) 42 | resource = self.class::RESOURCE_COLLECTION % { account_id: account.id } 43 | params = to_params.merge!( 44 | line_item_id: line_item_id, 45 | tracking_tag_url: tracking_tag_url, 46 | tracking_tag_type: 'IMPRESSION_TAG' 47 | ) 48 | response = Request.new(account.client, :post, resource, params: params).perform 49 | from_response(response.body[:data]) 50 | end 51 | 52 | class << self 53 | 54 | # Returns a Cursor instance for a given resource. 55 | # 56 | # @param account [Account] The Account object instance. 57 | # @param line_item_ids [String] A String or String array of Line Item IDs. 58 | # @param opts [Hash] An optional Hash of extended options. 59 | # @option opts [Boolean] :with_deleted Indicates if deleted items should be included. 60 | # @option opts [String] :sort_by The object param to sort the API response by. 61 | # 62 | # @return [Cursor] A Cusor object ready to iterate through the API response. 63 | # 64 | # @since 0.3.1 65 | # @see Cursor 66 | # @see https://dev.twitter.com/ads/basics/sorting Sorting 67 | def all(account, line_item_ids, opts = {}) 68 | if !line_item_ids.empty? 69 | params = { line_item_ids: Array(line_item_ids).join(',') }.merge!(opts) 70 | end 71 | resource = RESOURCE_COLLECTION % { account_id: account.id } 72 | request = Request.new(account.client, :get, resource, params: params) 73 | Cursor.new(self, request, init_with: [account]) 74 | end 75 | 76 | # Returns an object instance for a given resource. 77 | # 78 | # @param account [Account] The Account object instance. 79 | # @param id [String] The ID of the specific object to be loaded. 80 | # @param opts [Hash] An optional Hash of extended options. 81 | # @option opts [Boolean] :with_deleted Indicates if deleted items should be included. 82 | # @option opts [String] :sort_by The object param to sort the API response by. 83 | # 84 | # @return [self] The object instance for the specified resource. 85 | # 86 | # @since 0.3.1 87 | def load(account, id, opts = {}) 88 | params = { with_deleted: true }.merge!(opts) 89 | resource = RESOURCE % { account_id: account.id, id: id } 90 | response = Request.new(account.client, :get, resource, params: params).perform 91 | new(account).from_response(response.body[:data]) 92 | end 93 | 94 | end 95 | 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/twitter-ads/campaign/tweet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Tweet < Analytics 6 | 7 | RESOURCE_CREATE = "/#{TwitterAds::API_VERSION}/" \ 8 | 'accounts/%{account_id}/tweet' # @api private 9 | 10 | class << self 11 | 12 | # Creates a "Promoted-Only" Tweet using the specialized Ads API end point. 13 | # 14 | # @param opts [Hash] A hash of options. 15 | # 16 | # @option opts [String] :text The main Tweet body. 17 | # @option opts [Array] :media_keys A list of media keys (up to 4) to associate with the Tweet. 18 | # @option opts [Integer] :as_user_id The user ID whom you are posting the Tweet on behalf of. 19 | # @option opts [Boolean] :trim_user Excludes the user object from the hydrated Tweet response. 20 | # @option opts [String] :video_title An optional title to be included. 21 | # @option opts [String] :video_description An optional description to be included. 22 | # @option opts [String] :video_cta An optional CTA value for the associated video. 23 | # @option opts [String] :video_cta_value The value for the corresponding CTA. 24 | # 25 | # @since 0.3.0 26 | # 27 | # @see https://dev.twitter.com/ads/reference/post/accounts/%3Aaccount_id/tweet 28 | # 29 | # @return [Hash] A hash containing the newly created Tweet object. 30 | def create(account, opts = {}) 31 | resource = RESOURCE_CREATE % { account_id: account.id } 32 | response = TwitterAds::Request.new(account.client, :post, resource, params: opts).perform 33 | response.body[:data] 34 | end 35 | 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/twitter-ads/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | 6 | API_VERSION = '11' 7 | 8 | # The Ads API Client class which functions as a 9 | # container for basic API consumer information. 10 | class Client 11 | 12 | attr_accessor :consumer_key, 13 | :consumer_secret, 14 | :access_token, 15 | :access_token_secret, 16 | :options 17 | 18 | # Creates a new Ads API client instance. 19 | # 20 | # @param consumer_key nil [String] The application consumer key value. 21 | # @param consumer_secret nil [String] The application consumer secret value. 22 | # @param access_token nil [String] The access token value. 23 | # @param access_token_secret nil [String] The access token secret value. 24 | # 25 | # @param opts [Hash] An optional Hash of extended options. 26 | # @option opts [Boolean] :sandbox When true, enables sandbox mode for all requests. 27 | # @option opts [Boolean] :trace When true, enables verbose request tracing for all requests. 28 | # 29 | # @since 0.1.0 30 | # 31 | # @return [Client] The newly created client instance. 32 | def initialize(consumer_key, consumer_secret, access_token, access_token_secret, opts = {}) 33 | @consumer_key = consumer_key 34 | @consumer_secret = consumer_secret 35 | @access_token = access_token 36 | @access_token_secret = access_token_secret 37 | @options = opts.fetch(:options, {}) 38 | validate 39 | self 40 | end 41 | 42 | # Returns the Logger instance for request logging. 43 | # 44 | # @since 0.2.0 45 | # 46 | # @return [Logger] The logger instance. 47 | def logger 48 | @logger ||= Logger.new(STDOUT) 49 | @logger.progname = 'twitter-ads' unless @logger.progname 50 | @logger 51 | end 52 | 53 | # Returns an inspection string for the current Client instance. 54 | # 55 | # @example 56 | # client.inspect 57 | # 58 | # @since 0.1.0 59 | # 60 | # @return [String] The inspection string. 61 | def inspect 62 | "#<#{self.class.name}:0x#{object_id} consumer_key=\"#{@consumer_key}\">" 63 | end 64 | 65 | # Returns a collection of advertiser Accounts available to the current access token. 66 | # 67 | # @example 68 | # client.accounts 69 | # client.accounts('3ofs6l') 70 | # client.accounts('3ofs6l', with_deleted: true) 71 | # 72 | # @param id=nil [String] The account ID string. 73 | # @param opts={} [Hash] Hash of optional values. 74 | # 75 | # @option opts [String] :with_deleted Indicates whether or not to included deleted objects. 76 | # 77 | # @since 0.1.0 78 | # 79 | # @return [Account] The instance of the Account object. 80 | def accounts(id = nil, opts = {}) 81 | id ? Account.load(self, id) : Account.all(self, opts) 82 | end 83 | 84 | private 85 | 86 | def validate 87 | [:consumer_key, :consumer_secret, :access_token, :access_token_secret].each do |name| 88 | fail(ArgumentError, "Error! Missing required #{name}.") unless send(name) 89 | end 90 | end 91 | end 92 | 93 | end 94 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/account_media.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class AccountMedia 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | property :deleted, type: :bool, read_only: true 16 | property :created_at, type: :time, read_only: true 17 | property :updated_at, type: :time, read_only: true 18 | property :id, read_only: true 19 | property :creative_type, read_only: true 20 | property :media_key, read_only: true 21 | property :media_url, read_only: true 22 | 23 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 24 | 'accounts/%{account_id}/account_media' # @api private 25 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 26 | 'accounts/%{account_id}/account_media/%{id}' # @api private 27 | 28 | def initialize(account) 29 | @account = account 30 | self 31 | end 32 | 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/cards.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class Cards 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | property :id, read_only: true 16 | property :card_type, read_only: true 17 | property :card_uri, read_only: true 18 | property :created_at, type: :time, read_only: true 19 | property :deleted, type: :bool, read_only: true 20 | property :updated_at, type: :time, read_only: true 21 | # these are writable, but not in the sense that they can be set on an object and then saved 22 | property :name 23 | property :components 24 | 25 | RESOURCE = "/#{TwitterAds::API_VERSION}/" + 26 | 'accounts/%{account_id}/cards/%{id}' # @api private 27 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" + 28 | 'accounts/%{account_id}/cards' # @api private 29 | 30 | def save 31 | headers = { 'Content-Type' => 'application/json' } 32 | params = { 'name': name, 'components': components } 33 | if @id 34 | resource = RESOURCE % { 35 | account_id: account.id, 36 | id: id 37 | } 38 | request = Request.new(account.client, 39 | :put, 40 | resource, 41 | headers: headers, 42 | body: params.to_json) 43 | else 44 | resource = RESOURCE_COLLECTION % { 45 | account_id: account.id 46 | } 47 | request = Request.new(account.client, 48 | :post, 49 | resource, 50 | headers: headers, 51 | body: params.to_json) 52 | end 53 | 54 | response = request.perform 55 | from_response(response.body[:data]) 56 | end 57 | 58 | def initialize(account) 59 | @account = account 60 | self 61 | end 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/cards_fetch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class CardsFetch 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | property :country_code, read_only: true 16 | property :app_cta, read_only: true 17 | property :card_type, read_only: true 18 | property :card_uri, read_only: true 19 | property :content_duration_seconds, read_only: true 20 | property :created_at, type: :time, read_only: true 21 | property :deleted, type: :bool, read_only: true 22 | property :duration_in_minutes, read_only: true 23 | property :end_time, type: :time, read_only: true 24 | property :first_choice, read_only: true 25 | property :first_cta, read_only: true 26 | property :first_cta_tweet, read_only: true 27 | property :first_cta_welcome_message_id, read_only: true 28 | property :fouth_choice, read_only: true 29 | property :fouth_cta, read_only: true 30 | property :fouth_cta_tweet, read_only: true 31 | property :fourth_cta_welcome_message_id, read_only: true 32 | property :googleplay_app_id, read_only: true 33 | property :googleplay_deep_link, read_only: true 34 | property :id, read_only: true 35 | property :image, read_only: true 36 | property :image_display_height, read_only: true 37 | property :image_display_width, read_only: true 38 | property :ios_app_store_identifier, read_only: true 39 | property :ios_deep_link, read_only: true 40 | property :name, read_only: true 41 | property :recipient_user_id, read_only: true 42 | property :second_choice, read_only: true 43 | property :second_cta, read_only: true 44 | property :second_cta_tweet, read_only: true 45 | property :second_cta_welcome_message_id, read_only: true 46 | property :start_time, type: :time, read_only: true 47 | property :thank_you_text, read_only: true 48 | property :thank_you_url, read_only: true 49 | property :third_choice, read_only: true 50 | property :third_cta, read_only: true 51 | property :third_cta_tweet, read_only: true 52 | property :third_cta_welcome_message_id, read_only: true 53 | property :title, read_only: true 54 | property :updated_at, type: :time, read_only: true 55 | property :video_content_id, read_only: true 56 | property :video_height, read_only: true 57 | property :video_hls_url, read_only: true 58 | property :video_owner_id, read_only: true 59 | property :video_poster_height, read_only: true 60 | property :video_poster_url, read_only: true 61 | property :video_poster_width, read_only: true 62 | property :video_width, read_only: true 63 | property :video_url, read_only: true 64 | property :website_dest_url, read_only: true 65 | property :website_display_url, read_only: true 66 | property :website_shortened_url, read_only: true 67 | property :website_title, read_only: true 68 | property :website_url, read_only: true 69 | property :wide_app_image, read_only: true 70 | property :id, read_only: true 71 | 72 | FETCH_URI = "/#{TwitterAds::API_VERSION}/" + 73 | 'accounts/%{account_id}/cards/all' # @api private 74 | FETCH_ID = "/#{TwitterAds::API_VERSION}/" + 75 | 'accounts/%{account_id}/cards/all/%{id}' # @api private 76 | 77 | def all(*) 78 | raise ArgumentError.new( 79 | "'CardsFetch' object has no attribute 'all'") 80 | end 81 | 82 | def load(account, card_uris = nil, card_id = nil, with_deleted = nil, opts = {}) 83 | if (card_uris && card_id) || (card_uris.nil? && card_id.nil?) 84 | raise ArgumentError.new('card_uris and card_id are exclusive parameters. ' \ 85 | 'Please supply one or the other, but not both.') 86 | end 87 | 88 | params = {}.merge!(opts) 89 | 90 | if with_deleted && TwitterAds::Utils.to_bool(with_deleted) 91 | params = { with_deleted: true }.merge!(params) 92 | end 93 | 94 | if card_uris 95 | params = { card_uris: Array(card_uris).join(',') }.merge!(params) 96 | resource = FETCH_URI % { account_id: account.id } 97 | request = Request.new(account.client, :get, resource, params: params) 98 | return Cursor.new(self.class, request, init_with: [account]) 99 | else 100 | params = { card_id: card_id }.merge!(params) 101 | resource = FETCH_ID % { account_id: account.id, id: card_id } 102 | response = Request.new(account.client, :get, resource, params: params).perform 103 | return from_response(response.body[:data]) 104 | end 105 | end 106 | 107 | def reload! 108 | load(@account, card_id: @id) if @id 109 | end 110 | 111 | def initialize(account) 112 | @account = account 113 | self 114 | end 115 | 116 | end 117 | 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/draft_tweet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class DraftTweet 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | # read-only 16 | property :id, read_only: true 17 | property :created_at, type: :time, read_only: true 18 | property :updated_at, type: :time, read_only: true 19 | property :user_id, read_only: true 20 | # writable 21 | property :as_user_id 22 | property :card_uri 23 | property :media_keys 24 | property :nullcast, type: :bool 25 | property :text 26 | 27 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 28 | 'accounts/%{account_id}/draft_tweets' # @api private 29 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 30 | 'accounts/%{account_id}/draft_tweets/%{id}' # @api private 31 | 32 | def initialize(account) 33 | @account = account 34 | self 35 | end 36 | 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/image_conversation_card.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class ImageConversationCard 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | property :card_type, read_only: true 16 | property :card_uri, read_only: true 17 | property :created_at, type: :time, read_only: true 18 | property :deleted, type: :bool, read_only: true 19 | property :id, read_only: true 20 | property :image, read_only: true 21 | property :updated_at, type: :time, read_only: true 22 | property :media_url, read_only: true 23 | 24 | property :unlocked_image_media_key 25 | property :first_cta 26 | property :first_cta_tweet 27 | property :fourth_cta 28 | property :fourth_cta_tweet 29 | property :media_key 30 | property :name 31 | property :second_cta 32 | property :second_cta_tweet 33 | property :thank_you_text 34 | property :thank_you_url 35 | property :third_cta 36 | property :third_cta_tweet 37 | property :title 38 | 39 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 40 | 'accounts/%{account_id}/cards/image_conversation' # @api private 41 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 42 | 'accounts/%{account_id}/cards/image_conversation/%{id}' # @api private 43 | 44 | def initialize(account) 45 | @account = account 46 | self 47 | end 48 | 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/media_creative.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class MediaCreative < Analytics 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | property :approval_status, read_only: true 16 | property :created_at, type: :time, read_only: true 17 | property :deleted, type: :bool, read_only: true 18 | property :id, read_only: true 19 | property :entity_status, read_only: true 20 | property :updated_at, type: :time, read_only: true 21 | 22 | property :account_media_id 23 | property :line_item_id 24 | property :landing_url 25 | 26 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 27 | 'accounts/%{account_id}/media_creatives' # @api private 28 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 29 | 'accounts/%{account_id}/media_creatives/%{id}' # @api private 30 | 31 | def initialize(account) 32 | @account = account 33 | self 34 | end 35 | 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/media_library.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class MediaLibrary 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | # read-only 16 | property :aspect_ratio, read_only: true 17 | property :created_at, type: :bool, read_only: true 18 | property :deleted, type: :bool, read_only: true 19 | property :duration, read_only: true 20 | property :media_status, read_only: true 21 | property :media_type, read_only: true 22 | property :media_url, read_only: true 23 | property :tweeted, type: :bool, read_only: true 24 | property :updated_at, type: :time, read_only: true 25 | property :poster_media_url, read_only: true 26 | 27 | # writable 28 | property :media_key 29 | property :description 30 | property :file_name 31 | property :name 32 | property :poster_media_key 33 | property :title 34 | 35 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 36 | 'accounts/%{account_id}/media_library' # @api private 37 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 38 | 'accounts/%{account_id}/media_library/%{id}' # @api private 39 | 40 | def reload(account, opts = {}) 41 | if @media_key 42 | resource = self.class::RESOURCE % { account_id: account.id, id: media_key } 43 | response = Request.new(account.client, :get, resource, params: opts).perform 44 | response.body[:data] 45 | end 46 | end 47 | 48 | def add 49 | params = to_params 50 | resource = self.class::RESOURCE_COLLECTION % { account_id: account.id } 51 | response = Request.new(account.client, :post, resource, params: params).perform 52 | from_response(response.body[:data]) 53 | end 54 | 55 | def update 56 | params = to_params 57 | resource = self.class::RESOURCE % { account_id: account.id, id: media_key } 58 | response = Request.new(account.client, :put, resource, params: params).perform 59 | from_response(response.body[:data]) 60 | end 61 | 62 | def delete! 63 | resource = RESOURCE % { account_id: account.id, id: media_key } 64 | response = Request.new(account.client, :delete, resource).perform 65 | from_response(response.body[:data]) 66 | end 67 | 68 | def initialize(account) 69 | @account = account 70 | self 71 | end 72 | 73 | end 74 | 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/poll_cards.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class PollCard 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | property :card_type, read_only: true 16 | property :card_uri, read_only: true 17 | property :content_duration_seconds, read_only: true 18 | property :created_at, type: :time, read_only: true 19 | property :deleted, type: :bool, read_only: true 20 | property :end_time, type: :time, read_only: true 21 | property :id, read_only: true 22 | property :image, read_only: true 23 | property :image_display_height, read_only: true 24 | property :image_display_width, read_only: true 25 | property :start_time, type: :time, read_only: true 26 | property :updated_at, type: :time, read_only: true 27 | property :video_height, read_only: true 28 | property :video_hls_url, read_only: true 29 | property :video_poster_height, read_only: true 30 | property :video_poster_url, read_only: true 31 | property :video_poster_width, read_only: true 32 | property :video_url, read_only: true 33 | property :video_width, read_only: true 34 | 35 | property :duration_in_minutes 36 | property :name 37 | property :media_key 38 | property :first_choice 39 | property :second_choice 40 | property :third_choice 41 | property :fourth_choice 42 | 43 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 44 | 'accounts/%{account_id}/cards/poll' # @api private 45 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 46 | 'accounts/%{account_id}/cards/poll/%{id}' # @api private 47 | 48 | def initialize(account) 49 | @account = account 50 | self 51 | end 52 | 53 | end 54 | 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/promoted_account.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class PromotedAccount < Analytics 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | property :id, read_only: true 16 | property :approval_status, read_only: true 17 | property :created_at, type: :time, read_only: true 18 | property :updated_at, type: :time, read_only: true 19 | property :deleted, type: :bool, read_only: true 20 | 21 | property :line_item_id 22 | property :user_id 23 | property :paused, type: :bool 24 | 25 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 26 | 'accounts/%{account_id}/promoted_accounts' # @api private 27 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 28 | 'accounts/%{account_id}/promoted_accounts/%{id}' # @api private 29 | 30 | def initialize(account) 31 | @account = account 32 | self 33 | end 34 | 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/promoted_tweet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class PromotedTweet < Analytics 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | property :id, read_only: true 16 | property :approval_status, read_only: true 17 | property :created_at, type: :time, read_only: true 18 | property :updated_at, type: :time, read_only: true 19 | property :deleted, type: :bool, read_only: true 20 | property :entity_status, read_only: true 21 | 22 | property :line_item_id 23 | property :tweet_id 24 | 25 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 26 | 'accounts/%{account_id}/promoted_tweets' # @api private 27 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 28 | 'accounts/%{account_id}/promoted_tweets/%{id}' # @api private 29 | 30 | def initialize(account) 31 | @account = account 32 | self 33 | end 34 | 35 | # Saves or updates the current object instance depending on the presence of `object.id`. 36 | # 37 | # @example 38 | # object.save 39 | # 40 | # @return [self] Returns the instance refreshed from the API. 41 | # 42 | # Note: override to handle the inconsistency of the promoted tweet endpoint. (see REVAPI-5348) 43 | # 44 | # @since 0.2.4 45 | def save 46 | # manually check for missing params (due to API discrepancy) 47 | validate 48 | 49 | # convert to `tweet_ids` param 50 | params = to_params 51 | params[:tweet_ids] = params.delete(:tweet_id) if params.key?(:tweet_id) 52 | 53 | if @id 54 | raise TwitterAds::NotFound.new(nil, 'Method PUT not allowed.', 404) 55 | else 56 | resource = self.class::RESOURCE_COLLECTION % { account_id: account.id } 57 | response = Request.new(account.client, :post, resource, params: params).perform 58 | from_response(response.body[:data].first) 59 | end 60 | end 61 | 62 | private 63 | 64 | def validate 65 | details = [] 66 | 67 | unless @line_item_id 68 | details << { code: 'MISSING_PARAMETER', 69 | message: '"line_item_id" is a required parameter', 70 | parameter: 'line_item_id' } 71 | end 72 | 73 | unless @tweet_id 74 | details << { code: 'MISSING_PARAMETER', 75 | message: '"tweet_id" is a required parameter', 76 | parameter: 'tweet_id' } 77 | end 78 | 79 | raise TwitterAds::ClientError.new(nil, details, 400) unless details.empty? 80 | end 81 | 82 | end 83 | 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/scheduled_tweet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class ScheduledTweet 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | # read-only 16 | property :completed_at, type: :time, read_only: true 17 | property :created_at, type: :time, read_only: true 18 | property :id, read_only: true 19 | property :id_str, read_only: true 20 | property :scheduled_status, read_only: true 21 | property :tweet_id, read_only: true 22 | property :updated_at, type: :time, read_only: true 23 | property :user_id, read_only: true 24 | 25 | # writable 26 | property :as_user_id 27 | property :card_uri 28 | property :media_keys 29 | property :nullcast, type: :bool 30 | property :scheduled_at, type: :time 31 | property :text 32 | 33 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 34 | 'accounts/%{account_id}/scheduled_tweets' # @api private 35 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 36 | 'accounts/%{account_id}/scheduled_tweets/%{id}' # @api private 37 | 38 | def initialize(account) 39 | @account = account 40 | self 41 | end 42 | 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/tweet_previews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class TweetPreview 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | 12 | attr_reader :account 13 | 14 | property :preview, read_only: true 15 | property :tweet_id, read_only: true 16 | 17 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" + 18 | 'accounts/%{account_id}/tweet_previews' # @api private 19 | 20 | def load(account, tweet_ids:, tweet_type:) 21 | params = { tweet_ids: Array(tweet_ids).join(',') } 22 | params[:tweet_type] = tweet_type 23 | resource = RESOURCE_COLLECTION % { account_id: account.id } 24 | request = Request.new(account.client, :get, resource, params: params) 25 | Cursor.new(self.class, request, init_with: [account]) 26 | end 27 | 28 | def initialize(account) 29 | @account = account 30 | self 31 | end 32 | 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/tweets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class Tweets 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | 12 | attr_reader :account 13 | 14 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" + 15 | 'accounts/%{account_id}/tweets' # @api private 16 | 17 | # Retrieve Tweet details for the account's full promotable user (default) 18 | # or the user specified in the user_id parameter. 19 | # 20 | # @example 21 | # tweets = TwitterAds::Creative::Tweets.all( 22 | # account, 23 | # tweet_type: 'PUBLISHED', 24 | # tweet_ids: %w(1122911801354510336 1102836745790316550), 25 | # timeline_type: 'ORGANIC' 26 | # ) 27 | # 28 | # @param account [Account] The Account object instance. 29 | # @param tweet_type [String] The Tweet type for the specified tweet_ids. 30 | # @option opts [Int] :count The number of records to try and retrieve per distinct request. 31 | # @option opts [String] :cursor A cursor to get the next page of results. 32 | # @option opts [String] :timeline_type The granularity to use (default: NULLCAST). 33 | # @option opts [Boolean] :trim_user Whether to exclude the user object 34 | # in the Tweet response (default: false). 35 | # @option opts [Array] :tweet_ids A collection of tweet IDs to be fetched. 36 | # @option opts [Long] :user_id The user ID to scope Tweets to. 37 | # 38 | # @return A list of tweets details. 39 | # 40 | # @see https://developer.twitter.com/en/docs/ads/creatives/api-reference/tweets#get-accounts-account-id-tweets 41 | # @since 6.0.0 42 | 43 | def self.all(account, opts = {}) 44 | params = TwitterAds::Utils.flatten_params(opts) 45 | resource = self::RESOURCE_COLLECTION % { account_id: account.id } 46 | request = Request.new(account.client, :get, resource, params: params) 47 | Cursor.new(nil, request, init_with: [account]) 48 | end 49 | 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/twitter-ads/creative/video_conversation_card.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Creative 6 | 7 | class VideoConversationCard 8 | 9 | include TwitterAds::DSL 10 | include TwitterAds::Resource 11 | include TwitterAds::Persistence 12 | 13 | attr_reader :account 14 | 15 | property :card_type, read_only: true 16 | property :card_uri, read_only: true 17 | property :created_at, type: :time, read_only: true 18 | property :deleted, type: :bool, read_only: true 19 | property :id, read_only: true 20 | property :updated_at, type: :time, read_only: true 21 | property :media_url, read_only: true 22 | property :poster_media_url, read_only: true 23 | 24 | property :unlocked_image_media_key 25 | property :unlocked_video_media_key 26 | property :fourth_cta 27 | property :fourth_cta_tweet 28 | property :poster_media_key 29 | property :first_cta 30 | property :first_cta_tweet 31 | property :name 32 | property :second_cta 33 | property :second_cta_tweet 34 | property :thank_you_text 35 | property :thank_you_url 36 | property :third_cta 37 | property :third_cta_tweet 38 | property :title 39 | property :media_key 40 | 41 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 42 | 'accounts/%{account_id}/cards/video_conversation' # @api private 43 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 44 | 'accounts/%{account_id}/cards/video_conversation/%{id}' # @api private 45 | 46 | def initialize(account) 47 | @account = account 48 | self 49 | end 50 | 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/twitter-ads/cursor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Cursor 6 | 7 | include ::Enumerable 8 | extend ::Forwardable 9 | 10 | def_delegators :@collection, :first, :[] 11 | 12 | # Creates a new Cursor instance. 13 | # 14 | # @param klass [String] The object class the contained by the Cursor instance. 15 | # @param request [Request] The Request object instance. 16 | # @param opts [Hash] A Hash of extended options. 17 | # 18 | # @return [Cursor] The new Cursor instance. 19 | # 20 | # @since 0.1.0 21 | def initialize(klass, request, opts = {}) 22 | @klass = klass 23 | @client = request.client 24 | @method = request.method 25 | @resource = request.resource 26 | @options = opts.merge!(request.options) 27 | @collection = [] 28 | from_response(request.perform) 29 | self 30 | end 31 | 32 | # Exhausts the Cursor then returns the last object in the collection. 33 | # 34 | # @return [Object] The last object in the collection. 35 | # 36 | # @since 0.2.0 37 | def last 38 | each {} 39 | @collection.last 40 | end 41 | 42 | # Method to fetch the next page only. 43 | # 44 | # @return [Array] A collection containing the next page of objects. 45 | # 46 | # @since 0.1.0 47 | def next 48 | return @collection unless @next_cursor 49 | current_size = @collection.size - 1 50 | fetch_next 51 | @collection[current_size..-1] 52 | end 53 | 54 | # Method to iterate through all items until the Cursor is exhausted. 55 | # 56 | # @return [Cursor] The current Cursor instance. 57 | # 58 | # @since 0.1.0 59 | def each(offset = 0, &block) 60 | return to_enum(:each, offset) unless block_given? 61 | @collection[offset..-1].each { |element| yield(element) } 62 | unless exhausted? 63 | offset = [@collection.size, offset].max 64 | fetch_next 65 | each(offset, &block) 66 | end 67 | self 68 | end 69 | 70 | # Determines whether or not the current Cusor instance has been exhausted. 71 | # 72 | # @return [Boolean] A boolean value indicating Cursor status. 73 | # 74 | # @since 0.1.0 75 | def exhausted? 76 | !@next_cursor 77 | end 78 | 79 | # Returns the full size of the cursor (even if not exhausted). 80 | # 81 | # @return [Integer] The Cursor count / size. 82 | # 83 | # @since 0.1.0 84 | def count 85 | @total_count || @collection.size 86 | end 87 | alias size count 88 | 89 | # Returns an inspection string for the current Cursor instance. 90 | # 91 | # @example 92 | # cursor.inspect 93 | # 94 | # @since 0.1.0 95 | # 96 | # @return [String] The inspection string. 97 | def inspect 98 | "#<#{self.class.name}:0x#{object_id} " \ 99 | "count=#{size} fetched=#{@collection.size} " \ 100 | "exhausted=#{exhausted?}>" 101 | end 102 | 103 | private 104 | 105 | def fetch_next 106 | return unless @next_cursor 107 | opts = @options.dup 108 | opts[:params] = opts.fetch(:params, {}).merge!(cursor: @next_cursor) 109 | from_response(Request.new(@client, @method, @resource, opts).perform) 110 | end 111 | 112 | def from_response(response) 113 | @next_cursor = response.body[:next_cursor] 114 | @total_count = response.body[:total_count].to_i if response.body.key?(:total_count) 115 | 116 | TwitterAds::Utils.extract_response_headers(response.headers).each { |key, value| 117 | singleton_class.class_eval { attr_accessor key } 118 | instance_variable_set("@#{key}", value) 119 | } 120 | 121 | response.body.fetch(:data, []).each do |object| 122 | @collection << if @klass&.method_defined?(:from_response) 123 | @klass.new( 124 | *@options[:init_with]).from_response(object) 125 | else 126 | object 127 | end 128 | end 129 | end 130 | 131 | end 132 | 133 | end 134 | -------------------------------------------------------------------------------- /lib/twitter-ads/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | 6 | class Error < StandardError 7 | 8 | attr_reader :code, :headers, :response, :details 9 | 10 | def initialize(*args) 11 | if args.size == 1 && args[0].respond_to?(:body) && args[0].respond_to?(:code) 12 | @response = args[0] 13 | @code = args[0].code 14 | @details = args[0].body[:errors] if args[0].body.is_a?(Hash) && args[0].body[:errors] 15 | elsif args.size == 3 16 | @response = args[0] 17 | @details = args[1] 18 | @code = args[2] 19 | end 20 | self 21 | end 22 | 23 | def inspect 24 | str = +"#<#{self.class.name}:0x#{object_id}" 25 | str << " code=#{@code}" if @code 26 | str << " details=\"#{@details}\"" if @details 27 | str << '>' 28 | end 29 | alias to_s inspect 30 | 31 | class << self 32 | 33 | ERRORS = { 34 | 400 => 'TwitterAds::BadRequest', 35 | 401 => 'TwitterAds::NotAuthorized', 36 | 403 => 'TwitterAds::Forbidden', 37 | 404 => 'TwitterAds::NotFound', 38 | 429 => 'TwitterAds::RateLimit', 39 | 500 => 'TwitterAds::ServerError', 40 | 503 => 'TwitterAds::ServiceUnavailable' 41 | }.freeze 42 | 43 | # Returns an appropriately typed Error object based from an API response. 44 | # 45 | # @param object [Hash] The parsed JSON API response. 46 | # 47 | # @return [Error] The error object instance. 48 | # 49 | # @since 0.1.0 50 | # @api private 51 | def from_response(object) 52 | return class_eval(ERRORS[object.code]).new(object) if ERRORS.key?(object.code) 53 | new(object) # fallback, unknown error 54 | end 55 | 56 | end 57 | 58 | end 59 | 60 | # Server Errors (5XX) 61 | class ServerError < Error; end 62 | class ServiceUnavailable < ServerError; end 63 | 64 | # Client Errors (4XX) 65 | class ClientError < Error; end 66 | class NotAuthorized < ClientError; end 67 | class Forbidden < ClientError; end 68 | class NotFound < ClientError; end 69 | class BadRequest < ClientError; end 70 | 71 | class RateLimit < ClientError 72 | attr_reader :reset_at 73 | 74 | def initialize(object) 75 | super object 76 | header = object.headers.fetch('x-account-rate-limit-reset', nil) || 77 | object.headers.fetch('x-rate-limit-reset', nil) 78 | @reset_at = header.first.to_i 79 | self 80 | end 81 | end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /lib/twitter-ads/http/request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | 6 | # Generic container for API requests. 7 | class Request 8 | 9 | attr_reader :client, :method, :resource, :options 10 | 11 | HTTP_METHOD = { 12 | get: Net::HTTP::Get, 13 | post: Net::HTTP::Post, 14 | put: Net::HTTP::Put, 15 | delete: Net::HTTP::Delete 16 | }.freeze 17 | 18 | DEFAULT_DOMAIN = 'https://ads-api.twitter.com' 19 | SANDBOX_DOMAIN = 'https://ads-api-sandbox.twitter.com' 20 | 21 | private_constant :DEFAULT_DOMAIN, :SANDBOX_DOMAIN, :HTTP_METHOD 22 | 23 | # Creates a new Request object instance. 24 | # 25 | # @example 26 | # request = Request.new(client, :get, "/#{TwitterAds::API_VERSION}/accounts") 27 | # 28 | # @param client [Client] The Client object instance. 29 | # @param method [Symbol] The HTTP method to be used. 30 | # @param resource [String] The resource path for the request. 31 | # 32 | # @param opts [Hash] An optional Hash of extended options. 33 | # @option opts [String] :domain Forced override for default domain to use for the request. This 34 | # value will also override :sandbox mode on the client. 35 | # 36 | # @since 0.1.0 37 | # 38 | # @return [Request] The Request object instance. 39 | def initialize(client, method, resource, opts = {}) 40 | @client = client 41 | @method = method 42 | @resource = resource 43 | @options = opts 44 | self 45 | end 46 | 47 | # Executes the current Request object. 48 | # 49 | # @example 50 | # request = Request.new(client, :get, "/#{TwitterAds::API_VERSION}/accounts") 51 | # request.perform 52 | # 53 | # @since 0.1.0 54 | # 55 | # @return [Response] The Response object instance generated by the Request. 56 | def perform 57 | handle_error(oauth_request) 58 | end 59 | 60 | private 61 | 62 | def domain 63 | @domain ||= begin 64 | @options[:domain] || (@client.options[:sandbox] ? SANDBOX_DOMAIN : DEFAULT_DOMAIN) 65 | end 66 | end 67 | 68 | def oauth_request 69 | request = http_request 70 | consumer = OAuth::Consumer.new(@client.consumer_key, @client.consumer_secret, site: domain) 71 | token = OAuth::AccessToken.new(consumer, @client.access_token, @client.access_token_secret) 72 | request.oauth!(consumer.http, consumer, token) 73 | 74 | handle_rate_limit = @client.options.fetch(:handle_rate_limit, false) 75 | retry_max = @client.options.fetch(:retry_max, 0) 76 | retry_delay = @client.options.fetch(:retry_delay, 1500) 77 | retry_on_status = @client.options.fetch(:retry_on_status, [500, 503]) 78 | retry_count = 0 79 | retry_after = nil 80 | 81 | write_log(request) if @client.options[:trace] 82 | while retry_count <= retry_max 83 | response = consumer.http.request(request) 84 | status_code = response.code.to_i 85 | break if status_code >= 200 && status_code < 300 86 | 87 | if handle_rate_limit && retry_after.nil? 88 | rate_limit_reset = response.fetch('x-account-rate-limit-reset', nil) || 89 | response.fetch('x-rate-limit-reset', nil) 90 | if status_code == 429 91 | retry_after = rate_limit_reset.to_i - Time.now.to_i 92 | @client.logger.warn('Request reached Rate Limit: resume in %d seconds' % retry_after) 93 | sleep(retry_after + 5) 94 | next 95 | end 96 | end 97 | 98 | if retry_max.positive? 99 | break unless retry_on_status.include?(status_code) 100 | sleep(retry_delay / 1000) 101 | end 102 | 103 | retry_count += 1 104 | end 105 | write_log(response) if @client.options[:trace] 106 | 107 | Response.new(response.code, response.each {}, response.body) 108 | end 109 | 110 | def escape_params(input) 111 | input.map do |key, value| 112 | "#{CGI.escape key.to_s}=#{CGI.escape value.to_s}" 113 | end.join('&') 114 | end 115 | 116 | def http_request 117 | request_url = @resource 118 | 119 | if @options[:params] && !@options[:params].empty? 120 | request_url += "?#{escape_params(@options[:params])}" 121 | end 122 | 123 | request = HTTP_METHOD[@method].new(request_url) 124 | request.body = @options[:body] if @options[:body] 125 | 126 | @options[:headers]&.each { |header, value| request[header] = value } 127 | request['user-agent'] = user_agent 128 | 129 | request 130 | end 131 | 132 | def user_agent 133 | "twitter-ads version: #{TwitterAds::VERSION} " \ 134 | "platform: #{RUBY_ENGINE} #{RUBY_VERSION} (#{RUBY_PLATFORM})" 135 | end 136 | 137 | def write_log(object) 138 | if object.respond_to?(:code) 139 | @client.logger.info("Status: #{object.code} #{object.message}") 140 | else 141 | @client.logger.info("Send: #{object.method} #{domain}#{@resource} #{@options[:params]}") 142 | end 143 | 144 | object.each { |header| @client.logger.info("Header: #{header}: #{object[header]}") } 145 | 146 | # suppresses body content for non-Ads API domains (eg. upload.twitter.com) 147 | unless object.body&.empty? 148 | if @domain == SANDBOX_DOMAIN || @domain == DEFAULT_DOMAIN 149 | @client.logger.info("Body: #{object.body}") 150 | else 151 | @client.logger.info('Body: **OMITTED**') 152 | end 153 | end 154 | end 155 | 156 | def handle_error(response) 157 | raise TwitterAds::Error.from_response(response) unless response.code < 400 158 | response 159 | end 160 | 161 | end 162 | 163 | end 164 | -------------------------------------------------------------------------------- /lib/twitter-ads/http/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | 6 | # Generic container for API responses. 7 | class Response 8 | 9 | attr_reader :code, 10 | :headers, 11 | :raw_body, 12 | :body 13 | 14 | # Creates a new Response object instance. 15 | # 16 | # @example 17 | # response = Response.new(code, headers, body) 18 | # 19 | # @param code [String] The HTTP status code. 20 | # @param headers [Hash] A Hash object containing HTTP response headers. 21 | # @param body [String] The response body. 22 | # 23 | # @since 0.1.0 24 | # 25 | # @return [Response] The Response object instance. 26 | def initialize(code, headers, body) 27 | @code = code.to_i 28 | @headers = headers 29 | @raw_body = body 30 | 31 | # handle non-JSON responses 32 | begin 33 | @body = TwitterAds::Utils.symbolize!(MultiJson.load(body)) 34 | rescue MultiJson::ParseError 35 | @body = raw_body 36 | end 37 | 38 | self 39 | end 40 | 41 | # Returns an inspection string for the current Response instance. 42 | # 43 | # @example 44 | # response.inspect 45 | # 46 | # @since 0.1.0 47 | # 48 | # @return [String] The inspection string. 49 | def inspect 50 | "#<#{self.class.name}:0x#{object_id} code=\"#{@code}\" error=\"#{error?}\">" 51 | end 52 | 53 | # Helper method for determining if the current Response contains an error. 54 | # 55 | # @return [Boolean] True or false indicating if this Response contains an error. 56 | def error? 57 | @error ||= (@code >= 400 && @code <= 599) 58 | end 59 | 60 | end 61 | 62 | end 63 | -------------------------------------------------------------------------------- /lib/twitter-ads/legacy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | # legacy namespace support, to be removed in v1.0.0 (next major) 5 | TwitterAds::Objective = TwitterAds::Enum::Objective 6 | TwitterAds::Product = TwitterAds::Enum::Product 7 | TwitterAds::Placement = TwitterAds::Enum::Placement 8 | -------------------------------------------------------------------------------- /lib/twitter-ads/measurement/app_event_tag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class AppEventTag 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | include TwitterAds::Persistence 10 | 11 | property :id, read_only: true 12 | property :account_id 13 | property :app_store_identifier 14 | property :os_type 15 | property :conversion_type 16 | property :provider_app_event_id 17 | property :provider_app_event_name 18 | property :deleted, read_only: true 19 | property :post_view_attribution_window 20 | property :post_engagement_attribution_window 21 | 22 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 23 | 'accounts/%{account_id}/app_event_tags' # @api private 24 | 25 | def initialize(account) 26 | @account = account 27 | self 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/twitter-ads/measurement/web_event_tag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class WebEventTag 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | include TwitterAds::Persistence 10 | 11 | property :id, read_only: true 12 | property :name 13 | property :retargeting_enabled, type: :bool 14 | property :status, read_only: true 15 | property :type 16 | property :view_through_window 17 | property :deleted, read_only: true 18 | property :embed_code, read_only: true 19 | property :click_window 20 | 21 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 22 | 'accounts/%{account_id}/web_event_tags' # @api private 23 | 24 | def initialize(account) 25 | @account = account 26 | self 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/twitter-ads/resources/batch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Batch 6 | 7 | CLASS_ID_MAP = { 8 | 'TwitterAds::LineItem' => 'LINE_ITEM', 9 | 'TwitterAds::Campaign' => 'CAMPAIGN', 10 | 'TwitterAds::TargetingCriteria' => 'TARGETING_CRITERION' 11 | }.freeze # @api private 12 | 13 | def self.included(klass) 14 | klass.extend ClassMethods 15 | end 16 | 17 | module ClassMethods 18 | 19 | # Makes batch request(s) for a passed in list of objects 20 | # 21 | # @param account [Account] The Account object instance. 22 | # @param objs [Array] A collection of entities to save. 23 | # 24 | # @since 1.1.0 25 | def batch_save(account, objs) 26 | resource = self::RESOURCE_BATCH % { account_id: account.id } 27 | 28 | json_body = [] 29 | 30 | objs.each do |obj| 31 | entity_type = CLASS_ID_MAP[obj.class.name].downcase 32 | obj_params = obj.to_params 33 | obj_json = { 'params' => obj_params } 34 | 35 | if obj.id.nil? 36 | obj_json['operation_type'] = 'Create' 37 | elsif obj.to_delete == true 38 | obj_json['operation_type'] = 'Delete' 39 | obj_json['params'][entity_type + '_id'] = obj.id 40 | else 41 | obj_json['operation_type'] = 'Update' 42 | obj_json['params'][entity_type + '_id'] = obj.id 43 | end 44 | 45 | json_body.push(obj_json) 46 | 47 | end 48 | 49 | headers = { 'Content-Type' => 'application/json' } 50 | response = TwitterAds::Request.new(account.client, 51 | :post, 52 | resource, 53 | headers: headers, 54 | body: json_body.to_json).perform 55 | 56 | # persist each entity 57 | objs.zip(response.body[:data]) { |obj, res_obj| 58 | obj.from_response(res_obj) 59 | } 60 | end 61 | 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/twitter-ads/resources/dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | # A light-wight DSL for building out Twitter Ads API resources. 6 | module DSL 7 | 8 | def self.included(klass) 9 | klass.send :include, InstanceMethods 10 | klass.extend ClassMethods 11 | end 12 | 13 | module InstanceMethods 14 | 15 | # Populates a given objects attributes from a parsed JSON API response. This helper 16 | # handles all necessary type coercions as it assigns attribute values. 17 | # 18 | # @param object [Hash] The parsed JSON response object. 19 | # 20 | # @return [self] A fully hydrated instance of the current class. 21 | # 22 | # @since 0.1.0 23 | def from_response(object, headers = nil) 24 | if !headers.nil? 25 | TwitterAds::Utils.extract_response_headers(headers).each { |key, value| 26 | singleton_class.class_eval { attr_accessor key } 27 | instance_variable_set("@#{key}", value) 28 | } 29 | end 30 | 31 | self.class.properties.each do |name, type| 32 | value = nil 33 | if type == :time && object[name] && !object[name].empty? 34 | value = Time.parse(object[name]) 35 | elsif type == :bool && object[name] 36 | value = TwitterAds::Utils.to_bool(object[name]) 37 | end 38 | instance_variable_set("@#{name}", value || object[name]) 39 | end 40 | self 41 | end 42 | 43 | # Generates a Hash of property values for the current object. This helper 44 | # handles all necessary type coercions as it generates its output. 45 | # 46 | # @return [Hash] A Hash of the object's properties and cooresponding values. 47 | # 48 | # @since 0.1.0 49 | def to_params 50 | params = {} 51 | self.class.properties.each do |name, type| 52 | value = instance_variable_get("@#{name}") || send(name) 53 | next if value.nil? 54 | 55 | # handles unset with empty string 56 | if value.respond_to?(:strip) && value.strip == '' 57 | params[name] = value.strip 58 | next 59 | end 60 | 61 | if type == :time 62 | params[name] = value.iso8601 63 | elsif type == :bool 64 | params[name] = TwitterAds::Utils.to_bool(value) 65 | elsif value.is_a?(Array) 66 | next if value.empty? 67 | params[name] = value.join(',') 68 | else 69 | params[name] = value 70 | end 71 | end 72 | params 73 | end 74 | 75 | end 76 | 77 | module ClassMethods 78 | 79 | # Resource property declaration helper. 80 | # 81 | # @example 82 | # class Foo 83 | # include TwitterAds::DSL 84 | # 85 | # property :foo 86 | # property :bar, type: :bool 87 | # property :created_at, type: time, read_only: true 88 | # end 89 | # 90 | # @param name [Symbol] The name of the property. 91 | # @param opts [Hash] A Hash of extended options to be applied to the property (Optional). 92 | # 93 | # @return [Symbol] The property name. 94 | # 95 | # @since 0.1.0 96 | def property(name, opts = {}) 97 | properties[name] = opts.fetch(:type, nil) 98 | opts[:read_only] ? attr_reader(name) : attr_accessor(name) 99 | name 100 | end 101 | 102 | # Helper for managing properties for the current class. 103 | # 104 | # @return [Hash] A Hash of properties declared in the current class. 105 | # 106 | # @api private 107 | # @since 0.1.0 108 | def properties 109 | @properties ||= {} 110 | end 111 | 112 | end 113 | 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/twitter-ads/resources/persistence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Persistence 6 | 7 | # Saves or updates the current object instance depending on the presence of `object.id`. 8 | # 9 | # @example 10 | # object.save 11 | # 12 | # @return [self] Returns the instance refreshed from the API. 13 | # 14 | # @since 0.1.0 15 | def save 16 | if @id 17 | resource = self.class::RESOURCE % { account_id: account.id, id: id } 18 | response = Request.new(account.client, :put, resource, params: to_params).perform 19 | else 20 | resource = self.class::RESOURCE_COLLECTION % { account_id: account.id } 21 | response = Request.new(account.client, :post, resource, params: to_params).perform 22 | end 23 | from_response(response.body[:data]) 24 | end 25 | 26 | # Deletes the current object instance depending on the presence of `object.id`. 27 | # 28 | # @example 29 | # object.delete! 30 | # 31 | # Note: calls to this method are destructive and irreverisble for most API objects. 32 | # 33 | # @return [self] Returns the instance refreshed from the API. 34 | # 35 | # @since 0.1.0 36 | def delete! 37 | resource = self.class::RESOURCE % { account_id: account.id, id: id } 38 | response = Request.new(account.client, :delete, resource).perform 39 | from_response(response.body[:data]) 40 | end 41 | 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/twitter-ads/resources/resource.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Resource 6 | 7 | def self.included(klass) 8 | klass.send :include, InstanceMethods 9 | klass.extend ClassMethods 10 | end 11 | 12 | module InstanceMethods 13 | 14 | # Reloads all attributes for the current object instance from the API. 15 | # 16 | # @example 17 | # object.reload! 18 | # 19 | # Note: calls to this method dispose of any unsaved data on the object instance. 20 | # 21 | # @param opts [Hash] An optional Hash of extended request options. 22 | # 23 | # @return [self] The reloaded instance of the current object. 24 | # 25 | # @since 0.1.0 26 | def reload!(opts = {}) 27 | return self unless id 28 | params = { with_deleted: true }.merge!(opts) 29 | resource = self.class::RESOURCE % { account_id: account.id, id: id } 30 | response = Request.new(account.client, :get, resource, params: params).perform 31 | from_response(response.body[:data]) 32 | end 33 | 34 | # Returns an inspection string for the current object instance. 35 | # 36 | # @example 37 | # object.inspect 38 | # 39 | # @return [String] The object instance details. 40 | # 41 | # @since 0.1.0 42 | def inspect 43 | str = +"#<#{self.class.name}:0x#{object_id}" 44 | str << " id=\"#{@id}\"" if @id 45 | str << ' deleted="true"' if @deleted 46 | str << '>' 47 | end 48 | 49 | end 50 | 51 | module ClassMethods 52 | 53 | # Returns a Cursor instance for a given resource. 54 | # 55 | # @param account [Account] The Account object instance. 56 | # @param opts [Hash] An optional Hash of extended options. 57 | # @option opts [Boolean] :with_deleted Indicates if deleted items should be included. 58 | # @option opts [String] :sort_by The object param to sort the API response by. 59 | # 60 | # @return [Cursor] A Cusor object ready to iterate through the API response. 61 | # 62 | # @since 0.1.0 63 | # @see Cursor 64 | # @see https://dev.twitter.com/ads/basics/sorting Sorting 65 | def all(account, opts = {}) 66 | resource = self::RESOURCE_COLLECTION % { account_id: account.id } 67 | request = Request.new(account.client, :get, resource, params: opts) 68 | Cursor.new(self, request, init_with: [account]) 69 | end 70 | 71 | # Returns an object instance for a given resource. 72 | # 73 | # @param account [Account] The Account object instance. 74 | # @param id [String] The ID of the specific object to be loaded. 75 | # @param opts [Hash] An optional Hash of extended options. 76 | # @option opts [Boolean] :with_deleted Indicates if deleted items should be included. 77 | # @option opts [String] :sort_by The object param to sort the API response by. 78 | # 79 | # @return [self] The object instance for the specified resource. 80 | # 81 | # @since 0.1.0 82 | def load(account, id, opts = {}) 83 | params = { with_deleted: true }.merge!(opts) 84 | resource = self::RESOURCE % { account_id: account.id, id: id } 85 | response = Request.new(account.client, :get, resource, params: params).perform 86 | new(account).from_response(response.body[:data]) 87 | end 88 | 89 | end 90 | 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/twitter-ads/restapi.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterRestApi 5 | class UserIdLookup 6 | include TwitterAds::DSL 7 | include TwitterAds::Resource 8 | 9 | attr_reader :account 10 | 11 | property :id, read_only: true 12 | property :id_str, read_only: true 13 | property :screen_name, read_only: true 14 | 15 | DOMAIN = 'https://api.twitter.com' 16 | RESOURCE = '/1.1/users/show.json' 17 | 18 | def self.load(account, opts = {}) 19 | response = TwitterAds::Request.new( 20 | account.client, 21 | :get, 22 | RESOURCE, 23 | params: opts, 24 | domain: DOMAIN 25 | ).perform 26 | new.from_response(response.body, response.headers) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/twitter-ads/settings/tax.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class TaxSettings 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | include TwitterAds::Persistence 10 | 11 | attr_reader :account 12 | 13 | property :address_city 14 | property :address_country 15 | property :address_email 16 | property :address_first_name 17 | property :address_last_name 18 | property :address_name 19 | property :address_postal_code 20 | property :address_region 21 | property :address_street1 22 | property :address_street2 23 | property :bill_to 24 | property :business_relationship 25 | property :client_address_city 26 | property :client_address_country 27 | property :client_address_email 28 | property :client_address_first_name 29 | property :client_address_last_name 30 | property :client_address_name 31 | property :client_address_postal_code 32 | property :client_address_region 33 | property :client_address_street1 34 | property :client_address_street2 35 | property :invoice_jurisdiction 36 | property :tax_category 37 | property :tax_exemption_id 38 | property :tax_id 39 | 40 | # sdk only 41 | property :to_delete, type: :bool 42 | 43 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 44 | 'accounts/%{account_id}/tax_settings' # @api private 45 | 46 | def initialize(account) 47 | @account = account 48 | self 49 | end 50 | 51 | def self.load(account) 52 | resource = RESOURCE % { account_id: account.id } 53 | response = Request.new(account.client, :get, resource).perform 54 | new(account).from_response(response.body[:data]) 55 | end 56 | 57 | def save 58 | resource = RESOURCE % { account_id: account.id } 59 | params = to_params 60 | response = Request.new(account.client, :put, resource, params: params).perform 61 | from_response(response.body[:data]) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/twitter-ads/settings/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class UserSettings 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | include TwitterAds::Persistence 10 | 11 | attr_reader :account 12 | 13 | property :notification_email 14 | property :contact_phone 15 | property :contact_phone_extension 16 | property :subscribed_email_types 17 | property :user_id 18 | 19 | # sdk only 20 | property :to_delete, type: :bool 21 | 22 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 23 | 'accounts/%{account_id}/user_settings/%{id}' # @api private 24 | 25 | def initialize(account) 26 | @account = account 27 | self 28 | end 29 | 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting/audience_estimate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module AudienceEstimate 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | RESOURCE = "/#{TwitterAds::API_VERSION}/" \ 11 | 'accounts/%{account_id}/audience_estimate' 12 | 13 | property :audience_size, read_only: true 14 | 15 | class << self 16 | 17 | # Get an audience summary for the specified targeting criteria. 18 | # 19 | # @example 20 | # TwitterAds::AudienceEstimate.fetch( 21 | # account, 22 | # params: {targeting_criteria:[{targeting_type:'LOCATION', 23 | # targeting_value:'96683cc9126741d1'}]} 24 | # ) 25 | # 26 | # @param params [Hash] A hash of input targeting criteria values 27 | # 28 | # @return [Hash] A hash containing the min and max audience size. 29 | # 30 | # @since 7.0.0 31 | # @see https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/audience-summary 32 | def fetch(account, params) 33 | resource = RESOURCE % { account_id: account.id } 34 | headers = { 'Content-Type' => 'application/json' } 35 | 36 | response = TwitterAds::Request.new(account.client, 37 | :post, 38 | resource, 39 | headers: headers, 40 | body: params.to_json).perform 41 | response.body[:data] 42 | end 43 | 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/app_store_category.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class AppStoreCategory 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :name, read_only: true 11 | property :os_type, read_only: true 12 | property :targeting_type, read_only: true 13 | property :targeting_value, read_only: true 14 | 15 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 16 | 'targeting_criteria/app_store_categories' # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/conversation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Conversation 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :name, read_only: true 11 | property :targeting_type, read_only: true 12 | property :targeting_value, read_only: true 13 | property :conversation_type, read_only: true 14 | 15 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 16 | 'targeting_criteria/conversations' # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/device.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Device 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :id, read_only: true 11 | property :name, read_only: true 12 | property :targeting_type, read_only: true 13 | property :targeting_value, read_only: true 14 | property :platform, read_only: true 15 | property :manufacturer, read_only: true 16 | 17 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 18 | 'targeting_criteria/devices' # @api private 19 | 20 | def initialize(account) 21 | @account = account 22 | self 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Event 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :id, read_only: true 11 | property :name, read_only: true 12 | property :reach, read_only: true 13 | property :start_time, read_only: true 14 | property :end_time, read_only: true 15 | property :top_users, read_only: true 16 | property :top_tweets, read_only: true 17 | property :top_hashtags, read_only: true 18 | property :country_code, read_only: true 19 | property :is_global, read_only: true 20 | property :category, read_only: true 21 | property :gender_breakdown_percentage, read_only: true 22 | property :device_breakdown_percentage, read_only: true 23 | property :country_breakdown_percentage, read_only: true 24 | property :targeting_value, read_only: true 25 | 26 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 27 | 'targeting_criteria/events' # @api private 28 | 29 | def initialize(account) 30 | @account = account 31 | self 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/interest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Interest 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :name, read_only: true 11 | property :targeting_type, read_only: true 12 | property :targeting_value, read_only: true 13 | property :localized_name, read_only: true 14 | 15 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 16 | 'targeting_criteria/interests' # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/language.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Language 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :name, read_only: true 11 | property :targeting_type, read_only: true 12 | property :targeting_value, read_only: true 13 | 14 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 15 | 'targeting_criteria/languages' # @api private 16 | 17 | def initialize(account) 18 | @account = account 19 | self 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/location.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Location 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :name, read_only: true 11 | property :targeting_type, read_only: true 12 | property :targeting_value, read_only: true 13 | property :location_type, read_only: true 14 | 15 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 16 | 'targeting_criteria/locations' # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/network_operator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class NetworkOperator 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :name, read_only: true 11 | property :targeting_type, read_only: true 12 | property :targeting_value, read_only: true 13 | property :country_code, read_only: true 14 | 15 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 16 | 'targeting_criteria/network_operators' # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/platform.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class Platform 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :name, read_only: true 11 | property :targeting_type, read_only: true 12 | property :targeting_value, read_only: true 13 | property :localized_name, read_only: true 14 | 15 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 16 | 'targeting_criteria/platforms' # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/platform_version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class PlatformVersion 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :name, read_only: true 11 | property :targeting_type, read_only: true 12 | property :targeting_value, read_only: true 13 | property :number, read_only: true 14 | property :platform, read_only: true 15 | 16 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 17 | 'targeting_criteria/platform_versions' # @api private 18 | 19 | def initialize(account) 20 | @account = account 21 | self 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/tv_market.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class TVMarket 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :id, read_only: true 11 | property :name, read_only: true 12 | property :locale, read_only: true 13 | property :country_code, read_only: true 14 | 15 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 16 | 'targeting_criteria/tv_markets' # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/targeting_criteria/tv_show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | class TVShow 6 | 7 | include TwitterAds::DSL 8 | include TwitterAds::Resource 9 | 10 | property :id, read_only: true 11 | property :name, read_only: true 12 | property :estimated_users, read_only: true 13 | property :genre, read_only: true 14 | 15 | RESOURCE_COLLECTION = "/#{TwitterAds::API_VERSION}/" \ 16 | 'targeting_criteria/tv_shows' # @api private 17 | 18 | def initialize(account) 19 | @account = account 20 | self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter-ads/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | module Utils 6 | 7 | class << self 8 | 9 | # Helper to convert objects into boolean values. 10 | # 11 | # @param object [Object] The object to be converted. 12 | # 13 | # @return [Boolean] The boolean result. 14 | # 15 | # @api private 16 | # @since 0.1.0 17 | def to_bool(object) 18 | (object.to_s.downcase == 'false') ? false : !!object 19 | end 20 | 21 | # Helper to convert a time object according to a given granularity level. 22 | # 23 | # @param time [Time] A valid Time instance. 24 | # @param granularity [Symbol] A symbol representing the desired granuarlity (eg. :hour). 25 | # 26 | # @return [Time] The formatted Time instance. 27 | # 28 | # @api private 29 | # @since 0.1.0 30 | def to_time(time, granularity = nil, utc_offset = nil) 31 | return time.iso8601 unless granularity 32 | if granularity == :hour 33 | Time.new(time.year, time.month, time.day, time.hour, 0, 0, utc_offset).iso8601 34 | elsif granularity == :day 35 | Time.new(time.year, time.month, time.day, 0, 0, 0, utc_offset).iso8601 36 | else 37 | time.iso8601 38 | end 39 | end 40 | 41 | # Converts key names to symbols on a given object. 42 | # 43 | # @param object [Object] The object to be converted. 44 | # 45 | # @return [Object] The symbolized, converted object. 46 | # 47 | # @api private 48 | # @since 0.1.0 49 | def symbolize!(object) 50 | if object.is_a?(Array) 51 | object.each_with_index { |value, index| object[index] = symbolize!(value) } 52 | elsif object.is_a?(Hash) 53 | object.keys.each { |key| object[key.to_sym] = symbolize!(object.delete(key)) } 54 | end 55 | object 56 | end 57 | 58 | # Creates a deprecation message. 59 | # 60 | # @param name [String] The name of the object or method being deprecated. 61 | # @param replacement [String] The name of the new object or method (optional). 62 | # @param refer [String] HTTP address with supporting information (optional). 63 | # 64 | # @api private 65 | # @since 0.3.2 66 | def deprecated(name, opts = {}) 67 | message = +"[DEPRECATED] #{name} has been deprecated" 68 | message += opts[:replacement] ? " (please use #{opts[:replacement]})." : '.' 69 | message += " Please see #{opts[:refer]} for more info." if opts[:refer] 70 | warn message 71 | end 72 | 73 | def extract_response_headers(headers) 74 | values = {} 75 | # only get "X-${name}" custom response headers 76 | headers.each { |key, value| 77 | if key =~ /^x-/ 78 | values[key.gsub(/^x-/, '').tr('-', '_')] = \ 79 | value.first =~ /^[0-9]*$/ ? value.first.to_i : value.first 80 | end 81 | } 82 | values 83 | end 84 | 85 | def flatten_params(args) 86 | params = args 87 | params.each { |key, value| 88 | if value.is_a?(Array) 89 | next if value.empty? 90 | params[key] = value.join(',') 91 | end 92 | } 93 | params 94 | end 95 | 96 | end 97 | 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/twitter-ads/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (C) 2019 Twitter, Inc. 3 | 4 | module TwitterAds 5 | VERSION = '11.0.0' 6 | end 7 | -------------------------------------------------------------------------------- /public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDkjCCAnqgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMRcwFQYDVQQDDA50d2l0 3 | dGVyZGV2LWFkczEXMBUGCgmSJomT8ixkARkWB3R3aXR0ZXIxEzARBgoJkiaJk/Is 4 | ZAEZFgNjb20wHhcNMTcwMTEyMDIwNTIxWhcNMTgwMTEyMDIwNTIxWjBHMRcwFQYD 5 | VQQDDA50d2l0dGVyZGV2LWFkczEXMBUGCgmSJomT8ixkARkWB3R3aXR0ZXIxEzAR 6 | BgoJkiaJk/IsZAEZFgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 7 | AQCohThVTk44k9/nFQyBlZBpf6fxtqxtJkSUPQjBLyiXgNwtbsK1XVX7isPCe48P 8 | EfLyXhflaNdAxu4Ho26/Zdt3S2qyfP1hAWVmwJKTqVsE1bUnSTPcWs1LYWeyIkW2 9 | 5GRAndY6aQn9zncEVu/Ri8umA8NsIUL3WBVW1wcR28e61Fq1Js8Vqeui5n8C+sh3 10 | lce3vrlY1UtBeGmGGA5rQQTiGzN3FCUdjHo8bqq7AWw5jIwdRA3rOpT7zj25ppc9 11 | 7IacnOz8U7lJ/xjDwnZqkzQCPESSIWWUqhhaQY4c3cfq7nxHDVnxtN9DA7uzOLKx 12 | 8cM8dbobNw3Of/TTs1CG/dKTAgMBAAGjgYgwgYUwCQYDVR0TBAIwADALBgNVHQ8E 13 | BAMCBLAwHQYDVR0OBBYEFF/89vydkzOdlfrSj279rAJq5UNZMCUGA1UdEQQeMByB 14 | GnR3aXR0ZXJkZXYtYWRzQHR3aXR0ZXIuY29tMCUGA1UdEgQeMByBGnR3aXR0ZXJk 15 | ZXYtYWRzQHR3aXR0ZXIuY29tMA0GCSqGSIb3DQEBBQUAA4IBAQClOgtZ70c9sj73 16 | X3I0lXKHxTv+Pwvd3dRp7NXJzPjmq93muuj5uyHsi5gbQdHul8pRka3i5qxZodNn 17 | vWguk67iWN7m4pIqEtc456JflY2H5UWnChLHCmZ5MqrKNJh9us8cClyKn3/rBqnV 18 | uu/+mfbfpqDp2MLLKFo77cFNXpzKl0JmKyxtBJlWCK6PeTqU3IvWIvQ9R1OfyMN0 19 | B0nhMKYDZajZXhtjz/tyeakKeyVZV/99hDa63V+08xClwMLVHNoc1+no48kXRog5 20 | ouZXSlxLCCseakgV8oMPuyzeHOo/xRmPR+aePyLRG8makXFDQIVfHE2YdLuARcEy 21 | rpYQ/nux 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /spec/fixtures/accounts_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": {} 4 | }, 5 | "data": [ 6 | { 7 | "name": "Schuppe-Casper", 8 | "timezone": "America/Los_Angeles", 9 | "timezone_switch_at": "2014-11-17T08:00:00Z", 10 | "id": "2iqph", 11 | "created_at": "2015-03-04T10:50:42Z", 12 | "updated_at": "2015-04-11T05:20:08Z", 13 | "approval_status": "ACCEPTED", 14 | "deleted": false 15 | }, 16 | { 17 | "name": "Schuppe-Casper", 18 | "timezone": "America/Los_Angeles", 19 | "timezone_switch_at": "2014-11-17T08:00:00Z", 20 | "id": "pz6ec", 21 | "created_at": "2015-05-29T00:52:16Z", 22 | "updated_at": "2015-05-29T00:52:16Z", 23 | "approval_status": "ACCEPTED", 24 | "deleted": false 25 | }, 26 | { 27 | "name": "Kozey-Farrell", 28 | "timezone": "America/Los_Angeles", 29 | "timezone_switch_at": "2014-11-17T08:00:00Z", 30 | "id": "j9ozo", 31 | "created_at": "2015-05-01T12:08:10Z", 32 | "updated_at": "2015-05-01T12:08:10Z", 33 | "approval_status": "ACCEPTED", 34 | "deleted": false 35 | }, 36 | { 37 | "name": "Osinski, Quitzon and Hilll", 38 | "timezone": "America/Los_Angeles", 39 | "timezone_switch_at": "2014-11-17T08:00:00Z", 40 | "id": "9ttgd", 41 | "created_at": "2015-06-24T18:51:20Z", 42 | "updated_at": "2015-06-26T06:13:24Z", 43 | "approval_status": "ACCEPTED", 44 | "deleted": false 45 | }, 46 | { 47 | "name": "Jakubowski-Aufderhar", 48 | "timezone": "America/Los_Angeles", 49 | "timezone_switch_at": "2013-05-22T07:00:00Z", 50 | "id": "47d0v", 51 | "created_at": "2015-05-28T05:42:03Z", 52 | "updated_at": "2015-05-28T05:42:03Z", 53 | "approval_status": "ACCEPTED", 54 | "deleted": false 55 | } 56 | ], 57 | "data_type": "account", 58 | "total_count": 5, 59 | "next_cursor": null 60 | } 61 | -------------------------------------------------------------------------------- /spec/fixtures/accounts_features.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type": "features", 3 | "data": [ 4 | "CPI_CHARGING", 5 | "EVENT_TARGETING", 6 | "INSTALLED_APP_CATEGORY_TARGETING", 7 | "MOBILE_CONVERSION_TRANSACTION_VALUE", 8 | "OPTIMIZED_ACTION_BIDDING", 9 | "OPTIMIZED_WEBSITE_CONVERSIONS", 10 | "VIDEO_VIEWS_OBJECTIVE", 11 | "VIDEO_APP_DOWNLOAD_CARD" 12 | ], 13 | "request": { 14 | "params": { 15 | "account_id": "2iqph" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /spec/fixtures/accounts_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type": "account", 3 | "data": { 4 | "name": "Schuppe-Casper", 5 | "timezone": "America/Los_Angeles", 6 | "timezone_switch_at": "2014-11-17T08:00:00Z", 7 | "id": "2iqph", 8 | "created_at": "2015-03-04T10:50:42Z", 9 | "updated_at": "2015-04-11T05:20:08Z", 10 | "approval_status": "ACCEPTED", 11 | "deleted": false 12 | }, 13 | "request": { 14 | "params": { 15 | "account_id": "2iqph" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /spec/fixtures/app_lists_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "request" : { 3 | "params" : { 4 | "account_id" : "2iqph" 5 | } 6 | }, 7 | "data_type" : "app_list", 8 | "data" : [ 9 | { 10 | "name" : "Some test app list", 11 | "id" : "abc2" 12 | }, 13 | { 14 | "name" : "Yet another app list", 15 | "id" : "wdpr" 16 | }, 17 | { 18 | "name" : "The best app list yet", 19 | "id" : "wdps" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /spec/fixtures/app_lists_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type" : "app_list", 3 | "data" : { 4 | "apps" : [ 5 | { 6 | "os_type" : "Android", 7 | "app_store_identifier" : "com.supercell.clashofclans" 8 | }, 9 | { 10 | "app_store_identifier" : "com.functionx.viggle", 11 | "os_type" : "Android" 12 | }, 13 | { 14 | "app_store_identifier" : "io.fabric.samples.cannonball", 15 | "os_type" : "Android" 16 | }, 17 | { 18 | "os_type" : "Android", 19 | "app_store_identifier" : "com.hoteltonight.android.prod" 20 | } 21 | ], 22 | "name" : "Some test app list", 23 | "id" : "abc2" 24 | }, 25 | "request" : { 26 | "params" : { 27 | "account_id" : "2iqph", 28 | "app_list_id" : "abc2" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spec/fixtures/audience_estimate.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": { 4 | "targeting_criteria": null, 5 | "account_id": "2iqph" 6 | } 7 | }, 8 | "data": { 9 | "audience_size": { 10 | "min": 41133600, 11 | "max": 50274400 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /spec/fixtures/campaigns_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type": "campaign", 3 | "data": { 4 | "name": "Intelligent Granite Computer", 5 | "start_time": "2015-08-12T20:26:23Z", 6 | "reasons_not_servable": [], 7 | "servable": true, 8 | "daily_budget_amount_local_micro": 1000000, 9 | "end_time": null, 10 | "funding_instrument_id": "5aa0p", 11 | "standard_delivery": true, 12 | "total_budget_amount_local_micro": null, 13 | "id": "2wap7", 14 | "entity_status": "ACTIVE", 15 | "account_id": "2iqph", 16 | "currency": "USD", 17 | "created_at": "2015-08-12T20:26:24Z", 18 | "updated_at": "2015-08-12T20:26:24Z", 19 | "deleted": false 20 | }, 21 | "request": { 22 | "params": { 23 | "campaign_id": "2wap7", 24 | "account_id": "2iqph" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/fixtures/cards_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": { 4 | "account_id": "2iqph", 5 | "card_id": "1503831318555086849" 6 | } 7 | }, 8 | "data": { 9 | "name": "my new card", 10 | "components": [ 11 | { 12 | "media_key": "13_794652834998325248", 13 | "media_metadata": { 14 | "13_794652834998325248": { 15 | "type": "VIDEO", 16 | "url": "https://video.twimg.com/amplify_video/794652834998325248/vid/640x360/pUgE2UKcfPwF_5Uh.mp4", 17 | "width": 640, 18 | "height": 360, 19 | "video_duration": 7967, 20 | "video_aspect_ratio": "16:9" 21 | } 22 | }, 23 | "type": "MEDIA" 24 | }, 25 | { 26 | "title": "Twitter", 27 | "destination": { 28 | "url": "http://twitter.com/login", 29 | "type": "WEBSITE" 30 | }, 31 | "type": "DETAILS" 32 | } 33 | ], 34 | "id": "1503831318555086849", 35 | "created_at": "2022-03-15T20:31:40Z", 36 | "card_uri": "card://1503831318555086849", 37 | "updated_at": "2022-03-15T20:31:40Z", 38 | "deleted": false, 39 | "card_type": "VIDEO_WEBSITE" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spec/fixtures/custom_audiences_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": { 4 | "account_id": "2iqph" 5 | } 6 | }, 7 | "data": [ 8 | { 9 | "targetable": false, 10 | "name": "TA #2", 11 | "targetable_types": [ 12 | "WEB", 13 | "EXCLUDED_WEB" 14 | ], 15 | "audience_type": "WEB", 16 | "id": "abc2", 17 | "reasons_not_targetable": [ 18 | "TOO_SMALL" 19 | ], 20 | "list_type": null, 21 | "created_at": "2014-03-09T20:35:41Z", 22 | "updated_at": "2014-06-11T09:38:06Z", 23 | "partner_source": "OTHER", 24 | "deleted": false, 25 | "audience_size": null 26 | }, 27 | { 28 | "targetable": true, 29 | "name": "TA #1", 30 | "targetable_types": [ 31 | "CRM", 32 | "EXCLUDED_CRM" 33 | ], 34 | "audience_type": "CRM", 35 | "id": "abc1", 36 | "reasons_not_targetable": [], 37 | "list_type": "DEVICE_ID", 38 | "created_at": "2014-05-22T17:37:12Z", 39 | "updated_at": "2014-05-22T21:05:33Z", 40 | "partner_source": "OTHER", 41 | "deleted": false, 42 | "audience_size": null 43 | }, 44 | { 45 | "targetable": false, 46 | "name": "TA #3", 47 | "targetable_types": [ 48 | "CRM", 49 | "EXCLUDED_CRM" 50 | ], 51 | "audience_type": "CRM", 52 | "id": "abc3", 53 | "reasons_not_targetable": [ 54 | "TOO_SMALL" 55 | ], 56 | "list_type": "EMAIL", 57 | "created_at": "2014-05-22T21:43:45Z", 58 | "updated_at": "2014-05-23T02:27:31Z", 59 | "partner_source": "OTHER", 60 | "deleted": false, 61 | "audience_size": null 62 | } 63 | ], 64 | "data_type": "tailored_audiences", 65 | "total_count": 3, 66 | "next_cursor": null 67 | } 68 | -------------------------------------------------------------------------------- /spec/fixtures/custom_audiences_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type": "tailored_audiences", 3 | "data": { 4 | "targetable": false, 5 | "name": "TA #2", 6 | "targetable_types": [ 7 | "WEB", 8 | "EXCLUDED_WEB" 9 | ], 10 | "audience_type": "WEB", 11 | "id": "abc2", 12 | "reasons_not_targetable": [ 13 | "TOO_SMALL" 14 | ], 15 | "list_type": null, 16 | "created_at": "2014-03-09T20:35:41Z", 17 | "updated_at": "2014-06-11T09:38:06Z", 18 | "partner_source": "OTHER", 19 | "deleted": false, 20 | "audience_size": null 21 | }, 22 | "request": { 23 | "params": { 24 | "account_id": "2iqph", 25 | "name": "TA #2", 26 | "list_type": "EMAIL" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/funding_instruments_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": { 4 | "funding_instrument_ids": [ 5 | "7cdql", "5aa0p", "dhuk2" 6 | ], 7 | "account_id": "2iqph" 8 | } 9 | }, 10 | "data": [ 11 | { 12 | "start_time": "2015-08-25T09:18:19Z", 13 | "description": "Denesik, O'Reilly and Zulauf", 14 | "credit_limit_local_micro": null, 15 | "end_time": "2017-02-28T08:15:40Z", 16 | "id": "5aa0p", 17 | "entity_status": "ACTIVE", 18 | "account_id": "2iqph", 19 | "reasons_not_able_to_fund": [], 20 | "io_header": null, 21 | "currency": "USD", 22 | "funded_amount_local_micro": 5000000000, 23 | "created_at": "2015-08-19T01:35:10Z", 24 | "type": "INSERTION_ORDER", 25 | "able_to_fund": false, 26 | "updated_at": "2015-08-19T01:35:10Z", 27 | "credit_remaining_local_micro": null, 28 | "deleted": false 29 | }, 30 | { 31 | "start_time": "2011-07-11T07:00:00Z", 32 | "description": "Simonis-Barton", 33 | "credit_limit_local_micro": null, 34 | "end_time": "2012-07-12T06:59:59Z", 35 | "id": "7cdql", 36 | "entity_status": "ACTIVE", 37 | "account_id": "2iqph", 38 | "reasons_not_able_to_fund": [ 39 | "EXPIRED" 40 | ], 41 | "io_header": null, 42 | "currency": "USD", 43 | "funded_amount_local_micro": 5000000000, 44 | "created_at": "2011-07-11T17:33:01Z", 45 | "type": "INSERTION_ORDER", 46 | "able_to_fund": false, 47 | "updated_at": "2012-01-18T19:19:56Z", 48 | "credit_remaining_local_micro": null, 49 | "deleted": false 50 | }, 51 | { 52 | "start_time": "2015-08-25T09:18:19Z", 53 | "description": "Farrell Group", 54 | "credit_limit_local_micro": 5000000000, 55 | "end_time": "2017-02-28T08:15:40Z", 56 | "id": "dhuk2", 57 | "entity_status": "ACTIVE", 58 | "account_id": "2iqph", 59 | "reasons_not_able_to_fund": [], 60 | "io_header": null, 61 | "currency": "USD", 62 | "funded_amount_local_micro": null, 63 | "created_at": "2015-08-19T01:35:10Z", 64 | "type": "CREDIT_LINE", 65 | "able_to_fund": false, 66 | "updated_at": "2015-08-19T01:35:10Z", 67 | "credit_remaining_local_micro": null, 68 | "deleted": false 69 | } 70 | ], 71 | "data_type": "funding_instrument", 72 | "total_count": 3, 73 | "next_cursor": null 74 | } 75 | -------------------------------------------------------------------------------- /spec/fixtures/funding_instruments_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type": "funding_instrument", 3 | "data": { 4 | "start_time": "2015-08-25T09:18:19Z", 5 | "description": "Denesik, O'Reilly and Zulauf", 6 | "credit_limit_local_micro": null, 7 | "end_time": "2017-02-28T08:15:40Z", 8 | "id": "5aa0p", 9 | "entity_status": "ACTIVE", 10 | "account_id": "2iqph", 11 | "reasons_not_able_to_fund": [], 12 | "io_header": null, 13 | "currency": "USD", 14 | "funded_amount_local_micro": 5000000000, 15 | "created_at": "2015-08-19T01:35:10Z", 16 | "type": "INSERTION_ORDER", 17 | "able_to_fund": false, 18 | "updated_at": "2015-08-19T01:35:10Z", 19 | "credit_remaining_local_micro": null, 20 | "deleted": false 21 | }, 22 | "request": { 23 | "params": { 24 | "funding_instrument_id": "5aa0p", 25 | "account_id": "2iqph" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spec/fixtures/line_items_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type": "line_item", 3 | "data": { 4 | "placement_type": "PROMOTED_ACCOUNT", 5 | "bid_type": "MAX", 6 | "name": "Untitled", 7 | "placements": [ 8 | "ALL_ON_TWITTER" 9 | ], 10 | "bid_amount_local_micro": 2000000, 11 | "advertiser_domain": null, 12 | "primary_web_event_tag": null, 13 | "charge_by": "ENGAGEMENT", 14 | "product_type": "PROMOTED_ACCOUNT", 15 | "bid_unit": "ENGAGEMENT", 16 | "total_budget_amount_local_micro": null, 17 | "objective": "CUSTOM", 18 | "id": "bw2", 19 | "entity_status": "ACTIVE", 20 | "account_id": "2iqph", 21 | "optimization": "DEFAULT", 22 | "categories": [], 23 | "currency": "USD", 24 | "created_at": "2011-07-11T20:36:11Z", 25 | "updated_at": "2011-09-04T19:39:51Z", 26 | "campaign_id": "2wap7", 27 | "deleted": false 28 | }, 29 | "request": { 30 | "params": { 31 | "account_id": "2iqph" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /spec/fixtures/no_content.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/twitter-ruby-ads-sdk/0b6759a21236617ba5c4f39178e7a73fc1a4ae4e/spec/fixtures/no_content.json -------------------------------------------------------------------------------- /spec/fixtures/placements.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type": "placement", 3 | "data": [ 4 | { 5 | "product_type": "PROMOTED_TWEETS", 6 | "placements": [ 7 | [ 8 | "ALL_ON_TWITTER" 9 | ], 10 | [ 11 | "ALL_ON_TWITTER", 12 | "PUBLISHER_NETWORK" 13 | ], 14 | [ 15 | "PUBLISHER_NETWORK" 16 | ], 17 | [ 18 | "PUBLISHER_NETWORK", 19 | "TWITTER_TIMELINE" 20 | ], 21 | [ 22 | "TWITTER_SEARCH" 23 | ], 24 | [ 25 | "TWITTER_TIMELINE" 26 | ] 27 | ] 28 | } 29 | ], 30 | "request": { 31 | "params": { 32 | "product_type": "PROMOTED_TWEETS" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spec/fixtures/promotable_users_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": { 4 | "account_id": "2iqph" 5 | } 6 | }, 7 | "data": [ 8 | { 9 | "user_id": "330677333", 10 | "id": "4k", 11 | "account_id": "2iqph", 12 | "created_at": "2011-11-14T21:26:54Z", 13 | "updated_at": "2014-07-30T23:49:23Z", 14 | "deleted": false, 15 | "promotable_user_type": "FULL" 16 | }, 17 | { 18 | "user_id": "154303893", 19 | "id": "5ze", 20 | "account_id": "2iqph", 21 | "created_at": "2012-04-23T16:02:08Z", 22 | "updated_at": "2014-05-20T19:07:17Z", 23 | "deleted": false, 24 | "promotable_user_type": "RETWEETS_ONLY" 25 | }, 26 | { 27 | "user_id": "16088304", 28 | "id": "2jbq3", 29 | "account_id": "2iqph", 30 | "created_at": "2013-08-21T10:31:01Z", 31 | "updated_at": "2014-05-20T12:35:01Z", 32 | "deleted": false, 33 | "promotable_user_type": "RETWEETS_ONLY" 34 | }, 35 | { 36 | "user_id": "14216557", 37 | "id": "2jlym", 38 | "account_id": "2iqph", 39 | "created_at": "2013-09-04T22:36:24Z", 40 | "updated_at": "2014-05-20T20:09:11Z", 41 | "deleted": false, 42 | "promotable_user_type": "RETWEETS_ONLY" 43 | }, 44 | { 45 | "user_id": "312226591", 46 | "id": "2kuyo", 47 | "account_id": "2iqph", 48 | "created_at": "2013-09-12T22:59:10Z", 49 | "updated_at": "2014-05-22T14:36:22Z", 50 | "deleted": false, 51 | "promotable_user_type": "RETWEETS_ONLY" 52 | } 53 | ], 54 | "data_type": "promotable_user", 55 | "total_count": 5, 56 | "next_cursor": null 57 | } 58 | -------------------------------------------------------------------------------- /spec/fixtures/promotable_users_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": { 4 | "promotable_user_id": "4k", 5 | "account_id": "2iqph" 6 | } 7 | }, 8 | "data_type": "promotable_user", 9 | "data": { 10 | "user_id": "330677333", 11 | "id": "4k", 12 | "account_id": "2iqph", 13 | "created_at": "2011-11-14T21:26:54Z", 14 | "updated_at": "2014-07-30T23:49:23Z", 15 | "deleted": false, 16 | "promotable_user_type": "FULL" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /spec/fixtures/promoted_tweets_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type": "promoted_tweet", 3 | "data": { 4 | "line_item_id": "2b7xw", 5 | "id": "6thl4", 6 | "entity_status": "ACTIVE", 7 | "created_at": "2015-04-11T20:50:25Z", 8 | "updated_at": "2015-04-11T20:50:25Z", 9 | "approval_status": "ACCEPTED", 10 | "tweet_id": "585127452231467008", 11 | "deleted": false 12 | }, 13 | "request": { 14 | "params": { 15 | "promoted_tweet_id": "6thl4", 16 | "account_id": "2iqph" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/fixtures/reach_estimate.json: -------------------------------------------------------------------------------- 1 | { 2 | "data" : { 3 | "count" : 1617516, 4 | "infinite_bid_count" : 1840178 5 | }, 6 | "data_type" : "reach_estimate", 7 | "request" : { 8 | "params" : { 9 | "objective" : "WEBSITE_CLICKS", 10 | "gender" : "FEMALE", 11 | "followers_of_users" : null, 12 | "product_type" : "PROMOTED_TWEETS", 13 | "similar_to_followers_of_users" : [ 14 | 2153688540 15 | ], 16 | "account_id" : "2iqph" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/fixtures/tailored_audiences_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": { 4 | "account_id": "2iqph" 5 | } 6 | }, 7 | "data": [ 8 | { 9 | "targetable": false, 10 | "name": "TA #2", 11 | "targetable_types": [ 12 | "WEB", 13 | "EXCLUDED_WEB" 14 | ], 15 | "audience_type": "WEB", 16 | "id": "abc2", 17 | "owner_account_id": "18ce54uhdu0", 18 | "reasons_not_targetable": [ 19 | "TOO_SMALL" 20 | ], 21 | "list_type": null, 22 | "created_at": "2014-03-09T20:35:41Z", 23 | "updated_at": "2014-06-11T09:38:06Z", 24 | "partner_source": "OTHER", 25 | "deleted": false, 26 | "audience_size": null 27 | }, 28 | { 29 | "targetable": true, 30 | "name": "TA #1", 31 | "targetable_types": [ 32 | "CRM", 33 | "EXCLUDED_CRM" 34 | ], 35 | "audience_type": "CRM", 36 | "id": "abc1", 37 | "owner_account_id": "18ce54uhdu0", 38 | "reasons_not_targetable": [], 39 | "list_type": "DEVICE_ID", 40 | "created_at": "2014-05-22T17:37:12Z", 41 | "updated_at": "2014-05-22T21:05:33Z", 42 | "partner_source": "OTHER", 43 | "deleted": false, 44 | "audience_size": null 45 | }, 46 | { 47 | "targetable": false, 48 | "name": "TA #3", 49 | "targetable_types": [ 50 | "CRM", 51 | "EXCLUDED_CRM" 52 | ], 53 | "audience_type": "CRM", 54 | "id": "abc3", 55 | "owner_account_id": "18ce54uhdu0", 56 | "reasons_not_targetable": [ 57 | "TOO_SMALL" 58 | ], 59 | "list_type": "EMAIL", 60 | "created_at": "2014-05-22T21:43:45Z", 61 | "updated_at": "2014-05-23T02:27:31Z", 62 | "partner_source": "OTHER", 63 | "deleted": false, 64 | "audience_size": null 65 | } 66 | ], 67 | "data_type": "tailored_audiences", 68 | "total_count": 3, 69 | "next_cursor": null 70 | } 71 | -------------------------------------------------------------------------------- /spec/fixtures/targeted_audiences.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": { 4 | "account_id": "2iqph", 5 | "tailored_audience_id": "abc2" 6 | } 7 | }, 8 | "next_cursor": null, 9 | "data": [ 10 | { 11 | "campaign_id": "59hod", 12 | "campaign_name": "test-campaign", 13 | "line_items": [ 14 | { 15 | "id": "5gzog", 16 | "name": "test-line-item", 17 | "servable": true 18 | } 19 | ] 20 | }, 21 | { 22 | "campaign_id": "arja7", 23 | "campaign_name": "Untitled campaign", 24 | "line_items": [ 25 | { 26 | "id": "bjw1q", 27 | "name": null, 28 | "servable": true 29 | } 30 | ] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /spec/fixtures/tracking_tags_load.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "params": { 4 | "tracking_tag_id": "7035", 5 | "account_id": "18ce54uhdu0" 6 | } 7 | }, 8 | "data": { 9 | "line_item_id": "dmbc0", 10 | "tracking_tag_url": "https://ad.doubleclick.net/ddm/trackimp/N1234.2061500TWITTER-OFFICIAL/B9156151.125630439;dc_trk_aid=1355;dc_trk_cid=8675309", 11 | "tracking_tag_type": "IMPRESSION_TAG", 12 | "id": "7035", 13 | "created_at": "2021-11-16T00:12:26Z", 14 | "updated_at": "2021-11-16T00:12:26Z", 15 | "deleted": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /spec/fixtures/tweet_previews.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_type": "tweet_previews", 3 | "request": { 4 | "params": { 5 | "tweet_ids": [ 6 | "1130942781109596160", 7 | "1101254234031370240" 8 | ], 9 | "tweet_type": "PUBLISHED", 10 | "account_id": "2iqph" 11 | } 12 | }, 13 | "data": [ 14 | { 15 | "tweet_id": "1130942781109596160", 16 | "preview": "