├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── critical-bug-report.md ├── dependabot.yml └── workflows │ ├── chatops.yml │ ├── pull_request.yml │ ├── push.yml │ └── test.yml ├── .gitignore ├── .swiftlint.yml ├── Examples ├── Authenticator │ ├── Authenticator.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Authenticator.xcscheme │ └── Authenticator │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── ItunesArtwork@2x.png │ │ ├── Contents.json │ │ └── header.imageset │ │ │ ├── Contents.json │ │ │ └── header.png │ │ ├── AuthenticatorApp.swift │ │ ├── ContentView.swift │ │ ├── Info.plist │ │ ├── LoginView.swift │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ ├── Secret.swift │ │ └── Text.swift └── Followers │ ├── Followers.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Followers.xcscheme │ └── Followers │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── ItunesArtwork@2x.png │ ├── Contents.json │ └── placeholder.imageset │ │ ├── 44884218_345707102882519_2446069589734326272_n.jpg │ │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── ContentView.swift │ ├── FollowersView.swift │ ├── Info.plist │ ├── LoginView.swift │ ├── Models │ └── FollowersModel.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── Reusables │ ├── AvatarButton.swift │ ├── RemoteImage.swift │ └── UserCell.swift │ └── SceneDelegate.swift ├── LICENSE ├── Package.swift ├── Resources ├── header.png ├── landscape.mp4 └── portrait.mp4 ├── Sources ├── Swiftagram │ ├── Authentication │ │ ├── Authentication.swift │ │ ├── Authenticator+Error.swift │ │ ├── Authenticator+Key.swift │ │ ├── Authenticator+Keys.swift │ │ ├── Authenticator.swift │ │ ├── Secret.swift │ │ └── Visual │ │ │ ├── Authenticator+Visual.swift │ │ │ └── AuthenticatorWebView.swift │ ├── Client │ │ ├── Application.swift │ │ ├── Client.swift │ │ ├── Device.swift │ │ └── LegacyDevice.swift │ ├── Endpoints │ │ ├── Archived │ │ │ └── Endpoint+Archived.swift │ │ ├── Direct │ │ │ ├── Endpoint+Conversation.swift │ │ │ ├── Endpoint+ConversationRequest.swift │ │ │ ├── Endpoint+Direct.swift │ │ │ └── Endpoint+Message.swift │ │ ├── Endpoint.swift │ │ ├── Explore │ │ │ └── Endpoint+Explore.swift │ │ ├── Location │ │ │ ├── Endpoint+Location.swift │ │ │ └── Endpoint+LocationPosts.swift │ │ ├── Media │ │ │ ├── Endpoint+Comment.swift │ │ │ ├── Endpoint+ManyComments.swift │ │ │ └── Endpoint+Media.swift │ │ ├── Posts │ │ │ └── Endpoint+Posts.swift │ │ ├── Recent │ │ │ └── Endpoint+Recent.swift │ │ ├── Saved │ │ │ ├── Endpoint+Saved.swift │ │ │ └── Endpoint+SavedCollection.swift │ │ ├── Stories │ │ │ └── Endpoint+Stories.swift │ │ ├── Tag │ │ │ ├── Endpoint+Tag.swift │ │ │ └── Endpoint+TagPosts.swift │ │ └── User │ │ │ ├── Endpoint+ManyUsers.swift │ │ │ ├── Endpoint+User.swift │ │ │ └── Endpoint+Users.swift │ ├── Extensions │ │ ├── @_exported.swift │ │ ├── Agnostic.swift │ │ ├── Constants.swift │ │ ├── HTTPCookie.swift │ │ ├── Header.swift │ │ ├── Paginatable.swift │ │ ├── Publisher.swift │ │ └── URLSession.swift │ └── Models │ │ ├── Errors │ │ └── AuthenticationError.swift │ │ └── Specialized │ │ ├── Comment.swift │ │ ├── Conversation.swift │ │ ├── Friendship.swift │ │ ├── Location.swift │ │ ├── Media.swift │ │ ├── Recipient.swift │ │ ├── SavedCollection.swift │ │ ├── Section.swift │ │ ├── Specialized.swift │ │ ├── Status.swift │ │ ├── Sticker.swift │ │ ├── Tag.swift │ │ ├── TrayItem.swift │ │ ├── User.swift │ │ └── UserTag.swift └── SwiftagramCrypto │ ├── Authentication │ ├── Authenticator+Keychain.swift │ └── Basic │ │ ├── Authenticator+Basic.swift │ │ └── Authenticator+TwoFactor.swift │ ├── Endpoints │ ├── Endpoint+Comment.swift │ ├── Endpoint+ManyComments.swift │ ├── Endpoint+Media.swift │ ├── Endpoint+Posts.swift │ ├── Endpoint+Stories.swift │ ├── Endpoint+Tag.swift │ ├── Endpoint+Uploader.swift │ └── Endpoint+User.swift │ └── Extensions │ ├── @_exported.swift │ └── Crypto.swift ├── Tests └── SwiftagramTests │ ├── AuthenticatorTests.swift │ ├── ClientTests.swift │ ├── EndpointTests.swift │ ├── ModelTests.swift │ └── Shared │ ├── Media+Content.swift │ └── Reflection │ ├── Comment.swift │ ├── Conversation.swift │ ├── Friendship.swift │ ├── Location.swift │ ├── Media.swift │ ├── Recipient.swift │ ├── Reflected.swift │ ├── SavedCollection.swift │ ├── Status.swift │ ├── Sticker.swift │ ├── Tag.swift │ ├── TrayItem.swift │ ├── User.swift │ └── UserTag.swift └── docs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── MIGRATION_GUIDE.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: sbertix 4 | custom: ['https://www.paypal.me/sbertix'] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report things behaving unexpectedly requiring a fix. Use the new Discussions 4 | section for requests and support. 5 | title: '' 6 | labels: bug 7 | assignees: sbertix 8 | 9 | --- 10 | 11 | - [ ] I've searched past issues and I couldn't find reference to this. 12 | 13 | **Swiftagram version** 14 | e.g. `2.0.1`, etc. 15 | 16 | **Describe the bug** 17 | A clear and concise description of what the bug is. 18 | 19 | **To reproduce** 20 | Steps to reproduce the behavior: 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | **Expected behavior** 27 | A clear and concise description of what you expected to happen. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | 32 | **Device and system** 33 | e.g. macOS 10.5.16, iPhone Simulator 13.5. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/critical-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Critical bug report 3 | about: Report crashes and other exceptions rendering Swiftagram unusable. 4 | title: '' 5 | labels: critical 6 | assignees: sbertix 7 | 8 | --- 9 | 10 | - [ ] I've searched past issues and I couldn't find reference to this. 11 | 12 | **Swiftagram version** 13 | e.g. `2.0.1`, etc. 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To reproduce** 19 | Steps to reproduce the behavior: 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | 31 | **Device and system** 32 | e.g. macOS 10.5.16, iPhone Simulator 13.5. 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | -------------------------------------------------------------------------------- /.github/workflows/chatops.yml: -------------------------------------------------------------------------------- 1 | name: chatops 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | jobs: 8 | chatops: 9 | name: ChatOps 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | # launch workflows with specific instructions. 14 | - name: ChatOps 15 | uses: peter-evans/slash-command-dispatch@v2 16 | with: 17 | token: ${{ secrets.CHATOPS_PAT }} 18 | dispatch_type: workflow 19 | commands: | 20 | test 21 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: pull_request 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | # pull request-sepcific steps. 7 | validate_commits: 8 | name: Conventional Commits 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | repository: ${{ (github.event.pull_request_target || github.event.pull_request).head.repo.full_name }} 17 | ref: ${{ (github.event.pull_request_target || github.event.pull_request).head.sha }} 18 | # validate commits. 19 | - name: Validate commits 20 | uses: KevinDeJong-TomTom/commisery-action@master 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | pull_request: ${{ github.event.number }} 24 | 25 | # update the pull request message body. 26 | update_body: 27 | name: Changelog 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v2 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | repository: ${{ (github.event.pull_request_target || github.event.pull_request).head.repo.full_name }} 36 | ref: ${{ (github.event.pull_request_target || github.event.pull_request).head.sha }} 37 | # create the changelog. 38 | - name: Changelog 39 | id: changelog 40 | uses: metcalfc/changelog-generator@v1.0.0 41 | with: 42 | mytoken: ${{ secrets.GITHUB_TOKEN }} 43 | head-ref: ${{ (github.event.pull_request_target || github.event.pull_request).head.sha }} 44 | base-ref: ${{ (github.event.pull_request_target || github.event.pull_request).base.sha }} 45 | repository: ${{ (github.event.pull_request_target || github.event.pull_request).head.repo.full_name }} 46 | continue-on-error: true 47 | # update the pull request message body. 48 | - name: Update Pull Request Description 49 | uses: riskledger/update-pr-description@v2 50 | with: 51 | body: ${{ steps.changelog.outputs.changelog || steps.changelog.outputs.result || 'An error occured. Please populate this yourself @${{ github.event.pull_request.sender.login }}' }} 52 | token: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | # lint code. 55 | lint: 56 | name: Lint 57 | runs-on: ubuntu-latest 58 | 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v2 62 | with: 63 | token: ${{ secrets.GITHUB_TOKEN }} 64 | repository: ${{ (github.event.pull_request_target || github.event.pull_request).head.repo.full_name }} 65 | ref: ${{ (github.event.pull_request_target || github.event.pull_request).head.sha }} 66 | # only lint on actual code changes. 67 | - uses: dorny/paths-filter@v2 68 | id: changes 69 | with: 70 | base: ${{ (github.event.pull_request_target || github.event.pull_request).base.sha }} 71 | filters: | 72 | src: 73 | - '**/*.swift' 74 | - name: Lint 75 | if: steps.changes.outputs.src == 'true' 76 | uses: norio-nomura/action-swiftlint@3.2.1 77 | with: 78 | args: --strict 79 | 80 | # build the library. 81 | build: 82 | name: Build 83 | needs: lint 84 | runs-on: macos-latest 85 | 86 | steps: 87 | - name: Checkout 88 | uses: actions/checkout@v2 89 | with: 90 | token: ${{ secrets.GITHUB_TOKEN }} 91 | repository: ${{ (github.event.pull_request_target || github.event.pull_request).head.repo.full_name }} 92 | ref: ${{ (github.event.pull_request_target || github.event.pull_request).head.sha }} 93 | # only build on actual code changes. 94 | - uses: dorny/paths-filter@v2 95 | id: changes 96 | with: 97 | base: ${{ (github.event.pull_request_target || github.event.pull_request).base.sha }} 98 | filters: | 99 | src: 100 | - '**/*.swift' 101 | - name: Build 102 | if: steps.changes.outputs.src == 'true' 103 | run: swift build 104 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | repository_dispatch: 5 | types: 6 | - test-command 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: macos-latest 12 | 13 | steps: 14 | # checkout the current PR of `ComposableRequest`. 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }} 20 | ref: ${{ github.event.client_payload.pull_request.head.sha }} 21 | # filter updates. 22 | - uses: dorny/paths-filter@v2 23 | id: changes 24 | with: 25 | base: ${{ github.event.client_payload.pull_request.base.sha }} 26 | ref: ${{ github.event.client_payload.pull_request.head.sha }} 27 | filters: | 28 | src: 29 | - '**/*.swift' 30 | # run all tests. 31 | - name: Test 32 | if: steps.changes.outputs.src == 'true' 33 | run: swift test --parallel --enable-test-discovery 34 | env: 35 | SECRET: ${{ secrets.SECRET }} 36 | PASSWORD: ${{ secrets.PASSWORD }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .swiftpm/ 3 | .swiftpm/* 4 | *.xcodeproj 5 | !/Examples/*/*.xcodeproj 6 | /.build 7 | /Packages 8 | Package.resolved 9 | xcuserdata/ 10 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Examples 3 | - Sources 4 | - Tests 5 | 6 | opt_in_rules: 7 | - anyobject_protocol 8 | - array_init 9 | - attributes 10 | - class_delegate_protocol 11 | - closure_end_indentation 12 | - closure_spacing 13 | - collection_alignment 14 | - contains_over_filter_count 15 | - contains_over_filter_is_empty 16 | - contains_over_first_not_nil 17 | - contains_over_range_nil_comparison 18 | - convenience_type 19 | - discouraged_assert 20 | - empty_collection_literal 21 | - empty_count 22 | - empty_string 23 | - empty_xctest_method 24 | - enum_case_associated_values_count 25 | - expiring_todo 26 | - explicit_enum_raw_value 27 | - explicit_top_level_acl 28 | - extension_access_modifier 29 | - fallthrough 30 | - fatal_error_message 31 | - file_header 32 | - file_name_no_space 33 | - file_types_order 34 | - first_where 35 | - force_unwrapping 36 | - identical_operands 37 | - inert_defer 38 | - is_disjoint 39 | - joined_default_parameter 40 | - last_where 41 | - legacy_multiple 42 | - legacy_random 43 | - literal_expression_end_indentation 44 | - lower_acl_than_parent 45 | - missing_docs 46 | - modifier_order 47 | - multiline_arguments 48 | - multiline_function_chains 49 | - multiline_parameters 50 | - number_separator 51 | - operator_usage_whitespace 52 | - orphaned_doc_comment 53 | - pattern_matching_keywords 54 | - prefer_self_type_over_type_of_self 55 | - prefer_zero_over_explicit_init 56 | - private_subject 57 | - private_unit_test 58 | - prohibited_interface_builder 59 | - reduce_into 60 | - redundant_nil_coalescing 61 | - single_test_class 62 | - sorted_first_last 63 | - sorted_imports 64 | - static_operator 65 | - switch_case_on_newline 66 | - test_case_accessibility 67 | - toggle_bool 68 | - trailing_closure 69 | - type_contents_order 70 | - unavailable_function 71 | - unneeded_parentheses_in_closure_argument 72 | - unowned_variable_capture 73 | - untyped_error_in_catch 74 | - unused_import 75 | - vertical_parameter_alignment_on_call 76 | - vertical_whitespace_closing_braces 77 | - vertical_whitespace_opening_braces 78 | - yoda_condition 79 | 80 | cyclomatic_complexity: 81 | ignores_case_statements: true 82 | 83 | file_length: 84 | ignore_comment_only_lines: true 85 | 86 | function_body_length: 87 | warning: 50 88 | 89 | identifier_name: 90 | excluded: 91 | - x 92 | - y 93 | - id 94 | - iv 95 | - url 96 | 97 | line_length: 98 | ignores_comments: true 99 | ignores_function_declarations: true 100 | ignores_interpolated_strings: true 101 | ignores_urls: true 102 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator.xcodeproj/xcshareddata/xcschemes/Authenticator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-App-20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Icon-App-20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Icon-App-29x29@1x.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Icon-App-29x29@2x.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Icon-App-29x29@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "Icon-App-40x40@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Icon-App-40x40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "Icon-App-60x60@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Icon-App-60x60@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "Icon-App-20x20@1x.png", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Icon-App-20x20@2x.png", 65 | "idiom" : "ipad", 66 | "scale" : "2x", 67 | "size" : "20x20" 68 | }, 69 | { 70 | "filename" : "Icon-App-29x29@1x.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Icon-App-29x29@2x.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "29x29" 80 | }, 81 | { 82 | "filename" : "Icon-App-40x40@1x.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Icon-App-40x40@2x.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "40x40" 92 | }, 93 | { 94 | "filename" : "Icon-App-76x76@1x.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Icon-App-76x76@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "76x76" 104 | }, 105 | { 106 | "filename" : "Icon-App-83.5x83.5@2x.png", 107 | "idiom" : "ipad", 108 | "scale" : "2x", 109 | "size" : "83.5x83.5" 110 | }, 111 | { 112 | "filename" : "ItunesArtwork@2x.png", 113 | "idiom" : "ios-marketing", 114 | "scale" : "1x", 115 | "size" : "1024x1024" 116 | } 117 | ], 118 | "info" : { 119 | "author" : "xcode", 120 | "version" : 1 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/header.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "header.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Assets.xcassets/header.imageset/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Authenticator/Authenticator/Assets.xcassets/header.imageset/header.png -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/AuthenticatorApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticatorApp.swift 3 | // Authenticator 4 | // 5 | // Created by Stefano Bertagno on 07/02/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | internal struct AuthenticatorApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView().accentColor(.pink) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Authenticator 4 | // 5 | // Created by Stefano Bertagno on 07/02/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | import Swiftagram 11 | 12 | internal struct ContentView: View { 13 | /// Whether it should present the login view or not. 14 | @State var isPresentingLoginView: Bool = false 15 | /// The current secret. 16 | @State var secret: Secret? 17 | 18 | /// The actual view. 19 | var body: some View { 20 | VStack(spacing: 40) { 21 | // The header. 22 | Image("header") 23 | .resizable() 24 | .scaledToFit() 25 | .frame(maxWidth: .infinity) 26 | .padding(.horizontal, 50) 27 | // Check for token. 28 | if let secret = secret, let token = secret.token { 29 | Text(token) 30 | .font(Font.headline.smallCaps()) 31 | .foregroundColor(.primary) 32 | .lineLimit(3) 33 | .fixedSize(horizontal: false, vertical: true) 34 | .onTapGesture { UIPasteboard.general.string = token } 35 | Text("(Tap to copy it in your clipboard)") 36 | .font(.caption) 37 | .fixedSize(horizontal: false, vertical: true) 38 | } else { 39 | // The disclaimer. 40 | Text.combine( 41 | Text("Please authenticate with your "), 42 | Text("Instagram").bold(), 43 | Text(" account to receive a token for "), 44 | Text("SwiftagramTests").bold() 45 | ) 46 | .fixedSize(horizontal: false, vertical: true) 47 | // Login. 48 | Button { 49 | isPresentingLoginView = true 50 | } label: { 51 | Text("Authenticate").font(.headline) 52 | }.foregroundColor(.accentColor) 53 | } 54 | } 55 | .foregroundColor(.secondary) 56 | .multilineTextAlignment(.center) 57 | .frame(maxWidth: .infinity, maxHeight: .infinity) 58 | .padding(.vertical) 59 | .padding(.horizontal, 50) 60 | .sheet(isPresented: $isPresentingLoginView) { LoginView(secret: $secret) } 61 | } 62 | } 63 | 64 | internal struct ContentView_Previews: PreviewProvider { 65 | static var previews: some View { 66 | ContentView() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/LoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.swift 3 | // Authenticator 4 | // 5 | // Created by Stefano Bertagno on 07/02/21. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | import WebKit 11 | 12 | import Swiftagram 13 | 14 | /// A `class` defining a view controller capable of displaying the authentication web view. 15 | internal class LoginViewController: UIViewController { 16 | /// The completion handler. 17 | var completion: ((Secret) -> Void)? { 18 | didSet { 19 | guard oldValue == nil, let completion = completion else { return } 20 | // Authenticate. 21 | DispatchQueue.main.asyncAfter(deadline: .now()) { 22 | Authenticator.transient 23 | .visual(filling: self.view) 24 | .authenticate() 25 | .sink(receiveCompletion: { _ in self.dismiss(animated: true, completion: nil) }, 26 | receiveValue: completion) 27 | .store(in: &self.bin) 28 | } 29 | } 30 | } 31 | 32 | /// The dispose bag. 33 | private var bin: Set = [] 34 | } 35 | 36 | /// A `struct` defining a `View` used for logging in. 37 | internal struct LoginView: UIViewControllerRepresentable { 38 | /// A `Secret` binding. 39 | @Binding var secret: Secret? 40 | 41 | /// Compose the actual controller. 42 | /// 43 | /// - parameter context: A valid `Context`. 44 | /// - returns: A valid `LoginViewController`. 45 | func makeUIViewController(context: Context) -> LoginViewController { 46 | let controller = LoginViewController() 47 | controller.completion = { secret = $0 } 48 | return controller 49 | } 50 | 51 | /// Update the controller. 52 | /// 53 | /// - parameters: 54 | /// - uiViewController: A valid `LoginViewController`. 55 | /// - context: A valid `Context`. 56 | func updateUIViewController(_ uiViewController: LoginViewController, context: Context) { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Secret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Secret.swift 3 | // Authenticator 4 | // 5 | // Created by Stefano Bertagno on 07/02/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Secret { 13 | /// Compute the token. 14 | var token: String? { try? JSONEncoder().encode(self).base64EncodedString() } 15 | } 16 | -------------------------------------------------------------------------------- /Examples/Authenticator/Authenticator/Text.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Text.swift 3 | // Authenticator 4 | // 5 | // Created by Stefano Bertagno on 07/02/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Text { 11 | /// Combine a collection of texts. 12 | /// 13 | /// - parameters: A collection of `Text`s. 14 | /// - returns: A valid `Text`. 15 | static func combine(_ texts: Text...) -> Text { 16 | combine(texts) 17 | } 18 | 19 | /// Combine a collection of texts. 20 | /// 21 | /// - parameters: A collection of `Text`s. 22 | /// - returns: A valid `Text`. 23 | static func combine(_ texts: [Text]) -> Text { 24 | guard let first = texts.first else { 25 | fatalError("`texts` should not be empty") 26 | } 27 | return texts.dropFirst().reduce(first) { $0+$1 } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Examples/Followers/Followers.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Followers/Followers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Followers/Followers.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Followers/Followers.xcodeproj/xcshareddata/xcschemes/Followers.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Followers 4 | // 5 | // Created by Stefano Bertagno on 10/03/2020. 6 | // 7 | 8 | import UIKit 9 | 10 | import Swiftagram 11 | import SwiftagramCrypto 12 | 13 | @UIApplicationMain 14 | internal class AppDelegate: UIResponder, UIApplicationDelegate { 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Delete persisted data on updated version. 17 | if UserDefaults.standard.string(forKey: "swiftagram.version") != "4.1.0" { 18 | do { try Authenticator.keychain.secrets.delete() } catch { print(error) } 19 | Bundle.main.bundleIdentifier.flatMap(UserDefaults.standard.removePersistentDomain) 20 | // Update version. 21 | UserDefaults.standard.set("4.1.0", forKey: "swiftagram.version") 22 | UserDefaults.standard.synchronize() 23 | } 24 | return true 25 | } 26 | 27 | // MARK: UISceneSession Lifecycle 28 | 29 | func application(_ application: UIApplication, 30 | configurationForConnecting connectingSceneSession: UISceneSession, 31 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 32 | // Called when a new scene session is being created. 33 | // Use this method to select a configuration to create the new scene with. 34 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 35 | } 36 | 37 | func application(_ application: UIApplication, 38 | didDiscardSceneSessions sceneSessions: Set) { 39 | // Called when the user discards a scene session. 40 | // If any sessions were discarded while the application was not running, this will be called 41 | // shortly after application:didFinishLaunchingWithOptions. 42 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-App-20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Icon-App-20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Icon-App-29x29@1x.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Icon-App-29x29@2x.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Icon-App-29x29@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "Icon-App-40x40@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Icon-App-40x40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "Icon-App-60x60@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Icon-App-60x60@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "Icon-App-20x20@1x.png", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Icon-App-20x20@2x.png", 65 | "idiom" : "ipad", 66 | "scale" : "2x", 67 | "size" : "20x20" 68 | }, 69 | { 70 | "filename" : "Icon-App-29x29@1x.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Icon-App-29x29@2x.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "29x29" 80 | }, 81 | { 82 | "filename" : "Icon-App-40x40@1x.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Icon-App-40x40@2x.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "40x40" 92 | }, 93 | { 94 | "filename" : "Icon-App-76x76@1x.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Icon-App-76x76@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "76x76" 104 | }, 105 | { 106 | "filename" : "Icon-App-83.5x83.5@2x.png", 107 | "idiom" : "ipad", 108 | "scale" : "2x", 109 | "size" : "83.5x83.5" 110 | }, 111 | { 112 | "filename" : "ItunesArtwork@2x.png", 113 | "idiom" : "ios-marketing", 114 | "scale" : "1x", 115 | "size" : "1024x1024" 116 | } 117 | ], 118 | "info" : { 119 | "author" : "xcode", 120 | "version" : 1 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/placeholder.imageset/44884218_345707102882519_2446069589734326272_n.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Examples/Followers/Followers/Assets.xcassets/placeholder.imageset/44884218_345707102882519_2446069589734326272_n.jpg -------------------------------------------------------------------------------- /Examples/Followers/Followers/Assets.xcassets/placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "44884218_345707102882519_2446069589734326272_n.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Followers 4 | // 5 | // Created by Stefano Bertagno on 10/03/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | internal struct ContentView: View { 11 | var body: some View { 12 | NavigationView { 13 | FollowersView(model: .init()) 14 | } 15 | } 16 | } 17 | 18 | internal struct ContentView_Previews: PreviewProvider { 19 | static var previews: some View { 20 | ContentView() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/FollowersView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowersView.swift 3 | // Followers 4 | // 5 | // Created by Stefano Bertagno on 10/03/2020. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | internal struct FollowersView: View { 12 | /// The model. 13 | @ObservedObject var model: FollowersModel 14 | 15 | /// The underlying view. 16 | var body: some View { 17 | List { 18 | // Check for followers. 19 | if let followers = model.followers { 20 | // If it's empty, just let the user know. 21 | if followers.isEmpty { 22 | Text("No followers.").padding(.vertical) 23 | } else { 24 | ForEach(followers, id: \.identifier) { user in 25 | Button { 26 | // Open their profile on tap. 27 | guard let url = URL(string: "https://instagram.com/"+user.username) else { return } 28 | UIApplication.shared.open(url, 29 | options: [:], 30 | completionHandler: nil) 31 | } label: { 32 | UserCell(user: user).padding(.vertical) 33 | } 34 | } 35 | } 36 | } else { 37 | Text("Loading…").padding(.vertical) 38 | } 39 | } 40 | .listStyle(PlainListStyle()) 41 | .sheet(isPresented: model.shouldPresentLoginView) { LoginView(didAuthenticate: model.authenticate).id("login") } 42 | .navigationTitle("Followers") 43 | .navigationBarItems(trailing: AvatarButton(user: model.current, action: model.logOut)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | 50 | UISupportedInterfaceOrientations~ipad 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationPortraitUpsideDown 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/LoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.swift 3 | // Followers 4 | // 5 | // Created by Stefano Bertagno on 10/03/2020. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | import WebKit 11 | 12 | import Swiftagram 13 | import SwiftagramCrypto 14 | 15 | /// A `class` defining a view controller capable of displaying the authentication web view. 16 | internal class LoginViewController: UIViewController { 17 | /// The completion handler. 18 | var completion: ((Secret) -> Void)? { 19 | didSet { 20 | guard oldValue == nil, let completion = completion else { return } 21 | // Authenticate. 22 | DispatchQueue.main.asyncAfter(deadline: .now()) { 23 | Authenticator.keychain 24 | .visual(filling: self.view) 25 | .authenticate() 26 | .receive(on: RunLoop.main) 27 | .sink(receiveCompletion: { print($0); self.dismiss(animated: true, completion: nil) }, 28 | receiveValue: completion) 29 | .store(in: &self.bin) 30 | } 31 | } 32 | } 33 | 34 | /// The dispose bag. 35 | private var bin: Set = [] 36 | } 37 | 38 | /// A `struct` defining a `View` used for logging in. 39 | internal struct LoginView: UIViewControllerRepresentable { 40 | /// A completion handler. 41 | let didAuthenticate: (Secret) -> Void 42 | 43 | /// Compose the actual controller. 44 | /// 45 | /// - parameter context: A valid `Context`. 46 | /// - returns: A valid `LoginViewController`. 47 | func makeUIViewController(context: Context) -> LoginViewController { 48 | let controller = LoginViewController() 49 | controller.completion = didAuthenticate 50 | return controller 51 | } 52 | 53 | /// Update the controller. 54 | /// 55 | /// - parameters: 56 | /// - uiViewController: A valid `LoginViewController`. 57 | /// - context: A valid `Context`. 58 | func updateUIViewController(_ uiViewController: LoginViewController, context: Context) { 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Models/FollowersModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowersModel.swift 3 | // Followers 4 | // 5 | // Created by Stefano Bertagno on 10/03/2020. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import SwiftUI 11 | 12 | import SwiftagramCrypto 13 | 14 | /// An `ObservableObject` dealing with requests. 15 | internal final class FollowersModel: ObservableObject { 16 | /// The logged in user. 17 | @Published private(set) var current: User? 18 | /// Initial followers for the logged in user. 19 | @Published private(set) var followers: [User]? 20 | 21 | /// The current secret. 22 | private let secret: CurrentValueSubject = .init(try? Authenticator.keychain.secrets.get().first) 23 | /// The dispose bag. 24 | private var bin: Set = [] 25 | 26 | /// Whether it's authenticated or not. 27 | var shouldPresentLoginView: Binding { 28 | .init(get: { self.secret.value == nil }, set: { _ in }) 29 | } 30 | 31 | /// Init. 32 | init() { 33 | // Update current `User` every time secret is. 34 | // In a real app you would cache this. 35 | secret.removeDuplicates { $0?.identifier == $1?.identifier } 36 | .flatMap { secret -> AnyPublisher in 37 | guard let secret = secret else { return Just(nil).eraseToAnyPublisher() } 38 | // Fetch the user. 39 | return Endpoint.user(secret.identifier) 40 | .unlock(with: secret) 41 | .session(.instagram) 42 | .map(\.user) 43 | .catch { _ in Just(nil) } 44 | .eraseToAnyPublisher() 45 | } 46 | .receive(on: RunLoop.main) 47 | .assign(to: \.current, on: self) 48 | .store(in: &bin) 49 | 50 | // Update followers. 51 | // We only load the first 3 pages. 52 | secret.removeDuplicates { $0?.identifier == $1?.identifier } 53 | .flatMap { secret -> AnyPublisher<[User]?, Never> in 54 | guard let secret = secret else { return Just(nil).eraseToAnyPublisher() } 55 | // Fetch followers. 56 | return Endpoint.user(secret.identifier) 57 | .followers 58 | .unlock(with: secret) 59 | .session(.instagram) 60 | .pages(3) 61 | .compactMap(\.users) 62 | // swiftlint:disable reduce_into 63 | .reduce([], +) 64 | // swiftlint:enable reduce_into 65 | .map(Optional.some) 66 | .catch { _ in Just(nil) } 67 | .eraseToAnyPublisher() 68 | } 69 | .receive(on: RunLoop.main) 70 | .assign(to: \.followers, on: self) 71 | .store(in: &bin) 72 | } 73 | 74 | /// Update the current `Secret`. 75 | /// 76 | /// - parameter secret: A valid `Secret`. 77 | func authenticate(with secret: Secret) { 78 | guard secret.identifier != self.secret.value?.identifier else { return } 79 | DispatchQueue.main.async { 80 | self.objectWillChange.send() 81 | self.secret.send(secret) 82 | } 83 | } 84 | 85 | /// Log out. 86 | func logOut() { 87 | guard let secret = secret.value else { return } 88 | do { try Authenticator.keychain.secret(secret.identifier).delete() } catch { print(error) } 89 | self.secret.send(nil) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Reusables/AvatarButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AvatarButton.swift 3 | // Followers 4 | // 5 | // Created by Stefano Bertagno on 08/02/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | import Swiftagram 11 | 12 | /// A `struct` defining an image of a user. 13 | internal struct AvatarImage: View { 14 | /// The pixel length. 15 | @Environment(\.pixelLength) var pixelLength: CGFloat 16 | 17 | /// The actual user. 18 | let user: User? 19 | 20 | /// The underlying view. 21 | var body: some View { 22 | Color(.quaternarySystemFill) 23 | // We implement the image as an overlay 24 | // to make sure the background is always 25 | // drawn. 26 | .overlay(user.flatMap(\.thumbnail).flatMap { 27 | RemoteImage(url: $0, placeholder: .init()) 28 | .scaledToFill() 29 | }) 30 | .mask(Circle()) 31 | .overlay(Circle().strokeBorder(Color(.opaqueSeparator), lineWidth: pixelLength)) 32 | } 33 | } 34 | 35 | /// A `struct` defining a button with the logged in user image. 36 | internal struct AvatarButton: View { 37 | /// The pixel length. 38 | @Environment(\.pixelLength) var pixelLength: CGFloat 39 | 40 | /// The actual user. 41 | let user: User? 42 | /// The action. 43 | let action: () -> Void 44 | 45 | /// The underlying view. 46 | var body: some View { 47 | Button(action: action) { AvatarImage(user: user).frame(width: 30, height: 30) } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Reusables/RemoteImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteImage.swift 3 | // Followers 4 | // 5 | // Created by Stefano Bertagno on 08/02/21. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | import FetchImage 12 | 13 | /// A `struct` displaying a remote image. 14 | internal struct RemoteImage: View { 15 | /// The current image. 16 | @StateObject private var image: FetchImage = .init() 17 | 18 | /// The underlying url. 19 | var url: URL? 20 | /// The placeholder. 21 | var placeholder: UIImage 22 | 23 | /// The underlying view. 24 | var body: some View { 25 | (image.view ?? Image(uiImage: placeholder)) 26 | .renderingMode(.original) 27 | .resizable() 28 | .aspectRatio(contentMode: .fill) 29 | .animation(.default) 30 | .onAppear { if let url = url { image.load(url) } } 31 | .onDisappear(perform: image.cancel) 32 | } 33 | 34 | /// Init. 35 | /// - parameters: 36 | /// - url: An optional `URL`. 37 | /// - placeholder: A valid `UIImage`. 38 | init(url: URL?, placeholder: UIImage) { 39 | self.url = url 40 | self.placeholder = placeholder 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/Reusables/UserCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserCell.swift 3 | // Followers 4 | // 5 | // Created by Stefano Bertagno on 10/03/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | import Swiftagram 11 | 12 | /// A `struct` defining a `FollowersView` row. 13 | internal struct UserCell: View { 14 | /// A valid `User`. 15 | let user: User 16 | 17 | /// The actual body. 18 | var body: some View { 19 | HStack(spacing: 15) { 20 | // The user image. 21 | AvatarImage(user: user).frame(width: 44, height: 44) 22 | VStack(alignment: .leading) { 23 | // The username. 24 | Text(user.username).font(.headline).fixedSize(horizontal: false, vertical: true) 25 | // The actual name. 26 | if let name = user.name?.trimmingCharacters(in: .whitespacesAndNewlines), 27 | !name.isEmpty { 28 | Text(name) 29 | .font(.footnote) 30 | .foregroundColor(.secondary) 31 | .fixedSize(horizontal: false, vertical: true) 32 | } 33 | }.frame(maxWidth: .infinity, alignment: .leading) 34 | // The chevron. 35 | Spacer() 36 | Image(systemName: "chevron.right") 37 | .imageScale(.small) 38 | .foregroundColor(.secondary) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Examples/Followers/Followers/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Followers 4 | // 5 | // Created by Stefano Bertagno on 10/03/2020. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | internal class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 16 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 17 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 18 | 19 | // Create the SwiftUI view that provides the window contents. 20 | let contentView = ContentView() 21 | 22 | // Use a UIHostingController as window root view controller. 23 | if let windowScene = scene as? UIWindowScene { 24 | let window = UIWindow(windowScene: windowScene) 25 | window.rootViewController = UIHostingController(rootView: contentView) 26 | self.window = window 27 | window.makeKeyAndVisible() 28 | } 29 | } 30 | 31 | func sceneDidDisconnect(_ scene: UIScene) { 32 | // Called as the scene is being released by the system. 33 | // This occurs shortly after the scene enters the background, or when its session is discarded. 34 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 35 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 36 | } 37 | 38 | func sceneDidBecomeActive(_ scene: UIScene) { 39 | // Called when the scene has moved from an inactive state to an active state. 40 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 41 | } 42 | 43 | func sceneWillResignActive(_ scene: UIScene) { 44 | // Called when the scene will move from an active state to an inactive state. 45 | // This may occur due to temporary interruptions (ex. an incoming phone call). 46 | } 47 | 48 | func sceneWillEnterForeground(_ scene: UIScene) { 49 | // Called as the scene transitions from the background to the foreground. 50 | // Use this method to undo the changes made on entering the background. 51 | } 52 | 53 | func sceneDidEnterBackground(_ scene: UIScene) { 54 | // Called as the scene transitions from the foreground to the background. 55 | // Use this method to save data, release shared resources, and store enough scene-specific state information 56 | // to restore the scene back to its current state. 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import Foundation 4 | import PackageDescription 5 | 6 | // MARK: Definitions 7 | 8 | let package = Package( 9 | name: "Swiftagram", 10 | // Supported versions. 11 | platforms: [.iOS("13.0"), 12 | .macOS("10.15"), 13 | .tvOS("13.0"), 14 | .watchOS("6.0")], 15 | // Exposed libraries. 16 | products: [.library(name: "Swiftagram", 17 | targets: ["Swiftagram"]), 18 | .library(name: "SwiftagramCrypto", 19 | targets: ["SwiftagramCrypto"])], 20 | // Package dependencies. 21 | dependencies: [.package(url: "https://github.com/sbertix/ComposableRequest", .upToNextMinor(from: "5.3.1")), 22 | .package(url: "https://github.com/sbertix/SwCrypt.git", .upToNextMinor(from: "5.1.0"))], 23 | // All targets. 24 | targets: [.target(name: "Swiftagram", 25 | dependencies: [.product(name: "Requests", package: "ComposableRequest"), 26 | .product(name: "Storage", package: "ComposableRequest")]), 27 | .target(name: "SwiftagramCrypto", 28 | dependencies: ["Swiftagram", 29 | .product(name: "StorageCrypto", package: "ComposableRequest"), 30 | .product(name: "SwCrypt", package: "SwCrypt")]), 31 | .testTarget(name: "SwiftagramTests", 32 | dependencies: ["Swiftagram", "SwiftagramCrypto"])] 33 | ) 34 | 35 | if ProcessInfo.processInfo.environment["TARGETING_WATCHOS"] == "true" { 36 | // #workaround(xcodebuild -version 11.6, Test targets don’t work on watchOS.) @exempt(from: unicode) 37 | package.targets.removeAll(where: { $0.isTest }) 38 | } 39 | -------------------------------------------------------------------------------- /Resources/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Resources/header.png -------------------------------------------------------------------------------- /Resources/landscape.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Resources/landscape.mp4 -------------------------------------------------------------------------------- /Resources/portrait.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbertix/Swiftagram/de8a5d840a81b3d0dd66f0dc45304c54f29d0fda/Resources/portrait.mp4 -------------------------------------------------------------------------------- /Sources/Swiftagram/Authentication/Authentication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authentication.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 09/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `protocol` defining an authentication process to be executed mimicing a custom `Client`. 11 | public protocol CustomClientAuthentication: Authentication { 12 | /// Authenticate the given user. 13 | /// 14 | /// - parameter client: A valid `Client`. 15 | /// - returns: A valid `Publisher`. 16 | func authenticate(in client: Client) -> AnyPublisher 17 | } 18 | 19 | /// A `protocol` defining a generic authentication process. 20 | public protocol Authentication { 21 | /// Authenticate the given user. 22 | /// 23 | /// - returns: A valid `Publisher`. 24 | func authenticate() -> AnyPublisher 25 | } 26 | 27 | public extension CustomClientAuthentication { 28 | /// Authenticate the given user, with `Client.default`. 29 | /// 30 | /// - returns: A valid `Publisher`. 31 | func authenticate() -> AnyPublisher { 32 | authenticate(in: .default) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Authentication/Authenticator+Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticator+Error.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 10/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import ComposableStorage 11 | 12 | public extension Authenticator { 13 | /// An `enum` listing some authentication-specific errors. 14 | enum Error: Swift.Error { 15 | /// Generic error. 16 | case generic(String) 17 | /// Invalid cookies. 18 | case invalidCookies([HTTPCookie]) 19 | /// Invalid password. 20 | case invalidPassword 21 | /// Invalid response. 22 | case invalidResponse(URLResponse) 23 | /// Invalid URL. 24 | case invalidURL 25 | /// Invalid username. 26 | case invalidUsername 27 | /// Two factor authentication challenge. 28 | case twoFactorChallenge(TwoFactor) 29 | } 30 | } 31 | 32 | public extension Authenticator.Error { 33 | /// A `struct` defining a list of properties used for 34 | /// resolving a two factor authentication challenge. 35 | struct TwoFactor { 36 | /// The storage. 37 | public let storage: AnyStorage 38 | /// The client. 39 | public let client: Client 40 | /// The challenge identifier. 41 | public let identifier: String 42 | /// The username. 43 | public let username: String 44 | /// The cross site request forgery token. 45 | public let crossSiteRequestForgery: HTTPCookie 46 | 47 | /// Init. 48 | /// 49 | /// - parameters: 50 | /// - storage: Some `Storage`. 51 | /// - client: A valid `Client`. 52 | /// - identifier: A valid `String`. 53 | /// - username: A valid `String`. 54 | /// - crossSiteRequestForgery: A valid `HTTPCookie`. 55 | public init(storage: S, 56 | client: Client, 57 | identifier: String, 58 | username: String, 59 | crossSiteRequestForgery: HTTPCookie) where S.Item == Secret { 60 | self.storage = AnyStorage(storage) 61 | self.client = client 62 | self.identifier = identifier 63 | self.username = username 64 | self.crossSiteRequestForgery = crossSiteRequestForgery 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Authentication/Authenticator+Key.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticator+Key.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 10/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import ComposableStorage 11 | 12 | public extension Authenticator { 13 | /// A `class` defining an instance used for `Secret` management. 14 | final class Key { 15 | /// The authenticator. 16 | private let authenticator: Authenticator 17 | /// The `Secret` label. 18 | public let label: String 19 | 20 | /// Init. 21 | /// 22 | /// - parameters: 23 | /// - authenticator: A valid `Authenticator`. 24 | /// - label: A valid `String`. 25 | fileprivate init(authenticator: Authenticator, 26 | label: String) { 27 | self.authenticator = authenticator 28 | self.label = label 29 | } 30 | } 31 | 32 | /// Return a specific `Secret` manager. 33 | /// 34 | /// - parameter label: A valid `String`. 35 | /// - returns: A valid `Key`. 36 | func secret(_ label: String) -> Key { 37 | .init(authenticator: self, label: label) 38 | } 39 | 40 | /// Return a specific `Secret` manager. 41 | /// 42 | /// - parameter secret: A valid `Secret`. 43 | /// - returns: A valid `Key`. 44 | func secret(_ secret: Secret) -> Key { 45 | self.secret(secret.label) 46 | } 47 | } 48 | 49 | public extension Authenticator.Key { 50 | /// Try fetching a `Secret` matching the given `label`. 51 | /// 52 | /// - throws: Some `Error`. 53 | /// - returns: An optional `Secret`. 54 | func get() throws -> Secret? { 55 | try AnyStorage.item(matching: label, in: authenticator.storage) 56 | } 57 | 58 | /// Delete the selected `Secret`. 59 | /// 60 | /// - throws: Some `Error`. 61 | /// - returns: An optional `Secret`. 62 | @discardableResult 63 | func delete() throws -> Secret? { 64 | try AnyStorage.discard(label, in: authenticator.storage) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Authentication/Authenticator+Keys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticator+Keys.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 10/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import ComposableStorage 11 | 12 | public extension Authenticator { 13 | /// A `class` defining an instance used for `Secret`s management. 14 | final class Keys { 15 | /// The authenticator. 16 | private let authenticator: Authenticator 17 | /// `Secret`s labels. `nil` means all secrets should be managed. 18 | public let labels: [String]? 19 | 20 | /// Init. 21 | /// 22 | /// - parameters: 23 | /// - authenticator: A valid `Authenticator`. 24 | /// - labels: An optional array of `String`s. 25 | fileprivate init(authenticator: Authenticator, 26 | labels: [String]?) { 27 | self.authenticator = authenticator 28 | self.labels = labels 29 | } 30 | } 31 | 32 | /// Return a manager for all `Secret`s. 33 | var secrets: Keys { 34 | .init(authenticator: self, labels: nil) 35 | } 36 | 37 | /// Return some specific `Secret`s manager. 38 | /// 39 | /// - parameter labels: A collection of `String`s. 40 | /// - returns: Some valid `Keys`. 41 | func secrets(_ labels: C) -> Keys where C.Element == String { 42 | .init(authenticator: self, labels: Array(labels)) 43 | } 44 | 45 | /// Return some specific `Secret`s manager. 46 | /// 47 | /// - parameter secrets: A collection of `Secret`s. 48 | /// - returns: Some valid `Keys`. 49 | func secrets(_ secrets: C) -> Keys where C.Element == Secret { 50 | .init(authenticator: self, labels: secrets.map(\.label)) 51 | } 52 | } 53 | 54 | public extension Authenticator.Keys { 55 | /// Try fetching `Secret`s matching `label`s. 56 | /// 57 | /// - throws: Some `Error`. 58 | /// - returns: An array of `Secret`s. 59 | func get() throws -> [Secret] { 60 | switch labels { 61 | case let labels?: 62 | return try AnyStorage.items(in: authenticator.storage) 63 | .filter { labels.contains($0.label) } 64 | default: 65 | return try AnyStorage.items(in: authenticator.storage) 66 | } 67 | } 68 | 69 | /// Delete selected `Secret`s. 70 | /// 71 | /// - throws: Some `Error`. 72 | /// - returns: An optional `Secret`. 73 | @discardableResult 74 | func delete() throws -> [Secret] { 75 | switch labels { 76 | case let labels?: 77 | return try labels.compactMap { try authenticator.secret($0).delete() } 78 | default: 79 | return try authenticator.secrets.get().compactMap { try authenticator.secret($0).delete() } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Authentication/Authenticator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticator.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 09/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import ComposableStorage 11 | 12 | /// A `typealias` for `ComposableStorage.UserDefaultsStorage`. 13 | /// 14 | /// - note: 15 | /// We prefer this to `import @_exported`, as we can't guarantee `@_exported` 16 | /// to stick with future versions of **Swift**. 17 | public typealias UserDefaultsStorage = ComposableStorage.UserDefaultsStorage 18 | 19 | /// A `struct` defining an instance capable of 20 | /// starting the authentication flow for a given user. 21 | public struct Authenticator { 22 | /// The underlying storage. 23 | public let storage: AnyStorage 24 | 25 | /// Init. 26 | /// 27 | /// - parameters: 28 | /// - storage: A valid `Storage`. 29 | /// - client: A valid `Client`. Defaults to `.default`. 30 | public init(storage: S) where S.Item == Secret { 31 | self.storage = AnyStorage(storage) 32 | } 33 | } 34 | 35 | public extension Authenticator { 36 | /// An `enum` listing all authentication implementations. 37 | enum Group { } 38 | } 39 | 40 | public extension Authenticator { 41 | /// The default transient `Authenticator`. 42 | static var transient: Authenticator { 43 | .init(storage: TransientStorage()) 44 | } 45 | 46 | /// The default user defaults-backed `Authenticator`. 47 | static var userDefaults: Authenticator { 48 | userDefaults(.init(userDefaults: .standard)) 49 | } 50 | 51 | /// A user defaults-backed `Authenticator` with a specific `Client`. 52 | /// 53 | /// - parameter userDefaultsStorage: A valid `UserDefaultsStorage`. 54 | /// - returns: A valid `Authenticator.` 55 | static func userDefaults(_ userDefaultsStorage: UserDefaultsStorage) -> Authenticator { 56 | self.init(storage: userDefaultsStorage) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Client/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 27/10/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Client { 11 | /// A `struct` defining all possible information about a (mock) Instagram mobile app. 12 | /// 13 | /// - note: Keep in mind default values for `version` and `code` are not guaranteed to be the same among Android and iOS clients. 14 | /// - warning: `version` and `code` defaults are not guarantted to remain the same. 15 | struct Application: Equatable, Codable, CustomStringConvertible { 16 | /// The client's version. Android devices' versions end with _" Android"_. 17 | public let version: String 18 | 19 | /// The client's code. 20 | public let code: String 21 | 22 | /// Create an Android client. 23 | /// 24 | /// - parameters: 25 | /// - version: A valid `String`. Defaults to _"160.1.0.31.120"_. 26 | /// - code: A valid `String`. Defaults to _"185203708"_. 27 | /// - returns: A valid `Client`. 28 | public static func android(_ version: String = "160.1.0.31.120", code: String = "246979827") -> Application { 29 | .init(version: version + " Android", code: code) 30 | } 31 | 32 | /// Create an iOS client. 33 | /// 34 | /// - parameters: 35 | /// - version: A valid `String`. Defaults to _"121.0.0.29.119"_. 36 | /// - code: A valid `String`. Defaults to _"185203708"_. 37 | /// - returns: A valid `Client`. 38 | public static func iOS(_ version: String = "160.1.0.31.120", code: String = "246979827") -> Application { 39 | .init(version: version, code: code) 40 | } 41 | 42 | /// A valid description. 43 | public var description: String { "Instagram \(version)" } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Client/LegacyDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegacyDevice.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 27/10/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `struct` representing `Device`s used before `4.2.0`. 11 | /// 12 | /// This is kept in order to maintain backwards compatibility with `Secret`s. 13 | /// Please keep in mind support for this might be removed in the future. 14 | internal struct LegacyDevice: Codable { 15 | /// The brand. 16 | let brand: String 17 | /// The model. 18 | let model: String 19 | /// The device model boot. 20 | let modelBoot: String 21 | /// The CPU identifier. 22 | let cpu: String 23 | 24 | /// The device GUID. 25 | let deviceGUID: UUID 26 | /// The phone GUID. 27 | let phoneGUID: UUID 28 | /// The goold AD ID. 29 | let googleAdId: UUID 30 | 31 | /// The DPI. 32 | let dpi: Int 33 | /// The resolution of the screen. 34 | let resolution: [Double] 35 | 36 | /// The API version. 37 | let api: String 38 | /// The OS version. 39 | let version: String 40 | /// The OS release. 41 | let release: String 42 | /// The application code. 43 | let code: String 44 | } 45 | 46 | extension Client.Device { 47 | /// Init. 48 | /// 49 | /// - parameters: 50 | /// - device: A valid `LegacyDevice`. 51 | /// - width: A valid `Int`. 52 | /// - height: A valid `Int`. 53 | init(device: LegacyDevice, width: Int, height: Int) { 54 | self.init(identifier: device.deviceGUID, 55 | phoneIdentifier: device.phoneGUID, 56 | adIdentifier: device.googleAdId, 57 | hardware: .init(model: device.model, 58 | brand: device.brand, 59 | boot: device.modelBoot, 60 | cpu: device.cpu, 61 | manufacturer: nil), 62 | software: .init(version: [device.release, device.version].joined(separator: "/"), 63 | language: "en_US"), 64 | resolution: .init(width: width, 65 | height: height, 66 | scale: 2, 67 | dpi: device.dpi)) 68 | } 69 | } 70 | 71 | extension Client { 72 | /// Init. 73 | /// 74 | /// - parameters: 75 | /// - device: A valid `LegacyDevice`. 76 | /// - width: A valid `Int`. 77 | /// - height: A valid `Int`. 78 | init(device: LegacyDevice, width: Int, height: Int) { 79 | self.init(application: .android(device.api, code: device.code), 80 | device: .init(device: device, width: Int(width), height: Int(height))) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Archived/Endpoint+Archived.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Archived.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 19/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `struct` defining archive-related endpoints. 12 | struct Archived { } 13 | } 14 | 15 | public extension Endpoint { 16 | /// A wrapper for archive-related endpoints. 17 | static let archived: Endpoint.Group.Archived = .init() 18 | } 19 | 20 | public extension Endpoint.Group.Archived { 21 | /// All archived posts. 22 | var posts: Endpoint.Paginated < Swiftagram.Media.Collection, 23 | RankedOffset, 24 | Error> { 25 | .init { secret, session, pages in 26 | // Persist the rank token. 27 | let rank = pages.rank ?? UUID().uuidString 28 | // Prepare the actual pager. 29 | return Pager(pages) { 30 | Request.feed 31 | .path(appending: "only_me_feed/") 32 | .header(appending: secret.header) 33 | .header(appending: rank, forKey: "rank_token") 34 | .query(appending: $0, forKey: "max_id") 35 | .publish(with: session) 36 | .map(\.data) 37 | .wrap() 38 | .map(Swiftagram.Media.Collection.init) 39 | .iterateFirst(stoppingAt: $0) 40 | } 41 | .replaceFailingWithError() 42 | } 43 | } 44 | 45 | /// All archived stories. 46 | var stories: Endpoint.Paginated < TrayItem.Collection, 47 | RankedOffset, 48 | Error> { 49 | .init { secret, session, pages in 50 | // Persist the rank token. 51 | let rank = pages.rank ?? UUID().uuidString 52 | // Prepare the actual pager. 53 | return Pager(pages) { 54 | Request.version1 55 | .archive 56 | .reel 57 | .day_shells 58 | .appendingDefaultHeader() 59 | .header(appending: secret.header) 60 | .header(appending: rank, forKey: "rank_token") 61 | .query(appending: $0, forKey: "max_id") 62 | .publish(with: session) 63 | .map(\.data) 64 | .wrap() 65 | .map(TrayItem.Collection.init) 66 | .iterateFirst(stoppingAt: $0) 67 | } 68 | .replaceFailingWithError() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Direct/Endpoint+ConversationRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Direct+Request.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 27/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group.Direct.Conversation { 11 | /// A `class` defining a wrapper for a conversation request. 12 | final class Request { 13 | /// The conversation. 14 | public let conversation: Endpoint.Group.Direct.Conversation 15 | 16 | /// Init. 17 | /// 18 | /// - parameter conversation: A valid `Endpoint.Group.Direct.Conversation`. 19 | init(conversation: Endpoint.Group.Direct.Conversation) { 20 | self.conversation = conversation 21 | } 22 | } 23 | 24 | /// A wrapper for request endpoints. 25 | var request: Request { 26 | .init(conversation: self) 27 | } 28 | } 29 | 30 | public extension Endpoint.Group.Direct.Conversation.Request { 31 | /// Approve the current conversation request. 32 | /// 33 | /// - returns: A valid `Endpoint.Single`. 34 | /// - warning: This is not tested in `SwiftagramTests`, so it might not work in the future. Open an `issue` if that happens. 35 | func approve() -> Endpoint.Single { 36 | conversation.edit("approve/") 37 | } 38 | 39 | /// Decline the current conversation request. 40 | /// 41 | /// - returns: A valid `Endpoint.Single`. 42 | /// - warning: This is not tested in `SwiftagramTests`, so it might not work in the future. Open an `issue` if that happens. 43 | func decline() -> Endpoint.Single { 44 | conversation.edit("reject/") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Direct/Endpoint+Direct.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Direct.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 25/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `class` defining `direct_v2` endpoints. 12 | final class Direct { } 13 | } 14 | 15 | public extension Endpoint { 16 | /// A wrapper for direct endpoints. 17 | static let direct: Group.Direct = .init() 18 | } 19 | 20 | extension Request { 21 | /// The `direct_v2` base request. 22 | static let direct = Request.version1.direct_v2.appendingDefaultHeader() 23 | /// The threads base request. 24 | static let directThreads = Request.direct.threads.appendingDefaultHeader() 25 | } 26 | 27 | public extension Endpoint.Group.Direct { 28 | /// Get the user presence. 29 | var activity: Endpoint.Single { 30 | .init { secret, session in 31 | Deferred { 32 | Request.direct 33 | .path(appending: "get_presence/") 34 | .header(appending: secret.header) 35 | .publish(with: session) 36 | .map(\.data) 37 | .wrap() 38 | } 39 | .eraseToAnyPublisher() 40 | } 41 | } 42 | 43 | /// Paginate all approved conversations in your inbox. 44 | var conversations: Endpoint.Paginated { 45 | inbox(isPending: false) 46 | } 47 | 48 | /// Fetch all suggested recipients. 49 | var recipients: Endpoint.Single { 50 | recipients(matching: nil) 51 | } 52 | 53 | /// Paginate the pending requests inbox. 54 | var requests: Endpoint.Paginated { 55 | inbox(isPending: true) 56 | } 57 | 58 | /// Fetch all recipients, optinally matching a given query. 59 | /// 60 | /// - parameter query: A valid `String`. 61 | /// - returns: An `Endpoint.Single`. 62 | func recipients(matching query: String) -> Endpoint.Single { 63 | recipients(matching: .some(query)) 64 | } 65 | } 66 | 67 | fileprivate extension Endpoint.Group.Direct { 68 | /// Fetch the inbox. 69 | /// 70 | /// - parameter isPending: A valid `Bool`. 71 | /// - returns: An `Endpoint.Paginated`. 72 | func inbox(isPending: Bool) -> Endpoint.Paginated { 73 | .init { secret, session, pages in 74 | Pager(pages) { 75 | Request.direct 76 | .path(appending: isPending ? "pending_inbox" : "inbox") 77 | .header(appending: secret.header) 78 | .query(appending: ["visual_message_return_type": "unseen", 79 | "direction": $0.flatMap { _ in "older" }, 80 | "cursor": $0, 81 | "thread_message_limit": "10", 82 | "persistent_badging": "true", 83 | "limit": "20"]) 84 | .publish(with: session) 85 | .map(\.data) 86 | .wrap() 87 | .map(Swiftagram.Conversation.Collection.init) 88 | .iterateFirst(stoppingAt: $0) 89 | } 90 | .replaceFailingWithError() 91 | } 92 | } 93 | 94 | /// Fetch all recipients, optinally matching a given query. 95 | /// 96 | /// - parameter query: An optional `String`. 97 | /// - returns: An `Endpoint.Single`. 98 | func recipients(matching query: String?) -> Endpoint.Single { 99 | .init { secret, session in 100 | Deferred { 101 | Request.direct 102 | .path(appending: "ranked_recipients/") 103 | .header(appending: secret.header) 104 | .header(appending: ["mode": "raven", 105 | "query": query, 106 | "show_threads": "true"]) 107 | .publish(with: session) 108 | .map(\.data) 109 | .wrap() 110 | .map(Recipient.Collection.init) 111 | } 112 | .replaceFailingWithError() 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Direct/Endpoint+Message.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Direct+Message.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 25/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group.Direct.Conversation { 11 | /// A `class` defining a wrapper for a specific message. 12 | final class Message { 13 | /// The conversation. 14 | public let conversation: Endpoint.Group.Direct.Conversation 15 | /// The identifier. 16 | public let identifier: String 17 | 18 | /// Init. 19 | /// 20 | /// - parameters: 21 | /// - conversation: A valid `Endpoint.Group.Direct.Conversation`. 22 | /// - identifier: A valid `String`. 23 | init(conversation: Endpoint.Group.Direct.Conversation, 24 | identifier: String) { 25 | self.conversation = conversation 26 | self.identifier = identifier 27 | } 28 | } 29 | 30 | /// A wrapper for message endpoints. 31 | /// 32 | /// - parameter identifier: A valid `String`. 33 | /// - returns: A valid `Message`. 34 | func message(_ identifier: String) -> Message { 35 | .init(conversation: self, identifier: identifier) 36 | } 37 | } 38 | 39 | extension Swiftagram.Request { 40 | /// A specific message base request. 41 | /// 42 | /// - parameter message: A valid `Message`. 43 | static func directMessage(_ message: Endpoint.Group.Direct.Conversation.Message) -> Request { 44 | Swiftagram.Request.directThread(message.conversation).items.path(appending: message.identifier) 45 | } 46 | } 47 | 48 | public extension Endpoint.Group.Direct.Conversation.Message { 49 | /// Delete the current message. 50 | /// 51 | /// - returns: A valid `Endpoint.Single`. 52 | func delete() -> Endpoint.Single { 53 | .init { secret, session in 54 | Deferred { 55 | Request.directMessage(self) 56 | .path(appending: "delete/") 57 | .header(appending: secret.header) 58 | .body(appending: ["_csrftoken": secret["csrftoken"], 59 | "_uuid": secret.client.device.identifier.uuidString]) 60 | .publish(with: session) 61 | .map(\.data) 62 | .wrap() 63 | .map(Status.init) 64 | } 65 | .replaceFailingWithError() 66 | } 67 | } 68 | 69 | /// Mark the current message as watched. 70 | /// 71 | /// - returns: A valid `Endpoint.Single`. 72 | func open() -> Endpoint.Single { 73 | .init { secret, session in 74 | Deferred { 75 | Request.directMessage(self) 76 | .path(appending: "seen/") 77 | .header(appending: secret.header) 78 | .body(appending: ["_csrftoken": secret["csrftoken"], 79 | "_uuid": secret.client.device.identifier.uuidString, 80 | "use_unified_inbox": "true", 81 | "action": "mark_seen", 82 | "thread_id": self.conversation.identifier, 83 | "item_id": self.identifier]) 84 | .publish(with: session) 85 | .map(\.data) 86 | .wrap() 87 | .map(Status.init) 88 | } 89 | .replaceFailingWithError() 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Endpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 06/03/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A module-like `enum` defining all possible `Endpoint`s. 11 | public enum Endpoint { 12 | // swiftlint:disable line_length 13 | /// An `Endpoint` allowing for a paginated request with a custom `Response` value. 14 | /// 15 | /// - note: Always reference this alias, to abstract away `ComposableRequest` implementation. 16 | public typealias Paginated = LockSessionPagerProvider> 17 | // swiftlint:enable line_length 18 | 19 | /// An `Endpoint` allowing for a single request with a custom `Response` value. 20 | /// 21 | /// - note: Always reference this alias, to abstract away `ComposableRequest` implementation. 22 | public typealias Single = LockSessionProvider> 23 | 24 | /// A module-like `enum` to hide away endpoint wrappers definitions. 25 | public enum Group { } 26 | } 27 | 28 | public extension Request { 29 | /// An `Endpoint` pointing to `i.instagram.com`. 30 | static let api: Request = .init("https://i.instagram.com") 31 | 32 | /// An `Endpoint` pointing to `api/v1`. 33 | static let version1: Request = api.path(appending: "/api/v1") 34 | 35 | /// An `Endpoint` pointing to the Instagram homepage. 36 | static let generic: Request = .init("https://www.instagram.com") 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Explore/Endpoint+Explore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Explore.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 07/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `struct` defining `explore` endpoints. 12 | final class Explore { } 13 | } 14 | 15 | public extension Endpoint { 16 | /// A wrapper for explore endpoints. 17 | static let explore: Group.Explore = .init() 18 | } 19 | 20 | extension Request { 21 | /// A discover related request. 22 | static let discover = Request.version1.discover.appendingDefaultHeader() 23 | } 24 | 25 | public extension Endpoint.Group.Explore { 26 | /// A list of posts in the explore page. 27 | var posts: Endpoint.Paginated { 28 | .init { secret, session, pages in 29 | Pager(pages) { 30 | Request.discover 31 | .explore 32 | .header(appending: secret.header) 33 | .query(appending: $0, forKey: "max_id") 34 | .publish(with: session) 35 | .map(\.data) 36 | .wrap() 37 | .iterateFirst(stoppingAt: $0) { 38 | $0.flatMap { $0.nextMaxId.string(converting: true) } 39 | .flatMap(Instruction.load) ?? .stop 40 | } 41 | } 42 | .eraseToAnyPublisher() 43 | } 44 | } 45 | 46 | /// A list of topics in the explore page. 47 | var topics: Endpoint.Paginated { 48 | .init { secret, session, pages in 49 | Pager(pages) { 50 | Request.discover 51 | .topical_explore 52 | .header(appending: secret.header) 53 | .query(appending: ["is_prefetch": "true", 54 | "omit_cover_media": "false", 55 | "use_sectional_payload": "true", 56 | "timezone_offset": "43200", 57 | "session_id": secret["sessionid"], 58 | "include_fixed_destinations": "false", 59 | "max_id": $0]) 60 | .publish(with: session) 61 | .map(\.data) 62 | .wrap() 63 | .iterateFirst(stoppingAt: $0) { 64 | $0.flatMap { $0.nextMaxId.string(converting: true) } 65 | .flatMap(Instruction.load) ?? .stop 66 | } 67 | } 68 | .eraseToAnyPublisher() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Location/Endpoint+Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Location.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 01/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `class` defining location endpoints. 12 | final class Location { 13 | /// The location identifier. 14 | public let identifier: String 15 | 16 | /// Init. 17 | /// 18 | /// - parameter identifier: A valid `String`. 19 | init(identifier: String) { 20 | self.identifier = identifier 21 | } 22 | } 23 | } 24 | 25 | public extension Endpoint { 26 | /// A wrapper for location endpoints. 27 | /// 28 | /// - parameter identifier: A valid `String`. 29 | /// - returns: A valid `Location`. 30 | static func location(_ identifier: String) -> Endpoint.Group.Location { 31 | .init(identifier: identifier) 32 | } 33 | 34 | /// A summary for the location media. 35 | /// 36 | /// - parameter identifier: A valid `String`. 37 | /// - returns: A valid `Endpoint.Single`. 38 | static func location(_ identifier: String) -> Endpoint.Single { 39 | location(identifier).summary 40 | } 41 | 42 | /// A list of locations around the given coordiantes, matching an optional query. 43 | /// 44 | /// - parameters: 45 | /// - coordinates: Some valid `Location.Coordinates`. 46 | /// - query: An optional `String`. Defaults to `nil`. 47 | /// - returns: A valid `Endpoint.Single`. 48 | static func locations(around coordinates: Swiftagram.Location.Coordinates, 49 | matching query: String? = nil) -> Endpoint.Single { 50 | .init { secret, session in 51 | Deferred { 52 | Request.version1 53 | .appendingDefaultHeader() 54 | .path(appending: "location_search/") 55 | .header(appending: secret.header) 56 | .query(appending: [ 57 | "rank_token": "", 58 | "latitude": "\(coordinates.latitude)", 59 | "longitude": "\(coordinates.longitude)", 60 | "timestamp": query == nil ? "\(Int(Date().timeIntervalSince1970 * 1_000))" : nil, 61 | "search_query": query, 62 | "_csrftoken": secret["csrftoken"], 63 | "_uid": secret.identifier, 64 | "_uuid": secret.client.device.identifier.uuidString 65 | ]) 66 | .publish(with: session) 67 | .map(\.data) 68 | .wrap() 69 | .map(Swiftagram.Location.Collection.init) 70 | } 71 | .eraseToAnyPublisher() 72 | } 73 | } 74 | } 75 | 76 | extension Request { 77 | /// A locations related request. 78 | static let locations = Request.version1.locations.appendingDefaultHeader() 79 | 80 | /// A location related request. 81 | /// 82 | /// - parameter location: A valid `Endpoint.Location`. 83 | /// - returns: A valid `Request`. 84 | static func location(_ location: Endpoint.Group.Location) -> Request { 85 | locations.path(appending: location.identifier) 86 | } 87 | } 88 | 89 | public extension Endpoint.Group.Location { 90 | /// A summary for the current location. 91 | /// 92 | /// - note: Prefer `Endpoint.location(_:)` instead. 93 | var summary: Endpoint.Single { 94 | .init { secret, session in 95 | Deferred { 96 | Request.location(self) 97 | .path(appending: "info/") 98 | .appendingDefaultHeader() 99 | .header(appending: secret.header) 100 | .publish(with: session) 101 | .map(\.data) 102 | .wrap() 103 | .map(Swiftagram.Location.Unit.init) 104 | } 105 | .replaceFailingWithError() 106 | } 107 | } 108 | 109 | /// A list of some recent stories at the current location. 110 | var stories: Endpoint.Single { 111 | .init { secret, session in 112 | Deferred { 113 | Request.location(self) 114 | .path(appending: "story/") 115 | .appendingDefaultHeader() 116 | .header(appending: secret.header) 117 | .publish(with: session) 118 | .map(\.data) 119 | .wrap() 120 | .map(TrayItem.Unit.init) 121 | } 122 | .replaceFailingWithError() 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Location/Endpoint+LocationPosts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+LocationPosts.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 19/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group.Location { 11 | /// A `struct` defining location-related posts endpoints. 12 | struct Posts { 13 | /// The underlying location. 14 | public let location: Endpoint.Group.Location 15 | 16 | /// Init. 17 | /// 18 | /// - parameter location: A valid `Location`. 19 | init(location: Endpoint.Group.Location) { 20 | self.location = location 21 | } 22 | } 23 | 24 | /// A wrapper for location-related posts endpoints. 25 | var posts: Posts { 26 | .init(location: self) 27 | } 28 | } 29 | 30 | public extension Endpoint.Group.Location.Posts { 31 | /// A list of recent posts. 32 | var recent: Endpoint.Paginated { 33 | .init { secret, session, pages in 34 | Pager(pages) { 35 | Request.location(self.location) 36 | .sections 37 | .path(appending: "/") 38 | .header(appending: secret.header) 39 | .body(appending: ["max_id": $0?.identifier, 40 | "tab": "recent", 41 | "page": ($0?.page).flatMap { $0 <= 0 ? nil : "\($0)" }, 42 | "next_media_ids": "[\($0?.mediaIdentifiers.joined(separator: ",") ?? "")]", 43 | "_csrftoken": secret["csrftoken"], 44 | "_uuid": secret.client.device.identifier.uuidString, 45 | "session_id": secret["sessionid"]].compactMapValues { $0 }) 46 | .publish(with: session) 47 | .map(\.data) 48 | .wrap() 49 | .map(Section.Collection.init) 50 | .iterateFirst(stoppingAt: $0) 51 | } 52 | .replaceFailingWithError() 53 | } 54 | } 55 | 56 | /// A list of highest ranking posts. 57 | var top: Endpoint.Paginated { 58 | .init { secret, session, pages in 59 | Pager(pages) { 60 | Request.location(self.location) 61 | .sections 62 | .path(appending: "/") 63 | .header(appending: secret.header) 64 | .body(appending: ["max_id": $0?.identifier, 65 | "tab": "ranked", 66 | "page": ($0?.page).flatMap { $0 <= 0 ? nil : "\($0)" }, 67 | "next_media_ids": "[\($0?.mediaIdentifiers.joined(separator: ",") ?? "")]", 68 | "_csrftoken": secret["csrftoken"], 69 | "_uuid": secret.client.device.identifier.uuidString, 70 | "session_id": secret["sessionid"]].compactMapValues { $0 }) 71 | .publish(with: session) 72 | .map(\.data) 73 | .wrap() 74 | .map(Section.Collection.init) 75 | .iterateFirst(stoppingAt: $0) 76 | } 77 | .replaceFailingWithError() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Media/Endpoint+Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Comment.swift 3 | // ComposableRequest 4 | // 5 | // Created by Stefano Bertagno on 01/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group.Media { 11 | /// A `class` defining comment endpoints. 12 | final class Comment { 13 | /// The media. 14 | public let media: Endpoint.Group.Media 15 | /// The comment identifier. 16 | public let identifier: String 17 | 18 | /// Init. 19 | /// 20 | /// - parameters: 21 | /// - media: A valid `Endpoint.Group.Media`. 22 | /// - identifier: A valid `String`. 23 | init(media: Endpoint.Group.Media, 24 | identifier: String) { 25 | self.media = media 26 | self.identifier = identifier 27 | } 28 | } 29 | 30 | /// A wrapper for comments endpoints. 31 | /// 32 | /// - parameter identifier: A valid `String`. 33 | /// - returns: A valid `Endpoint.Comment`. 34 | func comment(_ identifier: String) -> Comment { 35 | .init(media: self, identifier: identifier) 36 | } 37 | } 38 | 39 | public extension Endpoint.Group.Media.Comment { 40 | /// Like the underlying comment. 41 | /// 42 | /// - returns: A valid `Endpoint.Single`. 43 | func like() -> Endpoint.Single { 44 | Endpoint.Group.Media(identifier: self.identifier).edit("comment_like/") 45 | } 46 | 47 | /// Unlike the underlying comment. 48 | /// 49 | /// - returns: A valid `Endpoint.Single`. 50 | func unlike() -> Endpoint.Single { 51 | Endpoint.Group.Media(identifier: self.identifier).edit("comment_unlike/") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Media/Endpoint+ManyComments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+ManyComments.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 08/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group.Media { 11 | /// A `class` defining multiple comments endpoints. 12 | final class ManyComments { 13 | /// The media. 14 | public let media: Endpoint.Group.Media 15 | /// A list of comment identifiers. 16 | public let identifiers: [String] 17 | 18 | /// Init. 19 | /// 20 | /// - parameters: 21 | /// - media: A valid `Endpoint.Group.Media`. 22 | /// - identifiers: An array of `String`s. 23 | init(media: Endpoint.Group.Media, 24 | identifiers: [String]) { 25 | self.media = media 26 | self.identifiers = identifiers 27 | } 28 | } 29 | 30 | /// A wrapper for comments-specific endpoints. 31 | /// 32 | /// - parameter identifiers: A collection of `String`s. 33 | /// - returns: A valid `Endpoint.ManyComments`. 34 | func comments(_ identifiers: C) -> ManyComments where C.Element == String { 35 | .init(media: self, identifiers: Array(identifiers)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Posts/Endpoint+Posts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Posts.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 08/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `class` defining posts-related endpoints. 12 | final class Posts { } 13 | } 14 | 15 | public extension Endpoint { 16 | /// A wrapper for posts-specific endpoints. 17 | static let posts: Endpoint.Group.Posts = .init() 18 | } 19 | 20 | public extension Endpoint.Group.Posts { 21 | /// A list of archived posts. 22 | var archived: Endpoint.Paginated < Swiftagram.Media.Collection, 23 | RankedOffset, 24 | Error> { 25 | Endpoint.archived.posts 26 | } 27 | 28 | /// The logged in user's timeline. 29 | var recent: Endpoint.Paginated, Error> { 30 | Endpoint.recent.posts 31 | } 32 | 33 | /// A list of all saved posts. 34 | /// 35 | /// - note: Use `Endpoint.saved` accessories to deal with specific collections. 36 | var saved: Endpoint.Paginated { 37 | Endpoint.saved.posts 38 | } 39 | 40 | /// A list of posts liked by the logged in user. 41 | var liked: Endpoint.Paginated < Swiftagram.Media.Collection, 42 | RankedOffset, 43 | Error> { 44 | .init { secret, session, pages in 45 | // Persist the rank token. 46 | let rank = pages.rank ?? UUID().uuidString 47 | // Prepare the actual pager. 48 | return Pager(pages) { 49 | Request.feed 50 | .liked 51 | .header(appending: secret.header) 52 | .header(appending: rank, forKey: "rank_token") 53 | .query(appending: $0, forKey: "max_id") 54 | .publish(with: session) 55 | .map(\.data) 56 | .wrap() 57 | .map(Swiftagram.Media.Collection.init) 58 | .iterateFirst(stoppingAt: $0) 59 | } 60 | .replaceFailingWithError() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Saved/Endpoint+Saved.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Saved.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 19/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `struct` defining saved-related endpoints. 12 | struct Saved { } 13 | } 14 | 15 | public extension Endpoint { 16 | /// A wrapper for saved-related endpoints. 17 | /// 18 | /// - returns: A valid `Endpoint.Group.Saved`. 19 | static let saved: Endpoint.Group.Saved = .init() 20 | } 21 | 22 | public extension Endpoint.Group.Saved { 23 | /// List all saved posts reguardless of their collection. 24 | /// 25 | /// - returns: A valid `Endpoint.Paginated`. 26 | var posts: Endpoint.Paginated { 27 | .init { secret, session, pages in 28 | Pager(pages) { 29 | Request.feed 30 | .saved 31 | .appendingDefaultHeader() 32 | .header(appending: secret.header) 33 | .query(appending: ["include_igtv_preview": "true", 34 | "show_igtv_first": "false", 35 | "max_id": $0]) 36 | .publish(with: session) 37 | .map(\.data) 38 | .wrap() 39 | .map(Swiftagram.Media.Collection.init) 40 | .iterateFirst(stoppingAt: $0) 41 | } 42 | .replaceFailingWithError() 43 | } 44 | } 45 | 46 | /// List all collections. 47 | /// 48 | /// - returns: A valid `Endpoint.Paginated`. 49 | var collections: Endpoint.Paginated { 50 | .init { secret, session, pages in 51 | let types = ["ALL_MEDIA_AUTO_COLLECTION", 52 | "PRODUCT_AUTO_COLLECTION", 53 | "MEDIA", 54 | "AUDIO_AUTO_COLLECTION", 55 | "GUIDES_AUTO_COLLECTION"] 56 | .map { #""\#($0)""# } 57 | .joined(separator: ",") 58 | // Return the actual publisher. 59 | return Pager(pages) { 60 | Request.version1 61 | .collections 62 | .list 63 | .query(appending: ["max_id": $0, "collection_types": "[\(types)]"]) 64 | .appendingDefaultHeader() 65 | .header(appending: secret.header) 66 | .publish(with: session) 67 | .map(\.data) 68 | .wrap() 69 | .map(SavedCollection.Collection.init) 70 | .iterateFirst(stoppingAt: $0) 71 | } 72 | .replaceFailingWithError() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Stories/Endpoint+Stories.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Stories.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 08/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `class` defining stories-related endpoints. 12 | final class Stories { } 13 | } 14 | 15 | public extension Endpoint { 16 | /// A wrapper for stories-specific endpoints. 17 | static let stories: Endpoint.Group.Stories = .init() 18 | 19 | /// An endpoint for loading specific endpoints. 20 | /// 21 | /// - parameter identifiers: A collection of `String`s. 22 | /// - returns: A valid `Endpoint.Single`. 23 | static func stories(_ identifiers: C) -> Endpoint.Single where C.Element == String { 24 | users(identifiers).stories 25 | } 26 | } 27 | 28 | public extension Endpoint.Group.Stories { 29 | /// A list of archived stories. 30 | var archived: Endpoint.Paginated < TrayItem.Collection, 31 | RankedOffset, 32 | Error> { 33 | Endpoint.archived.stories 34 | } 35 | 36 | /// The logged in user stories tray. 37 | var recent: Endpoint.Single { 38 | Endpoint.recent.stories 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Tag/Endpoint+Tag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Tag.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 07/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `class` defining tag endpoints. 12 | final class Tag { 13 | /// The tag name. 14 | public let name: String 15 | 16 | /// Init. 17 | /// 18 | /// - parameter name: A valid `String`. 19 | init(name: String) { 20 | self.name = name 21 | } 22 | } 23 | } 24 | 25 | public extension Endpoint { 26 | /// A wrapper for tag-specific endpoints. 27 | /// 28 | /// - parameter name: A valid `String`. 29 | /// - returns: A valid `Tag`. 30 | static func tag(_ name: String) -> Group.Tag { 31 | .init(name: name) 32 | } 33 | 34 | /// A summary for the current tag. 35 | /// 36 | /// - parameter name: A valid `String`. 37 | /// - returns: A valid `Endpoint.Single`. 38 | static func tag(_ name: String) -> Endpoint.Single { 39 | tag(name).summary 40 | } 41 | } 42 | 43 | extension Request { 44 | /// A tag-related request. 45 | /// 46 | /// - parameter tag: A valid `Tag`. 47 | /// - returns: A valid `Request`. 48 | static func tag(_ tag: Endpoint.Group.Tag) -> Request { 49 | Request.version1 50 | .tags 51 | .path(appending: tag.name) 52 | .appendingDefaultHeader() 53 | } 54 | } 55 | 56 | public extension Endpoint.Group.Tag { 57 | /// A summary for the current tag. 58 | /// 59 | /// - note: Prefer `Endpoint.tag(_:)` instead. 60 | var summary: Endpoint.Single { 61 | .init { secret, session in 62 | Deferred { 63 | Request.tag(self) 64 | .path(appending: "info/") 65 | .appendingDefaultHeader() 66 | .header(appending: secret.header) 67 | .publish(with: session) 68 | .map(\.data) 69 | .wrap() 70 | .map(Swiftagram.Tag.init) 71 | } 72 | .replaceFailingWithError() 73 | } 74 | } 75 | 76 | /// A list of some recent stories for the current tag. 77 | var stories: Endpoint.Single { 78 | .init { secret, session in 79 | Deferred { 80 | Request.tag(self) 81 | .path(appending: "story/") 82 | .appendingDefaultHeader() 83 | .header(appending: secret.header) 84 | .publish(with: session) 85 | .map(\.data) 86 | .wrap() 87 | .map(TrayItem.Unit.init) 88 | } 89 | .replaceFailingWithError() 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/Tag/Endpoint+TagPosts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+TagPosts.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 19/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group.Tag { 11 | /// A `struct` defining tag-related posts endpoints. 12 | struct Posts { 13 | /// The underlying tag. 14 | public let tag: Endpoint.Group.Tag 15 | 16 | /// Init. 17 | /// 18 | /// - parameter tag: A valid `Tag`. 19 | init(tag: Endpoint.Group.Tag) { 20 | self.tag = tag 21 | } 22 | } 23 | 24 | /// A wrapper for tag-related posts edpoints. 25 | var posts: Posts { 26 | .init(tag: self) 27 | } 28 | } 29 | 30 | public extension Endpoint.Group.Tag.Posts { 31 | /// A list of recent posts. 32 | var recent: Endpoint.Paginated { 33 | .init { secret, session, pages in 34 | Pager(pages) { 35 | Request.tag(self.tag) 36 | .sections 37 | .path(appending: "/") 38 | .header(appending: secret.header) 39 | .body(appending: ["max_id": $0?.identifier, 40 | "tab": "recent", 41 | "page": ($0?.page).flatMap { $0 <= 0 ? nil : "\($0)" }, 42 | "next_media_ids": "[\($0?.mediaIdentifiers.joined(separator: ",") ?? "")]", 43 | "_csrftoken": secret["csrftoken"], 44 | "_uuid": secret.client.device.identifier.uuidString, 45 | "session_id": secret["sessionid"]].compactMapValues { $0 }) 46 | .publish(with: session) 47 | .map(\.data) 48 | .wrap() 49 | .map(Section.Collection.init) 50 | .iterateFirst(stoppingAt: $0) 51 | } 52 | .replaceFailingWithError() 53 | } 54 | } 55 | 56 | /// A list of highest ranking posts. 57 | var top: Endpoint.Paginated { 58 | .init { secret, session, pages in 59 | Pager(pages) { 60 | Request.tag(self.tag) 61 | .sections 62 | .path(appending: "/") 63 | .header(appending: secret.header) 64 | .body(appending: ["max_id": $0?.identifier, 65 | "tab": "top", 66 | "page": ($0?.page).flatMap { $0 <= 0 ? nil : "\($0)" }, 67 | "next_media_ids": "[\($0?.mediaIdentifiers.joined(separator: ",") ?? "")]", 68 | "_csrftoken": secret["csrftoken"], 69 | "_uuid": secret.client.device.identifier.uuidString, 70 | "session_id": secret["sessionid"]].compactMapValues { $0 }) 71 | .publish(with: session) 72 | .map(\.data) 73 | .wrap() 74 | .map(Section.Collection.init) 75 | .iterateFirst(stoppingAt: $0) 76 | } 77 | .replaceFailingWithError() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/User/Endpoint+ManyUsers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+ManyUsers.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 26/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `class` defining users-related endpoints. 12 | final class ManyUsers { 13 | /// The user identifiers. 14 | public let identifiers: [String] 15 | 16 | /// Init. 17 | /// 18 | /// - parameter identifiers: An array of `String`s. 19 | init(identifiers: [String]) { 20 | self.identifiers = identifiers 21 | } 22 | } 23 | } 24 | 25 | public extension Endpoint { 26 | /// A wrapper for users specific endpoints. 27 | /// 28 | /// - parameter identifiers: A collection of `String`s. 29 | /// - returns: A valid `Endpoint.ManyUsers`. 30 | static func users(_ identifiers: C) -> Group.ManyUsers where C.Element == String { 31 | .init(identifiers: Array(identifiers)) 32 | } 33 | 34 | /// A wrapper for users specific endpoints. 35 | /// 36 | /// - parameter users: A collection of `User`s. 37 | /// - returns: A valid `Endpoint.ManyUsers`. 38 | static func users(_ users: C) -> Group.ManyUsers where C.Element == Swiftagram.User { 39 | self.users(users.compactMap(\.identifier)) 40 | } 41 | } 42 | 43 | public extension Endpoint.Group.ManyUsers { 44 | /// List all friendship statuses between the list of users and the logged in one. 45 | var friendships: Endpoint.Single { 46 | .init { secret, session in 47 | Deferred { 48 | Request.friendships 49 | .path(appending: "show_many/") 50 | .header(appending: secret.header) 51 | .body(["user_ids": self.identifiers.joined(separator: ","), 52 | "_csrftoken": secret["csrftoken"], 53 | "_uuid": secret.client.device.identifier.uuidString]) 54 | .publish(with: session) 55 | .map(\.data) 56 | .wrap() 57 | .map(Swiftagram.Friendship.Dictionary.init) 58 | } 59 | .replaceFailingWithError() 60 | } 61 | } 62 | 63 | /// List all recent stories by the list of users. 64 | var stories: Endpoint.Single { 65 | .init { secret, session in 66 | Deferred { 67 | Request.version1 68 | .feed 69 | .path(appending: "reels_media/") 70 | .appendingDefaultHeader() 71 | .header(appending: secret.header) 72 | .body(["user_ids": "[\(self.identifiers.joined(separator: ","))]"]) 73 | .publish(with: session) 74 | .map(\.data) 75 | .wrap() 76 | .map(TrayItem.Dictionary.init) 77 | } 78 | .replaceFailingWithError() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Endpoints/User/Endpoint+Users.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Users.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 26/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group { 11 | /// A `class` defining users-related endpoints. 12 | final class Users { } 13 | } 14 | 15 | public extension Endpoint { 16 | /// A wrapper for users endpoints. 17 | static let users: Group.Users = .init() 18 | 19 | /// All user matching `query`. 20 | /// 21 | /// - parameter query: A `String` holding reference to a valid user query. 22 | /// - returns: A valid `Endpoint.Pagianted`. 23 | static func users(matching query: String) -> Endpoint.Paginated < Swiftagram.User.Collection, 24 | RankedOffset, 25 | Error> { 26 | .init { secret, session, pages in 27 | // Persist the rank token. 28 | let rank = pages.rank ?? UUID().uuidString 29 | // Prepare the actual pager. 30 | return Pager(pages) { 31 | Request.users 32 | .search 33 | .header(appending: secret.header) 34 | .header(appending: rank, forKey: "rank_token") 35 | .query(appending: ["q": query, "max_id": $0]) 36 | .publish(with: session) 37 | .map(\.data) 38 | .wrap() 39 | .map(Swiftagram.User.Collection.init) 40 | .iterateFirst(stoppingAt: $0) 41 | } 42 | .replaceFailingWithError() 43 | } 44 | } 45 | } 46 | 47 | public extension Endpoint.Group.Users { 48 | /// A list of all profiles blocked by the logged in user. 49 | var blocked: Endpoint.Single { 50 | .init { secret, session in 51 | Deferred { 52 | Request.users 53 | .blocked_list 54 | .header(appending: secret.header) 55 | .publish(with: session) 56 | .map(\.data) 57 | .wrap() 58 | } 59 | .eraseToAnyPublisher() 60 | } 61 | } 62 | 63 | /// A list of users who requested to follow you. 64 | var requests: Endpoint.Paginated { 65 | .init { secret, session, pages in 66 | Pager(pages) { 67 | Request.friendships 68 | .pending 69 | .header(appending: secret.header) 70 | .query(appending: $0, forKey: "max_id") 71 | .publish(with: session) 72 | .map(\.data) 73 | .wrap() 74 | .map(Swiftagram.User.Collection.init) 75 | .iterateFirst(stoppingAt: $0) 76 | } 77 | .replaceFailingWithError() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Extensions/@_exported.swift: -------------------------------------------------------------------------------- 1 | // 2 | // @_exported.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 23/02/21. 6 | // 7 | 8 | @_exported import ComposableRequest 9 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Extensions/Agnostic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Agnostic.swift 3 | // SwiftagramCrypto 4 | // 5 | // Created by Stefano Bertagno on 01/09/20. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | #elseif canImport(AppKit) && !targetEnvironment(macCatalyst) 11 | import AppKit 12 | #endif 13 | 14 | /// A module-like `enum` listing some platform agnostic commonly-used definitions. 15 | public enum Agnostic { 16 | #if canImport(UIKit) 17 | /// `UIImage`. 18 | public typealias Image = UIImage 19 | /// `UIColor`. 20 | public typealias Color = UIColor 21 | #elseif canImport(AppKit) && !targetEnvironment(macCatalyst) 22 | /// `NSImage`. 23 | public typealias Image = NSImage 24 | /// `NSColor`. 25 | public typealias Color = NSColor 26 | #endif 27 | } 28 | 29 | #if canImport(UIKit) 30 | /// An extension for `UIImage` generation from `UIColor`. 31 | public extension UIColor { 32 | /// Create a solid color `UIImage`. 33 | func image(size: CGSize) -> UIImage? { 34 | let rect = CGRect(origin: .zero, size: size) 35 | UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) 36 | self.setFill() 37 | UIRectFill(rect) 38 | let image = UIGraphicsGetImageFromCurrentImageContext() 39 | UIGraphicsEndImageContext() 40 | return image?.cgImage.flatMap(UIImage.init) 41 | } 42 | } 43 | /// An extension for `UIImage`s. 44 | public extension UIImage { 45 | /// Compute the `.jpeg` representation. 46 | func jpegRepresentation() -> Data? { jpegData(compressionQuality: 1) } 47 | } 48 | #elseif canImport(AppKit) && !targetEnvironment(macCatalyst) 49 | /// An extension for `NSImage` generation from `NSColor`. 50 | public extension NSColor { 51 | /// Create a solid color `NSImage`. 52 | func image(size: CGSize) -> NSImage? { 53 | let image = NSImage(size: size) 54 | image.lockFocus() 55 | drawSwatch(in: .init(origin: .zero, size: size)) 56 | image.unlockFocus() 57 | return image 58 | } 59 | } 60 | /// An extension for `NSImage`s. 61 | public extension NSImage { 62 | /// Compute the `.jpeg` representation. 63 | func jpegRepresentation() -> Data? { 64 | cgImage(forProposedRect: nil, context: nil, hints: nil) 65 | .flatMap(NSBitmapImageRep.init)? 66 | .representation(using: .jpeg, properties: [:]) 67 | } 68 | } 69 | #endif 70 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Extensions/HTTPCookie.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPCookie.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 29/10/20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Collection where Element: HTTPCookie { 11 | /// Check wether the user is correctly authenticated or not. 12 | var containsAuthenticationCookies: Bool { 13 | Set(map(\.name)).intersection(["ds_user_id", "sessionid", "csrftoken"]).count == 3 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Extensions/Header.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Header.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 18/05/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Header { 11 | /// Append default headers. 12 | /// 13 | /// - returns: `self` with updated header fields. 14 | /// - note: Starting from `4.2.0`, `Client` related info is no longer added through this method. 15 | func appendingDefaultHeader() -> Self { 16 | header(appending: [ 17 | "X-Ads-Opt-Out": "0", 18 | "X-CM-Bandwidth-KBPS": "-1.000", 19 | "X-CM-Latency": "-1.000", 20 | "X-IG-App-Locale": "en_US", 21 | "X-IG-Device-Locale": "en_US", 22 | "X-Pigeon-Session-Id": UUID().uuidString.lowercased(), 23 | "X-Pigeon-Rawclienttime": "\(Int(Date().timeIntervalSince1970)).000", 24 | "X-IG-Connection-Speed": "\(Int.random(in: 1_000...3_700))kbps", 25 | "X-IG-Bandwidth-Speed-KBPS": "-1.000", 26 | "X-IG-Bandwidth-TotalBytes-B": "0", 27 | "X-IG-Bandwidth-TotalTime-MS": "0", 28 | "X-IG-Extended-CDN-Thumbnail-Cache-Busting-Value": "1000", 29 | "X-Bloks-Version-Id": "7b2216598d8fcf84fbda65652788cb12be5aa024c4ea5e03deeb2b81a383c9e0", 30 | "X-IG-WWW-Claim": "0", 31 | "X-Bloks-Is-Layout-RTL": "false", 32 | "X-IG-Connection-Type": "WIFI", 33 | "X-IG-Capabilities": "36r/Fx8=", 34 | "X-IG-App-ID": "567067343352427", 35 | "Accept-Language": "en-US", 36 | "X-FB-HTTP-Engine": "Liger", 37 | "Host": "i.instagram.com", 38 | "Accept-Encoding": "gzip", 39 | "Connection": "close" 40 | ]) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Extensions/Paginatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Paginatable.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `protocol` defining a `Paginatable` instance with an optional `String` offset. 11 | public protocol StringPaginatable: Paginatable where Offset == String? { } 12 | 13 | public extension Paginatable where Self: Wrappable, Offset == String? { 14 | /// The pagination parameters. 15 | var offset: Offset { wrapped.nextMaxId.string(converting: true) } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Extensions/Publisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 03/05/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import ComposableRequest 11 | 12 | public extension Publisher where Output: Specialized { 13 | /// Consider endpoint `Error`s. 14 | /// 15 | /// - returns: Some `Publisher`. 16 | func replaceFailingWithError() -> AnyPublisher { 17 | self.catch { Fail(error: $0 as Error) } 18 | .flatMap { output -> AnyPublisher in 19 | switch output.error { 20 | case let error?: 21 | return Fail(error: error).eraseToAnyPublisher() 22 | case .none: 23 | return Just(output).setFailureType(to: Error.self).eraseToAnyPublisher() 24 | } 25 | } 26 | .eraseToAnyPublisher() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Extensions/URLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSession.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 01/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension URLSession { 11 | /// An **Instagram**-safe `URLSession`. 12 | static let instagram: URLSession = { 13 | let configuration = URLSessionConfiguration.default 14 | configuration.httpMaximumConnectionsPerHost = 1 15 | return .init(configuration: configuration) 16 | }() 17 | 18 | /// An epehemeral `URLSession`. 19 | static let ephemeral: URLSession = { 20 | .init(configuration: URLSessionConfiguration.ephemeral) 21 | }() 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Errors/AuthenticationError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewAuthenticatorError.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 22/07/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An `enum` listing all possible `Error`s in a visual based authentication process. 11 | public enum WebViewAuthenticatorError: Swift.Error { 12 | /// You need to add the web view to the view hierarchy. 13 | case emptyViewHierarchy 14 | /// Invalid cookies. 15 | case invalidCookies 16 | /// Invalid URL. 17 | case invalidURL 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comment.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 14/08/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `struct` representing a `Comment`. 11 | public struct Comment: Wrapped { 12 | /// The underlying `Response`. 13 | public var wrapper: () -> Wrapper 14 | 15 | /// The comment primary key. 16 | public var identifier: String! { self["pk"].string(converting: true) } 17 | /// The `text` value. 18 | public var text: String! { self["text"].string() } 19 | /// The `commentLikeCount` value. 20 | public var likes: Int? { self["commentLikeCount"].int() } 21 | /// The `user` value. 22 | public var user: User? { 23 | (self["user"].optional() ?? self["owner"].optional()) 24 | .flatMap(User.init) 25 | } 26 | 27 | /// Init. 28 | /// - parameter wrapper: A valid `Wrapper`. 29 | public init(wrapper: @escaping () -> Wrapper) { 30 | self.wrapper = wrapper 31 | } 32 | } 33 | 34 | public extension Comment { 35 | /// A `struct` representing a `Comment` collection. 36 | struct Collection: Specialized, StringPaginatable { 37 | /// The underlying `Wrapper`. 38 | public var wrapper: () -> Wrapper 39 | 40 | /// The comments. 41 | public var comments: [Comment]? { 42 | (self["comments"].optional() ?? self["previewComments"].optional())? 43 | .array()? 44 | .map(Comment.init) 45 | } 46 | 47 | /// Init. 48 | /// - parameter wrapper: A valid `Wrapper`. 49 | public init(wrapper: @escaping () -> Wrapper) { 50 | self.wrapper = wrapper 51 | } 52 | } 53 | 54 | /// A `struct` representing a `Comment` unit. 55 | struct Unit: Specialized { 56 | /// The underlying `Wrapper`. 57 | public var wrapper: () -> Wrapper 58 | 59 | /// The comment. 60 | public var comment: Comment? { 61 | self["comment"].optional().flatMap(Comment.init) 62 | } 63 | 64 | /// Init. 65 | /// - parameter wrapper: A valid `Wrapper`. 66 | public init(wrapper: @escaping () -> Wrapper) { 67 | self.wrapper = wrapper 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/Conversation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conversation.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 10/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `struct` representing a `Conversation`. 11 | public struct Conversation: Wrapped { 12 | /// The underlying `Response`. 13 | public var wrapper: () -> Wrapper 14 | 15 | /// The identifier. 16 | public var identifier: String! { self["threadId"].string(converting: true) } 17 | /// The title. 18 | public var title: String! { self["threadTitle"].string() } 19 | /// Last update. 20 | public var updatedAt: Date? { self["lastActivityAt"].date() } 21 | /// Last seen for `identifier`. 22 | public var openedAt: [String: Date]? { 23 | self["lastSeenAt"].dictionary()?.compactMapValues { $0.timestamp.date() } 24 | } 25 | /// Muted. 26 | public var hasMutedMessages: Bool? { self["muted"].bool() } 27 | /// Muted videocalls. 28 | public var hasMutedVideocalls: Bool? { self["vcMuted"].bool() } 29 | /// Users. 30 | public var users: [User]? { self["users"].array()?.map(User.init) } 31 | 32 | /// The actual messages. 33 | public var messages: [Wrapper]? { self["items"].array() } 34 | 35 | /// Init. 36 | /// - parameter wrapper: A valid `Wrapper`. 37 | public init(wrapper: @escaping () -> Wrapper) { 38 | self.wrapper = wrapper 39 | } 40 | } 41 | 42 | public extension Conversation { 43 | /// A `struct` representing a `Conversation` single response. 44 | struct Unit: Specialized, StringPaginatable { 45 | /// The underlying `Response`. 46 | public var wrapper: () -> Wrapper 47 | 48 | /// The thread. 49 | public var conversation: Conversation? { self["thread"].optional().flatMap(Conversation.init) } 50 | 51 | /// The pagination parameters. 52 | public var offset: String? { self["thread"]["oldestCursor"].string(converting: true) } 53 | 54 | /// Init. 55 | /// - parameter wrapper: A valid `Wrapper`. 56 | public init(wrapper: @escaping () -> Wrapper) { 57 | self.wrapper = wrapper 58 | } 59 | } 60 | 61 | /// A `struct` representing a `Conversation` collection. 62 | struct Collection: Specialized, StringPaginatable { 63 | /// The underlying `Response`. 64 | public var wrapper: () -> Wrapper 65 | 66 | /// The threads. 67 | public var conversations: [Conversation]? { self["inbox"].threads.array()?.map(Conversation.init) } 68 | /// The logged in user. 69 | public var viewer: User? { self["viewer"].optional().flatMap(User.init) } 70 | 71 | /// The pagination parameters. 72 | public var offset: String? { self["inbox"]["oldestCursor"].string(converting: true) } 73 | 74 | /// Init. 75 | /// - parameter wrapper: A valid `Wrapper`. 76 | public init(wrapper: @escaping () -> Wrapper) { 77 | self.wrapper = wrapper 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/Friendship.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Friendship.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 31/07/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `struct` representing a `Friendship`. 11 | public struct Friendship: Wrapped { 12 | /// The underlying `Response`. 13 | public var wrapper: () -> Wrapper 14 | 15 | /// Whether they're followed by the logged in user or not. 16 | public var isFollowedByYou: Bool? { self["following"].bool() } 17 | /// Whether they follow the logged in user or not. 18 | public var isFollowingYou: Bool? { self["followedBy"].bool() } 19 | /// Whether they're blocked by the logged in user or not. 20 | public var isBlockedByYou: Bool? { self["blocking"].bool() } 21 | /// Whether they're in the logged in user's close firends list or not. 22 | public var isCloseFriend: Bool? { self["isBestie"].bool() } 23 | /// Whether they've requested to follow the logged in user or not. 24 | public var didRequestToFollowYou: Bool? { self["incomingRequest"].bool() } 25 | /// Whether the logged in user have requested to follow them or not. 26 | public var didRequestToFollow: Bool? { self["outgoingRequest"].bool() } 27 | 28 | /// Whether the logged in user is muting their stories. 29 | public var isMutingStories: Bool? { self["isMutingReel"].bool() } 30 | /// Whether the logged in user is muting their posts. 31 | public var isMutingPosts: Bool? { self["muting"].bool() } 32 | 33 | /// Init. 34 | /// - parameter wrapper: A valid `Wrapper`. 35 | public init(wrapper: @escaping () -> Wrapper) { 36 | self.wrapper = wrapper 37 | } 38 | } 39 | 40 | public extension Friendship { 41 | /// A `struct` representing a `Friendship` collection. 42 | struct Dictionary: Specialized { 43 | /// The underlying `Response`. 44 | public var wrapper: () -> Wrapper 45 | 46 | /// The friendships. 47 | public var friendships: [String: Friendship]! { 48 | self["friendshipStatuses"].dictionary()?.mapValues { Friendship(wrapper: $0) } 49 | } 50 | 51 | /// Init. 52 | /// - parameter wrapper: A valid `Wrapper`. 53 | public init(wrapper: @escaping () -> Wrapper) { 54 | self.wrapper = wrapper 55 | } 56 | } 57 | 58 | /// A `struct` representing a single `Friendship` value. 59 | struct Unit: Specialized { 60 | /// The underlying `Response`. 61 | public var wrapper: () -> Wrapper 62 | 63 | /// The friendship. 64 | public var friendship: Friendship? { 65 | self["friendshipStatus"].optional().flatMap(Friendship.init) 66 | } 67 | 68 | /// Init. 69 | /// - parameter wrapper: A valid `Wrapper`. 70 | public init(wrapper: @escaping () -> Wrapper) { 71 | self.wrapper = wrapper 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // SwiftagramCrypto 4 | // 5 | // Created by Stefano Bertagno on 30/07/20. 6 | // 7 | 8 | import CoreGraphics 9 | import Foundation 10 | 11 | /// A `class` representing a `Location` 12 | public struct Location: Wrapped { 13 | /// A `struct` holding reference to longitude and latitude. 14 | public struct Coordinates: Equatable { 15 | /// The longitude. 16 | public var longitude: CGFloat 17 | /// The latitude. 18 | public var latitude: CGFloat 19 | 20 | /// Init. 21 | /// - parameters: 22 | /// - latitude: A `CGFloat` repreenting the latitude. 23 | /// - longitude: A `CGFloat` repreenting the longitude. 24 | public init(latitude: CGFloat, longitude: CGFloat) { 25 | self.latitude = latitude 26 | self.longitude = longitude 27 | } 28 | } 29 | 30 | /// The underlying `Response`. 31 | public var wrapper: () -> Wrapper 32 | 33 | /// The latitude. 34 | public var coordinates: Coordinates? { 35 | guard let latitude = self["lat"].double().flatMap(CGFloat.init), 36 | let longitude = self["lng"].double().flatMap(CGFloat.init) else { return nil } 37 | return .init(latitude: latitude, longitude: longitude) 38 | } 39 | /// The name. 40 | public var name: String? { self["name"].string() } 41 | /// The short name. Only populated for `summary`. 42 | public var shortName: String? { self["shortName"].string() } 43 | /// The address. 44 | public var address: String? { self["address"].string() } 45 | /// The city. Only populated for `summary`. 46 | public var city: String? { self["city"].string() } 47 | /// The external id (`value`), paired with its source (`key`). 48 | public var identifier: [String: Int]? { 49 | if let source = self["externalIdSource"].string(), 50 | let identifier = self["externalId"].int() { 51 | return [source: identifier] 52 | } else if let source = self["externalSource"].string() { 53 | return [source: self[source.camelCased + "Id"].int()].compactMapValues { $0 } 54 | } else { 55 | return nil 56 | } 57 | } 58 | 59 | /// Init. 60 | /// - parameter wrapper: A valid `Wrapper`. 61 | public init(wrapper: @escaping () -> Wrapper) { 62 | self.wrapper = wrapper 63 | } 64 | } 65 | 66 | public extension Location { 67 | /// A `struct` representing a single `Location` response. 68 | struct Unit: Specialized { 69 | /// The underlying `Response`. 70 | public var wrapper: () -> Wrapper 71 | 72 | /// The location. 73 | public var location: Location? { self["location"].optional().flatMap(Location.init) } 74 | 75 | /// Init. 76 | /// - parameter wrapper: A valid `Wrapper`. 77 | public init(wrapper: @escaping () -> Wrapper) { 78 | self.wrapper = wrapper 79 | } 80 | } 81 | 82 | /// A `struct` representing a `Location` collection. 83 | struct Collection: Specialized, StringPaginatable { 84 | /// The underlying `Response`. 85 | public var wrapper: () -> Wrapper 86 | 87 | /// The venues. 88 | public var venues: [Location]? { self["venues"].array()?.map(Location.init) } 89 | 90 | /// Init. 91 | /// - parameter wrapper: A valid `Wrapper`. 92 | public init(wrapper: @escaping () -> Wrapper) { 93 | self.wrapper = wrapper 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/Recipient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Recipient.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 10/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An `enum` holding reference to either a `User` or a `Conversation` instance. 11 | public enum Recipient: Wrapped { 12 | /// A valid `User`. 13 | case user(User) 14 | /// A valid `Thread`. 15 | case thread(Conversation) 16 | /// Neither, meaning something went wrong. 17 | case error(Wrapper) 18 | 19 | /// The underlying `Response`. 20 | public var wrapper: () -> Wrapper { 21 | switch self { 22 | case .user(let user): 23 | return user.wrapper 24 | case .thread(let thread): 25 | return thread.wrapper 26 | case .error(let error): 27 | return { error } 28 | } 29 | } 30 | 31 | /// Init. 32 | /// - parameter wrapper: A valid `Wrapper`. 33 | public init(wrapper: @escaping () -> Wrapper) { 34 | let response = wrapper() 35 | switch response.dictionary()?.keys.first { 36 | case "thread": 37 | self = .thread(.init(wrapper: response["thread"])) 38 | case "user": 39 | self = .user(.init(wrapper: response["user"])) 40 | default: 41 | self = .error(response) 42 | } 43 | } 44 | } 45 | 46 | public extension Recipient { 47 | /// A `struct` representing a `Recipient` collection. 48 | struct Collection: Specialized, StringPaginatable { 49 | /// The underlying `Response`. 50 | public var wrapper: () -> Wrapper 51 | 52 | /// The recipients. 53 | public var recipients: [Recipient]? { self["rankedRecipients"].array()?.map(Recipient.init) } 54 | 55 | /// Init. 56 | /// - parameter wrapper: A valid `Wrapper`. 57 | public init(wrapper: @escaping () -> Wrapper) { 58 | self.wrapper = wrapper 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/SavedCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SavedCollection.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 19/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `struct` representing a `SavedCollection`. 11 | public struct SavedCollection: Wrapped { 12 | /// The underlying `Response`. 13 | public var wrapper: () -> Wrapper 14 | 15 | /// The primary key. 16 | public var identifier: String! { self["collectionId"].string(converting: true) } 17 | 18 | /// The name. 19 | public var name: String! { self["collectionName"].string() } 20 | 21 | /// The collection type. 22 | /// 23 | /// - note: Only populated when fetching all collections. 24 | public var type: String? { self["collectionMediaType"].string() } 25 | 26 | /// The collection media count. 27 | /// 28 | /// - note: Only populated when fetching all collections. 29 | public var count: Int? { self["collectionMediaCount"].int() } 30 | 31 | /// The cover media items. 32 | /// 33 | /// - note: Only populated when fetching all collections. 34 | public var cover: [Media]? { 35 | self["coverMediaList"].array()?.compactMap(Media.init) ?? 36 | self["coverMedia"].optional().flatMap { [Media.init(wrapper: $0)] } 37 | } 38 | 39 | /// Media in the response. 40 | /// 41 | /// - note: Only populated when fetching a single collection. 42 | public var items: [Media]? { 43 | self["items"].array()?.compactMap { $0.media.optional().flatMap(Media.init) } 44 | } 45 | 46 | /// Init. 47 | /// - parameter wrapper: A valid `Wrapper`. 48 | public init(wrapper: @escaping () -> Wrapper) { 49 | self.wrapper = wrapper 50 | } 51 | } 52 | 53 | public extension SavedCollection { 54 | /// A `struct` defining a collection of `SavedCollection`. 55 | struct Collection: Specialized, StringPaginatable { 56 | /// The underlying `Response`. 57 | public var wrapper: () -> Wrapper 58 | 59 | /// The collections. 60 | public var collections: [SavedCollection]? { 61 | self["items"] 62 | .array()? 63 | .compactMap(SavedCollection.init) 64 | } 65 | 66 | /// Init. 67 | /// - parameter wrapper: A valid `Wrapper`. 68 | public init(wrapper: @escaping () -> Wrapper) { 69 | self.wrapper = wrapper 70 | } 71 | } 72 | 73 | /// A `struct` defining a single `SavedCollection` response. 74 | struct Unit: Specialized, StringPaginatable { 75 | /// The underlying `Response`. 76 | public var wrapper: () -> Wrapper 77 | 78 | /// The collection. 79 | public var collection: SavedCollection? { 80 | self["saveMediaResponse"] 81 | .optional() 82 | .flatMap(SavedCollection.init) 83 | } 84 | 85 | /// Media in the response. 86 | /// 87 | /// - note: Only populated when fetching posts and igtvs. 88 | public var items: [Media]? { 89 | self["items"].array()?.compactMap { 90 | $0.media.optional().flatMap(Media.init) ?? 91 | $0.optional().flatMap(Media.init) 92 | } 93 | } 94 | 95 | /// The offset. 96 | public var offset: String? { 97 | self["saveMediaResponse"].nextMaxId.string(converting: true) 98 | ?? self["nextMaxId"].string(converting: true) 99 | ?? self["maxId"].string(converting: true) 100 | } 101 | 102 | /// Init. 103 | /// - parameter wrapper: A valid `Wrapper`. 104 | public init(wrapper: @escaping () -> Wrapper) { 105 | self.wrapper = wrapper 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/Section.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Section.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 19/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `struct` defining a valid tag/location section. 11 | public struct Section: Wrapped { 12 | /// The underlying `Response`. 13 | public var wrapper: () -> Wrapper 14 | 15 | /// Media in the response. 16 | public var items: [Media]? { 17 | self["layoutContent"] 18 | .fillItems 19 | .array()? 20 | .compactMap { $0.media.optional().flatMap(Media.init) } 21 | ?? self["layoutContent"] 22 | .medias 23 | .array()? 24 | .compactMap { $0.media.optional().flatMap(Media.init) } 25 | } 26 | 27 | /// Init. 28 | /// - parameter wrapper: A valid `Wrapper`. 29 | public init(wrapper: @escaping () -> Wrapper) { 30 | self.wrapper = wrapper 31 | } 32 | } 33 | 34 | public extension Section { 35 | /// A `struct` defining a valid posts offset. 36 | struct Offset: Equatable { 37 | /// Current max identifier. 38 | public let identifier: String 39 | /// Current page. 40 | public let page: Int? 41 | /// Current media identifiers. 42 | public let mediaIdentifiers: [String] 43 | 44 | /// Init. 45 | /// 46 | /// - parameters: 47 | /// - identifier: A valid `String`. 48 | /// - page: An optional `Int`. 49 | /// - mediaIdentifiers: An array of `String`s. 50 | /// - note: You should not build this directly. 51 | public init(identifier: String, 52 | page: Int?, 53 | mediaIdentifiers: [String]) { 54 | self.identifier = identifier 55 | self.page = page 56 | self.mediaIdentifiers = mediaIdentifiers 57 | } 58 | } 59 | 60 | /// A `struct` defining a collection of `Section`s. 61 | struct Collection: Specialized, Paginatable { 62 | /// The underlying `Response`. 63 | public var wrapper: () -> Wrapper 64 | 65 | /// All available sections. 66 | public var sections: [Section]? { 67 | self["sections"].array()?.compactMap(Section.init) 68 | } 69 | 70 | /// The offset. 71 | public var offset: Section.Offset? { 72 | guard self["moreAvailable"].bool() ?? false, 73 | let identifier = self["nextMaxId"].string(converting: true) else { 74 | return nil 75 | } 76 | // Parse all data. 77 | let page = self["nextPage"].int() 78 | let mediaIdentifiers = self["nextMediaIds"].array()?.compactMap { $0.string(converting: true) } ?? [] 79 | return .init(identifier: identifier, page: page, mediaIdentifiers: mediaIdentifiers) 80 | } 81 | 82 | /// Init. 83 | /// - parameter wrapper: A valid `Wrapper`. 84 | public init(wrapper: @escaping () -> Wrapper) { 85 | self.wrapper = wrapper 86 | } 87 | } 88 | } 89 | 90 | public extension Section.Collection { 91 | /// The associated offset. 92 | typealias Offset = Section.Offset? 93 | } 94 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/Specialized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Specialized.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 26/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An `enum` holding reference to possible `Error`s in the response. 11 | public enum SpecializedError: Error { 12 | /// A generic `Error`. 13 | case generic(String, response: Wrapper) 14 | /// Unforseen `status`. 15 | /// Check the underlying `Wrapper` to find out more. 16 | case unforseen(String?, response: Wrapper) 17 | /// The `status` was marked as `fail`, but no `message` was provided. 18 | /// Check the underlying `Wrapper` to find out more. 19 | case unknown(response: Wrapper) 20 | } 21 | 22 | /// A `protocol` describing a generic response returning an element of `Response`. 23 | public protocol Specialized: Wrapped { 24 | /// An optional `SpecializedError` message returned by a response. 25 | /// Default emplementation returns failing description, if it exists, 26 | /// otherwise `.unknown` if `status` is not `ok`, and `nil` if it is. 27 | var error: SpecializedError? { get } 28 | } 29 | 30 | public extension Specialized { 31 | /// The response status. 32 | @available(*, deprecated, message: "check for `error` instead (removing in 6.0)") 33 | var status: String! { self["status"].string() } 34 | 35 | /// An optional `SpecializedError` message returned by a response. 36 | /// It returns the failing description, if it exists, otherwise `.unknown` if `status` is not `ok`, and `nil` if it is. 37 | var error: SpecializedError? { 38 | switch self["status"].string() { 39 | case "ok": 40 | return nil 41 | case "fail": 42 | return self["message"].string().flatMap { .generic($0, response: self.wrapped) } 43 | ?? .unknown(response: self.wrapped) 44 | case let status: 45 | return .unforseen(status, response: self.wrapped) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/Status.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Status.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 31/07/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `struct` representing a `Status`. 11 | public struct Status: Specialized { 12 | /// The underlying `Response`. 13 | public var wrapper: () -> Wrapper 14 | 15 | /// Init. 16 | /// - parameter wrapper: A valid `Wrapper`. 17 | public init(wrapper: @escaping () -> Wrapper) { 18 | self.wrapper = wrapper 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/Tag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tag.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 20/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `struct` defining a tag instance. 11 | public struct Tag: Specialized { 12 | /// The underlying `Response`. 13 | public var wrapper: () -> Wrapper 14 | 15 | /// The identifier. 16 | public var identifier: String! { 17 | self["id"].string(converting: true) 18 | } 19 | 20 | /// The name. 21 | public var name: String! { 22 | self["name"].string(converting: true) 23 | } 24 | 25 | /// The amount of posts. 26 | public var count: Int! { 27 | self["mediaCount"].int() 28 | } 29 | 30 | /// Whether you're following it or not. 31 | public var isFollowed: Bool? { 32 | self["following"].bool() 33 | } 34 | 35 | /// Init. 36 | /// - parameter wrapper: A valid `Wrapper`. 37 | public init(wrapper: @escaping () -> Wrapper) { 38 | self.wrapper = wrapper 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/TrayItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrayItem.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 03/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A `struct` representing a `TrayItem`. 11 | public struct TrayItem: Wrapped { 12 | /// The underlying `Response`. 13 | public var wrapper: () -> Wrapper 14 | 15 | /// The identifier. 16 | public var identifier: String! { self["id"].string(converting: true) } 17 | 18 | /// The ranked position. 19 | public var position: Int? { self["rankedPosition"].int() } 20 | /// The seen ranked position. 21 | public var seenPosition: Int? { self["seenRankedPosition"].int() } 22 | 23 | /// The media count. 24 | public var availableCount: Int? { self["mediaCount"].int() } 25 | /// The count of media that have actually been fetched. 26 | public var fetchedCount: Int? { self["prefetchCount"].int() } 27 | 28 | /// The title, main timestamp of the tray item or author username. 29 | public var title: String? { 30 | self["title"].string() 31 | ?? self["timestamp"].string(converting: true) 32 | ?? user?.username 33 | } 34 | 35 | /// The cover media. 36 | public var cover: Media? { self["coverMedia"].optional().flatMap(Media.init) } 37 | 38 | /// The actual content. 39 | public var items: [Media]? { self["items"].array()?.map(Media.init) } 40 | 41 | /// The expiration date of the tray element, if it exists. 42 | public var expiringAt: Date? { 43 | self["expiringAt"].int() == 0 ? nil : self["expiringAt"].date() 44 | } 45 | 46 | /// The latest reel media date, if it exists. 47 | public var publishedAt: Date? { 48 | self["latestReelMedia"].int() == 0 ? nil : self["latestReelMedia"].date() 49 | } 50 | 51 | /// The date you last opened the tray element, if it exists. 52 | public var seenAt: Date? { 53 | self["seen"].int() == 0 ? nil : self["seen"].date() 54 | } 55 | 56 | /// The user. 57 | public var user: User? { 58 | self["user"].optional().flatMap { User(wrapper: $0) } 59 | } 60 | 61 | /// Whether it's muted or not. 62 | public var isMuted: Bool? { 63 | self["muted"].bool() ?? user?.friendship?.isMutingStories 64 | } 65 | 66 | /// Whether the tray has video content. 67 | public var containsVideos: Bool? { 68 | self["hasVideo"].bool() 69 | } 70 | 71 | /// Whether the tray has content the logged in user can see being a close friend. 72 | public var containsCloseFriendsExclusives: Bool? { 73 | self["hasBestiesMedia"].bool() 74 | } 75 | 76 | /// Init. 77 | /// - parameter wrapper: A valid `Wrapper`. 78 | public init(wrapper: @escaping () -> Wrapper) { 79 | self.wrapper = wrapper 80 | } 81 | } 82 | 83 | public extension TrayItem { 84 | /// A `struct` representing a `TrayItem` single response. 85 | struct Unit: Specialized { 86 | /// The underlying `Response`. 87 | public var wrapper: () -> Wrapper 88 | 89 | /// The tray item. 90 | public var item: TrayItem? { 91 | (wrapper()["story"].optional() 92 | ?? wrapper()["reel"].optional() 93 | ?? wrapper()["item"].optional() 94 | ?? wrapper().optional()) 95 | .flatMap(TrayItem.init) 96 | } 97 | 98 | /// Init. 99 | /// - parameter wrapper: A valid `Wrapper`. 100 | public init(wrapper: @escaping () -> Wrapper) { 101 | self.wrapper = wrapper 102 | } 103 | } 104 | 105 | /// A `struct` representing a `TrayItem` collection. 106 | struct Collection: Specialized, StringPaginatable { 107 | /// The underlying `Response`. 108 | public var wrapper: () -> Wrapper 109 | 110 | /// The items. 111 | public var items: [TrayItem]? { 112 | (self["tray"].array() ?? self["items"].array())?.map(TrayItem.init) 113 | } 114 | 115 | /// Init. 116 | /// - parameter wrapper: A valid `Wrapper`. 117 | public init(wrapper: @escaping () -> Wrapper) { 118 | self.wrapper = wrapper 119 | } 120 | } 121 | 122 | /// A `struct` representing a `TrayItem` dictionary. 123 | struct Dictionary: Specialized { 124 | /// The underlying `Response`. 125 | public var wrapper: () -> Wrapper 126 | 127 | /// The items. 128 | public var items: [String: TrayItem]? { 129 | self["reels"].dictionary()?.compactMapValues { $0.optional().flatMap(TrayItem.init) } 130 | } 131 | 132 | /// Init. 133 | /// - parameter wrapper: A valid `Wrapper`. 134 | public init(wrapper: @escaping () -> Wrapper) { 135 | self.wrapper = wrapper 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/Swiftagram/Models/Specialized/UserTag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserTag.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 01/08/20. 6 | // 7 | 8 | import CoreGraphics 9 | import Foundation 10 | 11 | /// A `struct` representing a `UserTag`. 12 | public struct UserTag: Wrapped { 13 | /// The underlying `Response`. 14 | public var wrapper: () -> Wrapper 15 | 16 | /// The user identifier. 17 | public var identifier: String! { self["userId"].string(converting: true) } 18 | /// The x relative position inside the canvas. 19 | public var x: CGFloat! { self["position"][0].double().flatMap(CGFloat.init) } 20 | /// The y relative position inside the canvas. 21 | public var y: CGFloat! { self["position"][1].double().flatMap(CGFloat.init) } 22 | 23 | /// Init. 24 | /// - parameter wrapper: A valid `Wrapper`. 25 | public init(wrapper: @escaping () -> Wrapper) { 26 | self.wrapper = wrapper 27 | } 28 | 29 | /// Init. 30 | /// - parameters: 31 | /// - x: A `CGFloat`. Values are adjusted to fall between `0.001` and `0.999`. 32 | /// - y: A `CGFloat`. Values are adjusted to fall between `0.001` and `0.999`. 33 | /// - identifier: A `String` representing a user identifier. 34 | public init(x: CGFloat, y: CGFloat, identifier: String) { 35 | self.init(wrapper: ["position": [max(0.001, min(Double(x), 0.999)).wrapped, 36 | max(0.001, min(Double(y), 0.999)).wrapped], 37 | "userId": identifier.wrapped]) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SwiftagramCrypto/Authentication/Authenticator+Keychain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticator+Keychain.swift 3 | // SwiftagramCrypto 4 | // 5 | // Created by Stefano Bertagno on 09/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import ComposableStorageCrypto 11 | 12 | /// A `typealias` for `ComposableStorageCrypto.KeychainStorage`. 13 | /// 14 | /// - note: 15 | /// We prefer this to `import @_exported`, as we can't guarantee `@_exported` 16 | /// to stick with future versions of **Swift**. 17 | public typealias KeychainStorage = ComposableStorageCrypto.KeychainStorage 18 | 19 | public extension Authenticator { 20 | /// The default keychain-backed `Authenticator`. 21 | static var keychain: Authenticator { 22 | keychain(.init()) 23 | } 24 | 25 | /// A keychain-backed `Authenticator`. 26 | /// 27 | /// - parameter keychain: A valid `KeychainStorage`. 28 | /// - returns: A valid `Authenticator.` 29 | static func keychain(_ keychainStorage: KeychainStorage) -> Authenticator { 30 | self.init(storage: keychainStorage) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SwiftagramCrypto/Authentication/Basic/Authenticator+TwoFactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticator+TwoFactor.swift 3 | // SwiftagramCrypto 4 | // 5 | // Created by Stefano Bertagno on 10/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import ComposableStorage 11 | 12 | public extension Authenticator.Group.Basic { 13 | /// A `struct` defining an instance capable of 14 | /// resolving a two factor authentication challenge. 15 | struct TwoFactor: Authentication { 16 | /// The storage. 17 | public let storage: AnyStorage 18 | /// The client. 19 | public let client: Client 20 | /// The two factor authentication identfiier. 21 | private let identifier: String 22 | /// The code. 23 | public let code: String 24 | /// The username. 25 | public let username: String 26 | /// The cross stie request forgery token. 27 | public let crossSiteRequestForgery: HTTPCookie 28 | 29 | /// Init. 30 | /// 31 | /// - parameters: 32 | /// - twoFactor: A valid `Authenticator.Error.TwoFactor`. 33 | /// - code: A valid `String`. 34 | fileprivate init(twoFactor: Authenticator.Error.TwoFactor, 35 | code: String) { 36 | self.storage = twoFactor.storage 37 | self.client = twoFactor.client 38 | self.identifier = twoFactor.identifier 39 | self.code = code 40 | self.username = twoFactor.username 41 | self.crossSiteRequestForgery = twoFactor.crossSiteRequestForgery 42 | } 43 | 44 | /// Authenticate the given user. 45 | /// 46 | /// - parameters: 47 | /// - username: A valid `String`. 48 | /// - encryptedPassword: A valid `String`. 49 | /// - cookies: An array of `HTTPCookie`s. 50 | /// - client: A valid `Client`. 51 | /// - returns: Some `Publisher`. 52 | public func authenticate() -> AnyPublisher { 53 | Request.version1 54 | .accounts 55 | .path(appending: "two_factor_login/") 56 | .appendingDefaultHeader() 57 | .header(appending: HTTPCookie.requestHeaderFields(with: [crossSiteRequestForgery])) 58 | .header(appending: ["X-IG-Device-ID": client.device.identifier.uuidString.lowercased(), 59 | "X-IG-Android-ID": client.device.instagramIdentifier, 60 | "User-Agent": client.description, 61 | "X-Csrf-Token": crossSiteRequestForgery.value]) 62 | .signing(body: [ 63 | "username": username, 64 | "verification_code": code, 65 | "_csrftoken": crossSiteRequestForgery.value, 66 | "two_factor_identifier": identifier, 67 | "trust_this_device": "1", 68 | "guid": client.device.identifier.uuidString, 69 | "device_id": client.device.instagramIdentifier, 70 | "verification_method": "1" 71 | ]) 72 | .publish(session: .ephemeral) 73 | .tryMap { result throws -> Secret in 74 | let value = try Wrapper.decode(result.data) 75 | guard value.isEmpty, let response = result.response as? HTTPURLResponse else { 76 | throw Authenticator.Error.invalidResponse(result.response) 77 | } 78 | // Prepare the actual `Secret`. 79 | if let error = value.errorType.string() { 80 | throw Authenticator.Error.generic(error) 81 | } else if value.loggedInUser.pk.int() != nil, 82 | let url = URL(string: "https://instagram.com"), 83 | let header = response.allHeaderFields as? [String: String] { 84 | let cookies = HTTPCookie.cookies(withResponseHeaderFields: header, for: url) 85 | guard let secret = Secret(cookies: cookies, client: self.client) else { 86 | throw Authenticator.Error.invalidResponse(result.response) 87 | } 88 | return try AnyStorage.store(secret, in: self.storage) 89 | } else { 90 | throw Authenticator.Error.invalidResponse(result.response) 91 | } 92 | } 93 | .eraseToAnyPublisher() 94 | } 95 | } 96 | } 97 | 98 | public extension Authenticator.Error.TwoFactor { 99 | /// Update the code for the 2FA challenge. 100 | /// 101 | /// - parameter code: A valid `String`. 102 | /// - returns: A valid `Authenticator.Group.Basic.TwoFactor`. 103 | func code(_ code: String) -> Authenticator.Group.Basic.TwoFactor { 104 | .init(twoFactor: self, code: code) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/SwiftagramCrypto/Endpoints/Endpoint+Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Comment.swift 3 | // SwiftagramCrypto 4 | // 5 | // Created by Stefano Bertagno on 07/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group.Media.Comment { 11 | /// Delete the current comment. 12 | /// 13 | /// - returns: A valid `Endpoint.Single`. 14 | func delete() -> Endpoint.Single { 15 | media.comments([identifier]).delete() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftagramCrypto/Endpoints/Endpoint+ManyComments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+ManyComments.swift 3 | // SwiftagramCrypto 4 | // 5 | // Created by Stefano Bertagno on 07/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group.Media.ManyComments { 11 | /// Delete all selected comments. 12 | /// 13 | /// - returns: A valid `Endpoint.Single`. 14 | func delete() -> Endpoint.Single { 15 | .init { secret, session in 16 | Deferred { 17 | Request.media 18 | .path(appending: self.media.identifier) 19 | .path(appending: "comment/bulk_delete/") 20 | .header(appending: secret.header) 21 | .signing(body: [ 22 | "comment_ids_to_delete": self.identifiers.joined(separator: ","), 23 | "_csrftoken": secret["csrftoken"], 24 | "_uid": secret.identifier, 25 | "_uuid": secret.client.device.identifier.uuidString 26 | ]) 27 | .publish(with: session) 28 | .map(\.data) 29 | .wrap() 30 | .map(Status.init) 31 | } 32 | .replaceFailingWithError() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftagramCrypto/Endpoints/Endpoint+Tag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+Tag.swift 3 | // SwiftagramCrypto 4 | // 5 | // Created by Stefano Bertagno on 20/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Endpoint.Group.Tag { 11 | /// Follow the current tag. 12 | /// 13 | /// - returns: A valid `Endpoint.Single`. 14 | func follow() -> Endpoint.Single { 15 | .init { secret, session in 16 | Deferred { 17 | Request.version1 18 | .tags 19 | .follow 20 | .path(appending: self.name) 21 | .path(appending: "/") 22 | .appendingDefaultHeader() 23 | .header(appending: secret.header) 24 | .signing(body: ["_csrftoken": secret["csrftoken"], 25 | "_uid": secret.identifier, 26 | "_uuid": secret.client.device.identifier.uuidString]) 27 | .publish(with: session) 28 | .map(\.data) 29 | .wrap() 30 | .map(Status.init) 31 | } 32 | .replaceFailingWithError() 33 | } 34 | } 35 | 36 | /// Unfollow the current tag. 37 | /// 38 | /// - returns: A valid `Endpoint.Single`. 39 | func unfollow() -> Endpoint.Single { 40 | .init { secret, session in 41 | Deferred { 42 | Request.version1 43 | .tags 44 | .unfollow 45 | .path(appending: self.name) 46 | .path(appending: "/") 47 | .appendingDefaultHeader() 48 | .header(appending: secret.header) 49 | .signing(body: ["_csrftoken": secret["csrftoken"], 50 | "_uid": secret.identifier, 51 | "_uuid": secret.client.device.identifier.uuidString]) 52 | .publish(with: session) 53 | .map(\.data) 54 | .wrap() 55 | .map(Status.init) 56 | } 57 | .replaceFailingWithError() 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/SwiftagramCrypto/Extensions/@_exported.swift: -------------------------------------------------------------------------------- 1 | // 2 | // @_exported.swift 3 | // SwiftagramCrypto 4 | // 5 | // Created by Stefano Bertagno on 23/02/21. 6 | // 7 | 8 | @_exported import Swiftagram 9 | -------------------------------------------------------------------------------- /Sources/SwiftagramCrypto/Extensions/Crypto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Crypto.swift 3 | // SwiftagramCrypto 4 | // 5 | // Created by Stefano Bertagno on 16/04/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | import SwCrypt 11 | import Swiftagram 12 | 13 | /// An `enum` listing all possible `Error`s in the signing process. 14 | public enum SigningError: Error { 15 | /// Cryptography unavailable. 16 | case cryptographyUnavailable 17 | /// Invalid `JSON` representation. 18 | case invalidRepresentation 19 | } 20 | 21 | extension Int { 22 | /// Breadcrumb. 23 | var breadcrumb: String { 24 | let term = Int.random(in: 2...3) * 1_000 + self + Int.random(in: 15...20) * 1_000 25 | var textChangeEventCount = round(Double(self) / Double.random(in: 2...3)) 26 | if textChangeEventCount == 0 { textChangeEventCount = 1 } 27 | let text = "\(self) \(term) \(textChangeEventCount) \(Int(Date().timeIntervalSince1970 * 1_000))" 28 | guard let data = text.data(using: .utf8), 29 | let instagramData = "iN4$aGr0m".data(using: .utf8) else { 30 | fatalError("Invalid breadcrumb for \(self)") 31 | } 32 | let hash = CC.HMAC(data, 33 | alg: .sha256, 34 | key: instagramData) 35 | .base64EncodedString() 36 | let body = data.base64EncodedString() 37 | return "\(hash)\n\(body)\n" 38 | } 39 | } 40 | 41 | extension Body { 42 | /// Sign `body` and update the request accordingly. 43 | /// 44 | /// - parameter body: A valid `Wrapper`. 45 | /// - returns: An updated copy of `self`. 46 | func signing(body: Wrapper) -> Self { 47 | do { 48 | // Encode parameters. 49 | guard let encoded = try? body.encode(), 50 | let description = String(data: encoded, encoding: .utf8), 51 | let data = description.data(using: .utf8), 52 | let hex = "937463b5272b5d60e9d20f0f8d7d192193dd95095a3ad43725d494300a5ea5fc" 53 | .dataFromHexadecimalString() else { 54 | throw SigningError.invalidRepresentation 55 | } 56 | // Compute hash. 57 | let hash = CC.HMAC(data, 58 | alg: .sha256, 59 | key: hex) 60 | .base64EncodedString() 61 | // Sign body. 62 | return self.body(appending: [ 63 | "signed_body": [hash, description].joined(separator: "."), 64 | "ig_sig_key_version": "5" 65 | ]) 66 | } catch { 67 | fatalError(["Exception raised when signing. \(error.localizedDescription).", 68 | error.localizedDescription, 69 | "Please open an issue at `https://github.com/sbertix/Swiftagram/issues`."].joined(separator: " ")) 70 | } 71 | } 72 | 73 | /// Sign `body` and update the request accordingly. 74 | /// 75 | /// - parameter body: A valid `Wrappable`. 76 | /// - returns: An updated copy of `self`. 77 | func signing(body: W) -> Self { 78 | signing(body: body.wrapped) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/AuthenticatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticatorTests.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 17/08/2020. 6 | // 7 | 8 | #if !os(watchOS) && canImport(XCTest) 9 | 10 | import Foundation 11 | import XCTest 12 | 13 | #if canImport(UIKit) && canImport(WebKit) 14 | import UIKit 15 | import WebKit 16 | #endif 17 | 18 | @testable import Swiftagram 19 | @testable import SwiftagramCrypto 20 | 21 | internal final class AuthenticatorTests: XCTestCase { 22 | /// The dispose bag. 23 | private var bin: Set = [] 24 | 25 | // MARK: Tests 26 | 27 | /// Test signing. 28 | func testSigning() { 29 | let request = Request("https://google.com") 30 | XCTAssert( 31 | request 32 | .signing(body: ["key": "value"]) 33 | .body 34 | .flatMap { String(data: $0, encoding: .utf8) }? 35 | .removingPercentEncoding? 36 | .contains("{\"key\":\"value\"}") == true 37 | ) 38 | } 39 | 40 | /// Test `BasicAuthenticator` login flow. 41 | func testBasicAuthenticator() { 42 | guard let password = ProcessInfo.processInfo.environment["PASSWORD"] else { return } 43 | let expectation = XCTestExpectation() 44 | Authenticator.userDefaults 45 | .basic(username: "swiftagram.tests", 46 | password: password.trimmingCharacters(in: .whitespacesAndNewlines)) 47 | .authenticate() 48 | .ignoreOutput() 49 | .sink( 50 | receiveCompletion: { 51 | switch $0 { 52 | case .failure(let error): 53 | switch error { 54 | case Authenticator.Error.twoFactorChallenge(_): 55 | break 56 | default: 57 | XCTFail(error.localizedDescription) 58 | } 59 | default: 60 | XCTFail("This should never be called.") 61 | } 62 | expectation.fulfill() 63 | }, 64 | receiveValue: { _ in } 65 | ) 66 | .store(in: &bin) 67 | wait(for: [expectation], timeout: 60) 68 | } 69 | 70 | #if canImport(UIKit) && canImport(WebKit) 71 | 72 | /// Test `WebViewAuthenticator` login flow. 73 | /// 74 | /// This is not an actual test, as we can't test interface-based implementations with SPM. 75 | func testWebViewAuthenticator() { 76 | if #available(macOS 10.13, iOS 11, *) { 77 | let expectation = XCTestExpectation() 78 | let view = UIView() 79 | Authenticator.userDefaults 80 | .visual(filling: view) 81 | .authenticate() 82 | .map { _ in () } 83 | .catch { _ in Just(()) } 84 | .sink { XCTFail("This should never be called.") } 85 | .store(in: &bin) 86 | DispatchQueue.main.asyncAfter(deadline: .now() + 8) { 87 | self.bin.removeAll() 88 | expectation.fulfill() 89 | } 90 | wait(for: [expectation], timeout: 10) 91 | } 92 | } 93 | 94 | #endif 95 | } 96 | 97 | #endif 98 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/ClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientTests.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 27/10/20. 6 | // 7 | 8 | #if !os(watchOS) && canImport(XCTest) 9 | 10 | import Foundation 11 | import XCTest 12 | 13 | #if canImport(UIKit) 14 | import UIKit 15 | #endif 16 | 17 | @testable import Swiftagram 18 | 19 | internal final class ClientTests: XCTestCase { 20 | /// Test an Android device. 21 | func testAndroid() { 22 | let device = Client.samsungGalaxyS20 23 | let description = ["Instagram 160.1.0.31.120 Android", 24 | "(29/10; 480dpi; 1080x2277; samsung; SM-G981B; x1s; exynos990;", 25 | "en_US; 246979827)"].joined(separator: " ") 26 | XCTAssert(device.description == description, "Invalid user agent") 27 | } 28 | 29 | /// Test an iOS device. 30 | func testIOS() { 31 | let device = Client.iPhone11ProMax 32 | let description = ["Instagram 160.1.0.31.120", 33 | "(iPhone12,5; iOS 14_0; en_US; en-US; scale=3.00;", 34 | "1242x2688; 246979827)"].joined(separator: " ") 35 | XCTAssert(device.description == description, "Invalid user agent") 36 | } 37 | } 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Media+Content.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Media+Content.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 23/08/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | internal extension Media.Content { 13 | /// Fetch all images. 14 | func images() -> [Media.Version]? { 15 | switch self { 16 | case .picture(let picture): 17 | return picture.images 18 | case .video(let video): 19 | return video.images 20 | case .album(let album): 21 | return album.first?.images() 22 | default: 23 | return nil 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comment.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Comment: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["text": \Self.text, 17 | "likes": \Self.likes, 18 | "user": \Self.user, 19 | "identifier": \Self.identifier] 20 | } 21 | 22 | extension Comment.Collection: Reflected { 23 | /// The prefix. 24 | public static var debugDescriptionPrefix: String { "Comment." } 25 | /// A list of to-be-reflected properties. 26 | public static let properties: [String: PartialKeyPath] = ["comments": \Self.comments, 27 | "error": \Self.error] 28 | } 29 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Conversation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conversation.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Conversation: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["identifier": \Self.identifier, 17 | "title": \Self.title, 18 | "updatedAt": \Self.updatedAt, 19 | "openedAt": \Self.openedAt, 20 | "hasMutedMessages": \Self.hasMutedMessages, 21 | "hasMutedVideocalls": \Self.hasMutedVideocalls, 22 | "users": \Self.users, 23 | "messages": \Self.messages] 24 | } 25 | 26 | extension Conversation.Unit: Reflected { 27 | /// The prefix. 28 | public static var debugDescriptionPrefix: String { "Conversation." } 29 | /// A list of to-be-reflected properties. 30 | public static let properties: [String: PartialKeyPath] = ["thread": \Self.conversation, 31 | "error": \Self.error] 32 | } 33 | 34 | extension Conversation.Collection: Reflected { 35 | /// The prefix. 36 | public static var debugDescriptionPrefix: String { "Conversation." } 37 | /// A list of to-be-reflected properties. 38 | public static let properties: [String: PartialKeyPath] = ["threads": \Self.conversations, 39 | "viewer": \Self.viewer, 40 | "error": \Self.error] 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Friendship.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Friendship.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Friendship: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = [ 17 | "isFollowedByYou": \Self.isFollowedByYou, 18 | "isFollowingYou": \Self.isFollowingYou, 19 | "isBlockedByYou": \Self.isBlockedByYou, 20 | "isCloseFriend": \Self.isCloseFriend, 21 | "didRequestToFollowYou": \Self.didRequestToFollowYou, 22 | "didRequestToFollow": \Self.didRequestToFollow, 23 | "isMutingStories": \Self.isMutingStories, 24 | "isMutingPosts": \Self.isMutingPosts 25 | ] 26 | } 27 | 28 | extension Friendship.Dictionary: Reflected { 29 | /// The prefix. 30 | public static var debugDescriptionPrefix: String { "Friendship." } 31 | /// A list of to-be-reflected properties. 32 | public static let properties: [String: PartialKeyPath] = ["friendships": \Self.friendships, 33 | "error": \Self.error] 34 | } 35 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Location: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["coordinates": \Self.coordinates, 17 | "name": \Self.name, 18 | "shortName": \Self.shortName, 19 | "address": \Self.address, 20 | "city": \Self.city, 21 | "identifier": \Self.identifier] 22 | } 23 | 24 | extension Location.Unit: Reflected { 25 | /// The prefix. 26 | public static var debugDescriptionPrefix: String { "Location." } 27 | /// A list of to-be-reflected properties. 28 | public static let properties: [String: PartialKeyPath] = ["location": \Self.location, 29 | "error": \Self.error] 30 | } 31 | 32 | extension Location.Collection: Reflected { 33 | /// The prefix. 34 | public static var debugDescriptionPrefix: String { "Location." } 35 | /// A list of to-be-reflected properties. 36 | public static let properties: [String: PartialKeyPath] = ["venues": \Self.venues, 37 | "error": \Self.error] 38 | } 39 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Media.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Media.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Media: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["identifier": \Self.identifier, 17 | "primaryKey": \Self.primaryKey, 18 | "code": \Self.code, 19 | "wasLikedByYou": \Self.wasLikedByYou, 20 | "expiringAt": \Self.expiringAt, 21 | "takenAt": \Self.takenAt, 22 | "size": \Self.size, 23 | "aspectRatio": \Self.aspectRatio, 24 | "resolution": \Self.resolution, 25 | "caption": \Self.caption, 26 | "comments": \Self.comments, 27 | "likes": \Self.likes, 28 | "content": \Self.content, 29 | "user": \Self.user, 30 | "location": \Self.location] 31 | } 32 | 33 | extension Media.Collection: Reflected { 34 | /// The prefix. 35 | public static var debugDescriptionPrefix: String { "Media." } 36 | /// A list of to-be-reflected properties. 37 | public static let properties: [String: PartialKeyPath] = ["media": \Self.media, 38 | "error": \Self.error] 39 | } 40 | 41 | extension Media.Picture: Reflected { 42 | /// The prefix. 43 | public static var debugDescriptionPrefix: String { "Media." } 44 | /// A list of to-be-reflected properties. 45 | public static let properties: [String: PartialKeyPath] = ["images": \Self.images] 46 | } 47 | 48 | extension Media.Unit: Reflected { 49 | /// The prefix. 50 | public static var debugDescriptionPrefix: String { "Media." } 51 | /// A list of to-be-reflected properties. 52 | public static let properties: [String: PartialKeyPath] = ["media": \Self.media, 53 | "error": \Self.error] 54 | } 55 | 56 | extension Media.Version: Reflected { 57 | /// The prefix. 58 | public static var debugDescriptionPrefix: String { "Media." } 59 | /// A list of to-be-reflected properties. 60 | public static let properties: [String: PartialKeyPath] = ["url": \Self.url, 61 | "size": \Self.size, 62 | "aspectRatio": \Self.aspectRatio, 63 | "resolution": \Self.resolution] 64 | } 65 | 66 | extension Media.Video: Reflected { 67 | /// The prefix. 68 | public static var debugDescriptionPrefix: String { "Media." } 69 | /// A list of to-be-reflected properties. 70 | public static let properties: [String: PartialKeyPath] = ["duration": \Self.duration, 71 | "images": \Self.images, 72 | "clips": \Self.clips] 73 | } 74 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Recipient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Recipient.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Recipient.Collection: Reflected { 13 | /// The prefix. 14 | public static var debugDescriptionPrefix: String { "Recipient." } 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["recipients": \Self.recipients, 17 | "error": \Self.error] 18 | } 19 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Reflected.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reflected.swift 3 | // Swiftagram 4 | // 5 | // Created by Stefano Bertagno on 26/08/20. 6 | // 7 | 8 | import Foundation 9 | 10 | import ComposableRequest 11 | 12 | /// A `protocol` returning all underlying properties. 13 | public protocol Reflected: Wrapped, CustomDebugStringConvertible { 14 | /// An optional prefix. 15 | static var debugDescriptionPrefix: String { get } 16 | 17 | /// A list of all properties. 18 | /// - note: This does not use `Mirror` reflection, to allow for computed properties and fine tuning. 19 | static var properties: [String: PartialKeyPath] { get } 20 | } 21 | 22 | public extension Reflected { 23 | /// A custom debug description. 24 | var debugDescription: String { 25 | let name = String(describing: Self.self) 26 | let properties = Self.properties 27 | .map { $0 + ": " + String(reflecting: self[keyPath: $1]) } 28 | .sorted { $0.count < $1.count } 29 | .joined(separator: ", ") 30 | return Self.debugDescriptionPrefix + name + "(" + properties + ")" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/SavedCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SavedCollection.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 19/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension SavedCollection: Reflected { 13 | /// The prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["collectionId": \Self.identifier, 17 | "collectionName": \Self.name, 18 | "collectionMediaType": \Self.type, 19 | "collectionMediaCount": \Self.count, 20 | "coverMediaList": \Self.cover, 21 | "items": \Self.items] 22 | } 23 | 24 | extension SavedCollection.Unit: Reflected { 25 | /// The prefix. 26 | public static let debugDescriptionPrefix: String = "SavedCollection." 27 | /// A list of to-be-reflected properties. 28 | public static let properties: [String: PartialKeyPath] = ["saveMediaResponse": \Self.collection] 29 | } 30 | 31 | extension SavedCollection.Collection: Reflected { 32 | /// The prefix. 33 | public static let debugDescriptionPrefix: String = "SavedCollection." 34 | /// A list of to-be-reflected properties. 35 | public static let properties: [String: PartialKeyPath] = ["items": \Self.collections] 36 | } 37 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Status.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Status.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Status: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["error": \Self.error] 17 | } 18 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Sticker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sticker.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Sticker: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["identifier": \Self.identifier, 17 | "level": \Self.level, 18 | "offset": \Self.offset, 19 | "rotation": \Self.rotation] 20 | } 21 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/Tag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tag.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/04/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension Tag: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["id": \Self.identifier, 17 | "name": \Self.name, 18 | "mediaCount": \Self.count, 19 | "following": \Self.isFollowed] 20 | } 21 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/TrayItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrayItem.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension TrayItem: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = [ 17 | "identifier": \Self.identifier, 18 | "position": \Self.position, 19 | "seenPosition": \Self.seenPosition, 20 | "availableCount": \Self.availableCount, 21 | "fetchedCount": \Self.fetchedCount, 22 | "title": \Self.title, 23 | "cover": \Self.cover, 24 | "items": \Self.items, 25 | "expiringAt": \Self.expiringAt, 26 | "publishedAt": \Self.publishedAt, 27 | "seenAt": \Self.seenAt, 28 | "user": \Self.user, 29 | "isMuted": \Self.isMuted, 30 | "containsVideos": \Self.containsVideos, 31 | "containsCloseFriendsExclusives": \Self.containsCloseFriendsExclusives 32 | ] 33 | } 34 | 35 | extension TrayItem.Unit: Reflected { 36 | /// The prefix. 37 | public static var debugDescriptionPrefix: String { "TrayItem." } 38 | /// A list of to-be-reflected properties. 39 | public static let properties: [String: PartialKeyPath] = ["item": \Self.item, 40 | "error": \Self.error] 41 | } 42 | 43 | extension TrayItem.Collection: Reflected { 44 | /// The prefix. 45 | public static var debugDescriptionPrefix: String { "TrayItem." } 46 | /// A list of to-be-reflected properties. 47 | public static let properties: [String: PartialKeyPath] = ["items": \Self.items, 48 | "error": \Self.error] 49 | } 50 | 51 | extension TrayItem.Dictionary: Reflected { 52 | /// The prefix. 53 | public static var debugDescriptionPrefix: String { "TrayItem." } 54 | /// A list of to-be-reflected properties. 55 | public static let properties: [String: PartialKeyPath] = ["items": \Self.items, 56 | "error": \Self.error] 57 | } 58 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension User: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["identifier": \Self.identifier, 17 | "username": \Self.username, 18 | "name": \Self.name, 19 | "biography": \Self.biography, 20 | "thumbnail": \Self.thumbnail, 21 | "avatar": \Self.avatar, 22 | "access": \Self.access, 23 | "counter": \Self.counter, 24 | "friendship": \Self.friendship] 25 | } 26 | 27 | extension User.Unit: Reflected { 28 | /// The prefix. 29 | public static var debugDescriptionPrefix: String { "Comment." } 30 | /// A list of to-be-reflected properties. 31 | public static let properties: [String: PartialKeyPath] = ["user": \Self.user, 32 | "error": \Self.error] 33 | } 34 | 35 | extension User.Collection: Reflected { 36 | /// The prefix. 37 | public static var debugDescriptionPrefix: String { "User." } 38 | /// A list of to-be-reflected properties. 39 | public static let properties: [String: PartialKeyPath] = ["users": \Self.users, 40 | "error": \Self.error] 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftagramTests/Shared/Reflection/UserTag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserTag.swift 3 | // SwiftagramTests 4 | // 5 | // Created by Stefano Bertagno on 20/03/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Swiftagram 11 | 12 | extension UserTag: Reflected { 13 | /// The debug description prefix. 14 | public static let debugDescriptionPrefix: String = "" 15 | /// A list of to-be-reflected properties. 16 | public static let properties: [String: PartialKeyPath] = ["identifier": \Self.identifier, 17 | "x": \Self.x, 18 | "y": \Self.y] 19 | } 20 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributes are always welcome. 4 | That said, we require some guidelines to be followed, in order for PRs to be merged. 5 | 6 | > The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL 7 | > NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and 8 | > "OPTIONAL" in this document are to be interpreted as described in 9 | > RFC 2119. 10 | 11 | ## Before Contributing 12 | 13 | - For **bugfixes** only, you MUST open a new [**issue**](https://github.com/sbertix/Swiftagram/issues), if one on the topic does not exist already, before submitting your pull request. 14 | - You SHOULD rely on provided issue templates. 15 | - For **enhancements** only, you SHOULD open a new [**discussion**](https://github.com/sbertix/Swiftagram/discussions), if one on the topic does not exist already, before submitting your pull request. 16 | - Discussions for small additive implementations are OPTIONAL. 17 | - Discussions for breaking changes are REQUIRED. 18 | - Wait for feedback before embarking on time-consuming projects. 19 | - Understand that consistency _always_ comes first. 20 | 21 | ## When Writing your Code 22 | 23 | - You MUST write your code so that it runs on `Swift 5.3`. 24 | - You MUST lint your code using [`swiftlint`](https://github.com/realm/SwiftLint). 25 | 26 | ## When Contributing 27 | 28 | - Your commits SHOULD be [signed](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification/signing-commits). 29 | - **Commits pushing new features and CI implementations MUST be signed**. 30 | - Commits pushing bugfixes SHOULD be signed. 31 | - Other commits MAY be signed. 32 | - **Your commits MUST follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) guidelines**. 33 | - The message MUST start with one of the following types: `chore`, `ci`, `feat`, `fix`, `perf` or `test`. 34 | - The message type MAY be immediately followed by a scope, in brackets, e.g. `(endpoints)`, `(docs)`, etc. 35 | - A non-empty message description MUST follow type and (optional) scope, begin with a lowercase character and always start with subjunctive verbs, e.g. _update_, _fix_, _add_, with no period at the end. 36 | - Commits MAY contain a single paragraph body, using regular word capitalization, but no period at the end. 37 | - Commits with breaking changes MUST contain a footer, separated from the body by a newline, if it exists, and describe the changes using the message format, preceded by `BREAKING CHANGE: `, e.g. `BREAKING CHANGE: remove upload endpoints` 38 | - Commits fixing a bug MUST contain a footer, separated from the body by a newline, if it exists, referencing the number of the issue their closing, preceded by `Closes `, e.g. `Closes #123`. 39 | - You SHOULD open `draft` pull requests as soon as you start working on them, in order to let the community know of your plans and avoid duplication. 40 | - **You MUST leave the pull request body empty**, as it will be populated automatically. 41 | - You MAY add an additional comment for context. 42 | - **Pull requests SHOULD only solve one problem: stick to the minimal set of changes**. 43 | - New code SHOULD come with new tests. 44 | - **Pull requests MUST always target `main` latest commit**. 45 | - Pull requests SHOULD have a linear commit history. 46 | - You SHOULD ask for a review as soon as you are done with your changes. 47 | --------------------------------------------------------------------------------