├── .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