├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Sources
└── V2exAPI
│ ├── Models
│ ├── V2Response.swift
│ ├── V2Token.swift
│ ├── V2Notification.swift
│ ├── V2Comment.swift
│ ├── V2Topic.swift
│ ├── V2Node.swift
│ └── V2Member.swift
│ └── V2exAPI.swift
├── V2exAPI.podspec
├── LICENSE
├── Package.swift
├── Tests
└── V2exAPITests
│ └── V2exAPITests.swift
├── README.md
└── README_EN.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/V2exAPI/Models/V2Response.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by isaced on 2022/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// V2EX API Response
11 | public struct V2Response: Decodable {
12 | public let success: Bool
13 | public let message: String?
14 | public let result: T
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/V2exAPI/Models/V2Token.swift:
--------------------------------------------------------------------------------
1 | //
2 | // V2Token.swift
3 | //
4 | //
5 | // Created by isaced on 2022/8/7.
6 | //
7 |
8 | import Foundation
9 |
10 | /**
11 | Token
12 | */
13 | public struct V2Token: Decodable, Equatable, Hashable {
14 | public let token, scope: String?
15 | public let expiration, goodForDays, totalUsed, lastUsed: Int?
16 | public let created: Int?
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case token, scope, expiration
20 | case goodForDays = "good_for_days"
21 | case totalUsed = "total_used"
22 | case lastUsed = "last_used"
23 | case created
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/V2exAPI/Models/V2Notification.swift:
--------------------------------------------------------------------------------
1 | //
2 | // V2Notification.swift
3 | //
4 | //
5 | // Created by isaced on 2022/8/7.
6 | //
7 |
8 | import Foundation
9 |
10 | /**
11 | 通知
12 | */
13 | public struct V2Notification: Decodable, Identifiable, Hashable {
14 | public let id, memberID, forMemberID: Int?
15 | public let text, payload, payloadRendered: String?
16 | public let created: Int?
17 | public let member: V2Member?
18 |
19 | enum CodingKeys: String, CodingKey {
20 | case id
21 | case memberID = "member_id"
22 | case forMemberID = "for_member_id"
23 | case text, payload
24 | case payloadRendered = "payload_rendered"
25 | case created, member
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/V2exAPI/Models/V2Comment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // V2Comment.swift
3 | //
4 | //
5 | // Created by isaced on 2022/7/31.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct V2Comment: Identifiable, Decodable, Hashable {
11 |
12 | public let id: Int
13 | public let content: String
14 | public let contentRendered: String
15 | public let created: Int
16 | public let member: V2Member
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case id, content
20 | case contentRendered = "content_rendered"
21 | case created, member
22 | }
23 |
24 | public init(id: Int, content: String, contentRendered: String, created: Int, member: V2Member) {
25 | self.id = id
26 | self.content = content
27 | self.contentRendered = contentRendered
28 | self.created = created
29 | self.member = member
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/V2exAPI.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 |
3 | spec.name = "V2exAPI"
4 | spec.version = "1.0.0"
5 | spec.summary = "V2ex API 的 Swift 封装,支持 iOS/macOS"
6 |
7 | spec.description = <<-DESC
8 | V2ex API 的 Swift 封装,支持 iOS/macOS,支持 V1/V2 API。
9 | DESC
10 |
11 | spec.homepage = "https://github.com/isaced/V2exAPI"
12 |
13 | spec.license = { :type => "MIT", :file => "LICENSE" }
14 |
15 | spec.author = { "isaced" => "isaced@163.com" }
16 |
17 | spec.ios.deployment_target = "15.0"
18 | spec.osx.deployment_target = "12.0"
19 | spec.watchos.deployment_target = "8.0"
20 | spec.tvos.deployment_target = "15.0"
21 |
22 | spec.source = { :git => "https://github.com/isaced/V2exAPI.git", :tag => "#{spec.version}" }
23 |
24 | spec.source_files = ["Sources/**/*.swift"]
25 |
26 | spec.swift_versions = ['5.0']
27 | end
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 isaced
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "V2exAPI",
8 | platforms: [
9 | .iOS(.v15),
10 | .macOS(.v12),
11 | .tvOS(.v15),
12 | .watchOS(.v8),
13 | ],
14 | products: [
15 | // Products define the executables and libraries a package produces, and make them visible to other packages.
16 | .library(
17 | name: "V2exAPI",
18 | targets: ["V2exAPI"])
19 | ],
20 | dependencies: [
21 | // Dependencies declare other packages that this package depends on.
22 | // .package(url: /* package url */, from: "1.0.0"),
23 | ],
24 | targets: [
25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
26 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
27 | .target(
28 | name: "V2exAPI",
29 | dependencies: []),
30 | .testTarget(
31 | name: "V2exAPITests",
32 | dependencies: ["V2exAPI"]),
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/Tests/V2exAPITests/V2exAPITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import V2exAPI
4 |
5 | final class V2exAPITests: XCTestCase {
6 |
7 | func testAPI() async throws {
8 | let v2ex = V2exAPI()
9 |
10 | let nodes = try? await v2ex.nodesList()
11 | XCTAssertNotNil(nodes)
12 |
13 | let latest = try await v2ex.latestTopics()
14 | XCTAssertNotNil(latest)
15 |
16 | let hot = try await v2ex.hotTopics()
17 | XCTAssertNotNil(hot)
18 |
19 | let nodeDetail = try await v2ex.nodesShow(name: "swift")
20 | XCTAssertNotNil(nodeDetail)
21 |
22 | let member = try await v2ex.memberShow(username: "isaced")
23 | XCTAssertNotNil(member)
24 |
25 | let repliesAll = try await v2ex.repliesAll(topicId: 883252)
26 | XCTAssertNotNil(repliesAll)
27 |
28 | let topics = try await v2ex.topics(nodeName: "apple")
29 | XCTAssertNotNil(topics)
30 |
31 | // --- V2 ---
32 |
33 | // let topics = try await v2ex.topics(nodeName: "swift")
34 | // XCTAssertNotNil(topics)
35 | //
36 | // let topic = try await v2ex.topic(topicId: 870607)
37 | // XCTAssertNotNil(topic)
38 | //
39 | // let node = try await v2ex.getNode(nodeName: "swift")
40 | // XCTAssertNotNil(node)
41 | //
42 | // let notifications = try await v2ex.notifications()
43 | // XCTAssertNotNil(notifications)
44 | //
45 | // let delNotifications = try await v2ex.deleteNotification(notificationId: 5846288)
46 | // XCTAssertNotNil(delNotifications)
47 | //
48 | // let memberMe = try await v2ex.member()
49 | // XCTAssertNotNil(memberMe)
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/V2exAPI/Models/V2Topic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by isaced on 2022/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// 话题
11 | public struct V2Topic: Identifiable, Decodable, Hashable {
12 |
13 | public let id: Int
14 | public let node: V2Node?
15 | public let member: V2Member?
16 | public let lastReplyBy: String?
17 | public let lastTouched: Int?
18 | public let title: String?
19 | public let url: String?
20 | public let created: Int?
21 | public let deleted: Int?
22 | public let content: String?
23 | public let contentRendered: String?
24 | public let lastModified: Int?
25 | public let replies: Int?
26 |
27 | public init(
28 | id: Int,
29 | node: V2Node? = nil,
30 | member: V2Member? = nil,
31 | lastReplyBy: String? = nil,
32 | lastTouched: Int? = nil,
33 | title: String? = nil,
34 | url: String? = nil,
35 | created: Int? = nil,
36 | deleted: Int? = nil,
37 | content: String? = nil,
38 | contentRendered: String? = nil,
39 | lastModified: Int? = nil,
40 | replies: Int? = nil
41 | ) {
42 | self.id = id
43 | self.node = node
44 | self.member = member
45 | self.lastReplyBy = lastReplyBy
46 | self.lastTouched = lastTouched
47 | self.title = title
48 | self.url = url
49 | self.created = created
50 | self.deleted = deleted
51 | self.content = content
52 | self.contentRendered = contentRendered
53 | self.lastModified = lastModified
54 | self.replies = replies
55 | }
56 |
57 | enum CodingKeys: String, CodingKey {
58 | case node, member
59 | case lastReplyBy = "last_reply_by"
60 | case lastTouched = "last_touched"
61 | case title, url, created, deleted, content
62 | case contentRendered = "content_rendered"
63 | case lastModified = "last_modified"
64 | case replies, id
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/V2exAPI/Models/V2Node.swift:
--------------------------------------------------------------------------------
1 | //
2 | // V2Node.swift
3 | //
4 | //
5 | // Created by isaced on 2022/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// 节点
11 | public struct V2Node: Identifiable, Decodable, Hashable {
12 |
13 | public let name: String
14 | public let title: String?
15 | public var id: Int?
16 | public let avatarLarge: String?
17 | public let avatarNormal: String?
18 | public let url: String?
19 | public let topics: Int?
20 | public let footer: String?
21 | public let header: String?
22 | public let titleAlternative: String?
23 | public let avatarMini: String?
24 | public let stars: Int?
25 | public let aliases: [String]?
26 | public let root: Bool?
27 | public let parentNodeName: String?
28 |
29 | public init(
30 | name: String, avatarLarge: String?, avatarNormal: String?, title: String?, url: String?,
31 | topics: Int?, footer: String?, header: String?, titleAlternative: String?, avatarMini: String?,
32 | stars: Int?, aliases: [String]?, root: Bool?, id: Int?, parentNodeName: String?
33 | ) {
34 | self.name = name
35 | self.avatarLarge = avatarLarge
36 | self.avatarNormal = avatarNormal
37 | self.title = title
38 | self.url = url
39 | self.topics = topics
40 | self.footer = footer
41 | self.header = header
42 | self.titleAlternative = titleAlternative
43 | self.avatarMini = avatarMini
44 | self.stars = stars
45 | self.aliases = aliases
46 | self.root = root
47 | self.id = id
48 | self.parentNodeName = parentNodeName
49 | }
50 |
51 | enum CodingKeys: String, CodingKey {
52 | case name, stars, aliases, root, id, title, url, topics, footer, header
53 | case avatarLarge = "avatar_large"
54 | case avatarNormal = "avatar_normal"
55 | case titleAlternative = "title_alternative"
56 | case avatarMini = "avatar_mini"
57 | case parentNodeName = "parent_node_name"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/V2exAPI/Models/V2Member.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by isaced on 2022/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// 会员
11 | public struct V2Member : Decodable, Hashable {
12 |
13 | public var id: Int?
14 | public var username: String?
15 | public var url: String?
16 | public var website: String?
17 | public var twitter: String?
18 | public var psn: String?
19 | public var github: String?
20 | public var btc: String?
21 | public var location: String?
22 | public var tagline: String?
23 | public var bio: String?
24 | public var avatar: String?
25 | public var avatarMini: String?
26 | public var avatarNormal: String?
27 | public var avatarLarge: String?
28 | public var created: Int?
29 | public var lastModified: Int?
30 |
31 | public init(id: Int? = nil, username: String? = nil, url: String? = nil, website: String? = nil, twitter: String? = nil, psn: String? = nil, github: String? = nil, btc: String? = nil, location: String? = nil, tagline: String? = nil, bio: String? = nil, avatar: String? = nil, avatarMini: String? = nil, avatarNormal: String? = nil, avatarLarge: String? = nil, created: Int? = nil, lastModified: Int? = nil) {
32 | self.id = id
33 | self.username = username
34 | self.url = url
35 | self.website = website
36 | self.twitter = twitter
37 | self.psn = psn
38 | self.github = github
39 | self.btc = btc
40 | self.location = location
41 | self.tagline = tagline
42 | self.bio = bio
43 | self.avatar = avatar
44 | self.avatarMini = avatarMini
45 | self.avatarNormal = avatarNormal
46 | self.avatarLarge = avatarLarge
47 | self.created = created
48 | self.lastModified = lastModified
49 | }
50 |
51 | enum CodingKeys: String, CodingKey {
52 | case id, username, url, website, twitter, psn, github, btc, location, tagline, bio, created, avatar
53 | case avatarMini = "avatar_mini"
54 | case avatarNormal = "avatar_normal"
55 | case avatarLarge = "avatar_large"
56 | case lastModified = "last_modified"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # V2exAPI
2 | [](https://github.com/isaced/V2exAPI)
3 | [](https://cocoapods.org/pods/V2exAPI)
4 | [](https://github.com/Carthage/Carthage)
5 | [](https://github.com/isaced/V2exAPI)
6 | [](https://github.com/isaced/V2exAPI)
7 |
8 | 中文|[English](/README_EN.md)
9 |
10 | 一个 [V2ex](https://v2ex.com/) API 的 Swift 封装,支持 iOS/macOS,支持 SPM 引入和 async/await 方式调用。
11 |
12 | ## 特性
13 |
14 | - [x] SPM(Swift Package Manager) 支持
15 | - [x] Swift async/await 异步 API
16 | - [x] V2ex API V1 支持度 100%
17 | - [x] V2ex API V2 支持度 100%
18 | - [x] Zero dependency 零三方依赖
19 |
20 | ## API 支持范围
21 |
22 | ### V1
23 |
24 | | 接口 | 路径 | 进度 |
25 | |------|-------------------------|-----|
26 | | 最热主题 | /api/topics/hot.json | ☑ |
27 | | 最新主题 | /api/topics/latest.json | ☑ |
28 | | 节点列表 | /api/nodes/list.json | ☑ |
29 | | 节点信息 | /api/nodes/show.json | ☑ |
30 | | 用户主页 | /api/members/show.json | ☑ |
31 | | 回复列表 | /api/replies/show.json | ☑ |
32 |
33 | ### V2
34 |
35 | | 接口 | 路径 | 进度 |
36 | |----------------------|--------------------------------|---------|
37 | | 获取最新的提醒 | notifications | ☑ |
38 | | 删除指定的提醒 | notifications/:notification_id | ☑ |
39 | | 获取自己的 Profile | member | ☑ |
40 | | 查看当前使用的令牌 | token | ☑ |
41 | | 获取指定节点 | nodes/:node_name | ☑ |
42 | | 获取指定节点下的主题 | nodes/:node_name/topics | ☑ |
43 | | 获取指定主题 | topics/:topic_id | ☑ |
44 | | 获取指定主题下的回复 | topics/:topic_id/replies | ☑ |
45 |
46 | ## 安装
47 |
48 | ### Swift Package Manager
49 |
50 | 通过 [Swift Package Manager](https://swift.org/package-manager/) 安装 V2exAPI 到你的项目,在 `Package.swift` 中添加:
51 |
52 | ```swift
53 | .package(name: "V2exAPI", url: "git@github.com:isaced/V2exAPI.git", .upToNextMinor(from: "1.0.0")),
54 | ```
55 |
56 | 在 Xcode 中:
57 | - 菜单 File > Swift Packages > Add Package Dependency
58 | - 搜索 https://github.com/isaced/V2exAPI.git
59 | - 选择 "Up to Next Major" 版本 "1.0.0"
60 |
61 |
62 | ### CocoaPods
63 |
64 | ```ruby
65 | pod 'V2exAPI', '~> 1.0'
66 | ```
67 |
68 | ## 使用
69 |
70 | ```swift
71 | import V2exAPI
72 |
73 | // 初始化 V2eXAPI 对象
74 | let v2ex = V2exAPI(accessToken: "XXXXX-XXXX-XXXX-XXXX-XXXXXXXXX")
75 |
76 | // 获取节点列表
77 | let nodes = try await v2ex.nodesList()
78 |
79 | // 获取最新主题
80 | let latest = try await v2ex.latestTopics()
81 |
82 | // 获取指定节点下的主题列表
83 | let topics = try await v2ex.topics(nodeName: "swift", page: 1)
84 |
85 | // 获取指定主题下的回复
86 | let replies = try await v2ex.replies(topicId: 870607, page: 1)
87 | ```
88 |
89 | ## API 参考
90 |
91 | - [V2EX API 接口](https://www.v2ex.com/p/7v9TEc53)
92 | - [API 2.0 Beta](https://v2ex.com/help/api)
93 |
94 | API Rate Limit
95 |
96 | 默认情况下,每个 IP 每小时可以发起的 API 请求数被限制在 120 次。你可以在 API 返回结果的 HTTP 头部找到 Rate Limit 信息:
97 |
98 | ```
99 | X-Rate-Limit-Limit: 120
100 | X-Rate-Limit-Reset: 1409479200
101 | X-Rate-Limit-Remaining: 116
102 | ```
103 |
104 | 对于能够被 CDN 缓存的 API 请求,只有第一次请求时,才会消耗 Rate Limit 配额。
105 |
106 | > Personal Access Token 申请参考:https://v2ex.com/help/personal-access-token
107 |
108 | ## 使用示例
109 |
110 | - [V2exOS](https://github.com/isaced/V2exOS) - 一个用 SwiftUI 编写的 V2ex macOS 客户端
111 |
112 | 
113 | 
114 |
115 | ## License
116 |
117 | V2exAPI 在 MIT 许可下发布的,有关详细信息,请参阅 [LICENSE](/LICENSE)
118 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # V2exAPI
2 | [](https://github.com/isaced/V2exAPI)
3 | [](https://cocoapods.org/pods/V2exAPI)
4 | [](https://github.com/Carthage/Carthage)
5 | [](https://github.com/isaced/V2exAPI)
6 | [](https://github.com/isaced/V2exAPI)
7 |
8 | [中文](/README.md)|English
9 |
10 | A [V2ex](https://v2ex.com/) API wrapper for iOS/macOS, built with Swift.
11 |
12 | > V2EX is a community of start-ups, designers, developers and creative people.
13 |
14 | ## Features
15 |
16 | - [x] SPM(Swift Package Manager) Support
17 | - [x] Swift async/await Support
18 | - [x] V2ex API V1 100%
19 | - [x] V2ex API V2 100%
20 | - [x] Zero dependency
21 |
22 | ## API Support
23 |
24 | ### V1
25 |
26 | | 接口 | 路径 | 进度 |
27 | |------|-------------------------|-----|
28 | | Hottest topic | /api/topics/hot.json | ☑ |
29 | | Latest Topics | /api/topics/latest.json | ☑ |
30 | | Node list | /api/nodes/list.json | ☑ |
31 | | Node Info | /api/nodes/show.json | ☑ |
32 | | Profile info | /api/members/show.json | ☑ |
33 | | Replies | /api/replies/show.json | ☑ |
34 |
35 | ### V2
36 |
37 | | 接口 | 路径 | 进度 |
38 | |----------------------|--------------------------------|---------|
39 | | Get the latest notifications | notifications | ☑ |
40 | | Delete the specified notification | notifications/:notification_id | ☑ |
41 | | Get your own Profile | member | ☑ |
42 | | Get the currently used token | token | ☑ |
43 | | get the specified node | nodes/:node_name | ☑ |
44 | | Get the topics under the specified node | nodes/:node_name/topics | ☑ |
45 | | Get specific topic details | topics/:topic_id | ☑ |
46 | | Get replies under the specified topic | topics/:topic_id/replies | ☑ |
47 |
48 | ## Installation
49 |
50 | ### Swift Package Manager
51 |
52 | via [Swift Package Manager](https://swift.org/package-manager/) Install to your project,Add in `Package.swift`:
53 |
54 | ```swift
55 | .package(name: "V2exAPI", url: "git@github.com:isaced/V2exAPI.git", .upToNextMinor(from: "1.0.0")),
56 | ```
57 |
58 | In Xcode:
59 | - File > Swift Packages > Add Package Dependency
60 | - Add https://github.com/isaced/V2exAPI.git
61 | - Select "Up to Next Major" with "1.0.0"
62 |
63 | ### CocoaPods
64 |
65 | ```ruby
66 | pod 'V2exAPI', '~> 1.0'
67 | ```
68 |
69 | ## Useage
70 |
71 | ```swift
72 | import V2exAPI
73 |
74 | // Init V2eXAPI object
75 | let v2ex = V2exAPI(accessToken: "XXXXX-XXXX-XXXX-XXXX-XXXXXXXXX")
76 |
77 | // Get node list
78 | let nodes = try await v2ex.nodesList()
79 |
80 | // Get latest topics
81 | let latest = try await v2ex.latestTopics()
82 |
83 | // Get the topics under the specified node
84 | let topics = try await v2ex.topics(nodeName: "swift", page: 1)
85 |
86 | // Get replies under the specified topic
87 | let replies = try await v2ex.replies(topicId: 870607, page: 1)
88 | ```
89 |
90 | ## API Reference
91 |
92 | - [V2EX API interface](https://www.v2ex.com/p/7v9TEc53)
93 | - [API 2.0 Beta](https://v2ex.com/help/api)
94 |
95 | API Rate Limit
96 |
97 | By default, each IP is limited to 120 API requests per hour. You can find the Rate Limit information in the HTTP header of the API return result:
98 |
99 | ```
100 | X-Rate-Limit-Limit: 120
101 | X-Rate-Limit-Reset: 1409479200
102 | X-Rate-Limit-Remaining: 116
103 | ```
104 |
105 | For API requests that can be cached by the CDN, the Rate Limit quota will only be consumed on the first request.
106 |
107 | > Personal Access Token:https://v2ex.com/help/personal-access-token
108 |
109 | ## Examples
110 |
111 | - [V2exOS](https://github.com/isaced/V2exOS) - A SwiftUI V2ex client for macOS
112 |
113 | 
114 | 
115 |
116 |
117 | ## License
118 |
119 | V2exAPI Kingfisher is released under the MIT license. See [LICENSE](/LICENSE) for details.
120 |
--------------------------------------------------------------------------------
/Sources/V2exAPI/V2exAPI.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// V2ex API
4 | public struct V2exAPI {
5 |
6 | /**
7 | 个人访问令牌
8 |
9 | 生成参考: https://v2ex.com/help/personal-access-token
10 | */
11 | public var accessToken: String?
12 | public var session = URLSession.shared
13 |
14 | private let endpointV1 = "https://v2ex.com/api/"
15 | private let endpointV2 = "https://www.v2ex.com/api/v2/"
16 |
17 | public init(accessToken: String? = nil) {
18 | self.accessToken = accessToken
19 | }
20 |
21 | /**
22 | HTTP 请求
23 | */
24 | private func request(httpMethod: String = "GET", url: String, args: [String: Any]? = nil, decodeClass: T.Type) async throws -> (
25 | T?, URLResponse?
26 | ) where T : Decodable {
27 | let urlComponents = NSURLComponents(string: url)!
28 |
29 | if httpMethod != "POST" && args != nil {
30 | urlComponents.queryItems =
31 | args?.map({ (k, v) in
32 | return NSURLQueryItem(name: k, value: "\(v)")
33 | }) as [URLQueryItem]?
34 | }
35 |
36 | guard let requestUrl = urlComponents.url else {
37 | return (nil, nil)
38 | }
39 |
40 | var request = URLRequest(url: requestUrl)
41 | request.httpMethod = httpMethod
42 | request.addValue("application/json", forHTTPHeaderField: "Content-Type")
43 |
44 | if let accessToken = accessToken {
45 | request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
46 | }
47 |
48 | if httpMethod == "POST" && args != nil{
49 | request.httpBody = try? JSONSerialization.data(withJSONObject: args as Any)
50 | }
51 |
52 | let (data, response) = try await session.data(for: request)
53 |
54 | let decoder = JSONDecoder()
55 |
56 | let obj = try decoder.decode(decodeClass.self, from: data)
57 |
58 | return (obj, response)
59 | }
60 |
61 |
62 | // =========== Others ===========
63 |
64 | /**
65 | 获取节点列表
66 | */
67 | public func nodesList(fields: [String]? = nil, sortBy: String = "topics", reverse: String = "1") async throws -> [V2Node]? {
68 | var fieldsList = ["id","name","title","topics","aliases"]
69 | if let fields {
70 | fieldsList = fields
71 | }
72 | let (data, _) = try await request(
73 | url: endpointV1 + "nodes/list.json",
74 | args: [
75 | "fields": fieldsList.joined(separator: ","),
76 | "sort_by": sortBy,
77 | "reverse": reverse,
78 | ],
79 | decodeClass: [V2Node].self
80 | )
81 | return data;
82 | }
83 |
84 | // =========== V1 ===========
85 |
86 | /**
87 | 最热主题
88 |
89 | 相当于首页右侧的 10 大每天的内容。
90 | */
91 | public func hotTopics() async throws -> [V2Topic]? {
92 | let (data, _) = try await request(
93 | url: endpointV1 + "topics/hot.json",
94 | decodeClass: [V2Topic].self
95 | )
96 | return data
97 | }
98 |
99 | /**
100 | 最新主题
101 |
102 | 相当于首页的“全部”这个 tab 下的最新内容。
103 | */
104 | public func latestTopics() async throws -> [V2Topic]? {
105 | let (data, _) = try await request(
106 | url: endpointV1 + "topics/latest.json",
107 | decodeClass: [V2Topic].self
108 | )
109 | return data
110 | }
111 |
112 | /**
113 | 节点信息
114 |
115 | 获得指定节点的名字,简介,URL 及头像图片的地址。
116 |
117 | - parameter name: 节点名(V2EX 的节点名全是半角英文或者数字)
118 | */
119 | public func nodesShow(name: String) async throws -> V2Node? {
120 | let (data, _) = try await request(
121 | url: endpointV1 + "nodes/show.json",
122 | args: [
123 | "name": name
124 | ],
125 | decodeClass: V2Node.self
126 | )
127 | return data;
128 | }
129 |
130 | /**
131 | 用户主页
132 |
133 | 获得指定用户的自我介绍,及其登记的社交网站信息。
134 |
135 | - parameter username: 用户名
136 | - parameter id: 用户在 V2EX 的数字 ID
137 | */
138 | public func memberShow(username: String? = nil, id: Int? = nil) async throws -> V2Member? {
139 | var args:[String:String] = [:]
140 | if let username = username {
141 | args["username"] = username;
142 | }
143 | if let id = id {
144 | args["id"] = String(id);
145 | }
146 |
147 | if args.isEmpty {
148 | return nil
149 | }
150 |
151 | let (data, _) = try await request(
152 | url: endpointV1 + "members/show.json",
153 | args: args,
154 | decodeClass: V2Member.self
155 | )
156 | return data;
157 | }
158 |
159 | /**
160 | 获取指定主题下的回复列表
161 |
162 | - parameter topicId: 主题ID
163 | */
164 | public func repliesAll(topicId: Int) async throws -> [V2Comment]? {
165 | let path = "replies/show.json"
166 | let (data, _) = try await request(
167 | url: endpointV1 + path,
168 | args: [
169 | "topic_id": topicId
170 | ],
171 | decodeClass: [V2Comment].self
172 | )
173 | return data
174 | }
175 |
176 | /**
177 | 获取节点下的主题列表
178 |
179 | - parameter topicId: 主题ID
180 | */
181 | public func topics(nodeName: String) async throws -> [V2Topic]? {
182 | let path = "topics/show.json"
183 | let (data, _) = try await request(
184 | url: endpointV1 + path,
185 | args: [
186 | "node_name": nodeName
187 | ],
188 | decodeClass: [V2Topic].self
189 | )
190 | return data
191 | }
192 |
193 | // =========== V2 ===========
194 |
195 | /**
196 | 获取指定节点下的主题
197 |
198 | - parameter nodeName: 节点名,如 "swift"
199 | - parameter page: 分页页码,默认为 1
200 | */
201 | public func topics(nodeName: String, page: Int = 1) async throws -> V2Response<[V2Topic]?>? {
202 | let path = "nodes/\(nodeName)/topics"
203 | let (data, _) = try await request(
204 | url: endpointV2 + path,
205 | args: [
206 | "p": String(page)
207 | ],
208 | decodeClass: V2Response<[V2Topic]?>.self
209 | )
210 | return data
211 | }
212 |
213 | /**
214 | 获取指定主题下的回复
215 |
216 | - parameter topicId: 主题ID
217 | - parameter page: 分页页码,默认为 1
218 | */
219 | public func replies(topicId: Int, page: Int = 1) async throws -> V2Response<[V2Comment]?>? {
220 | let path = "topics/\(topicId)/replies"
221 | let (data, _) = try await request(
222 | url: endpointV2 + path,
223 | args: [
224 | "p": String(page)
225 | ],
226 | decodeClass: V2Response<[V2Comment]?>.self
227 | )
228 | return data
229 | }
230 |
231 | /**
232 | 获取指定主题
233 |
234 | - parameter topicId: 主题ID
235 | */
236 | public func topic(topicId: Int) async throws -> V2Response? {
237 | let path = "topics/\(topicId)"
238 | let (data, _) = try await request(
239 | url: endpointV2 + path,
240 | decodeClass: V2Response.self
241 | )
242 | return data
243 | }
244 |
245 | /**
246 | 获取指定节点
247 |
248 | - parameter nodeName: 节点名
249 | */
250 | public func getNode(nodeName: String) async throws -> V2Response? {
251 | let path = "nodes/\(nodeName)"
252 | let (data, _) = try await request(
253 | url: endpointV2 + path,
254 | decodeClass: V2Response.self
255 | )
256 | return data
257 | }
258 |
259 | /**
260 | 获取最新的提醒
261 |
262 | - parameter page: 分页页码,默认为 1
263 | */
264 | public func notifications(page: Int = 1) async throws -> V2Response<[V2Notification]?>? {
265 | let path = "notifications"
266 | let (data, _) = try await request(
267 | url: endpointV2 + path,
268 | args: [
269 | "p": String(page)
270 | ],
271 | decodeClass: V2Response<[V2Notification]?>.self
272 | )
273 | return data
274 | }
275 |
276 | /**
277 | 获取最新的提醒
278 |
279 | - parameter notification_id: 提醒ID
280 | */
281 | public func deleteNotification(notificationId: Int) async throws -> V2Response? {
282 | let path = "notifications/\(notificationId)"
283 | let (data, _) = try await request(
284 | httpMethod: "DELETE",
285 | url: endpointV2 + path,
286 | decodeClass: V2Response.self
287 | )
288 | return data
289 | }
290 |
291 |
292 | /**
293 | 获取自己的 Profile
294 | */
295 | public func member() async throws -> V2Response? {
296 | let path = "member"
297 | let (data, _) = try await request(
298 | url: endpointV2 + path,
299 | decodeClass: V2Response.self
300 | )
301 | return data
302 | }
303 |
304 |
305 | /**
306 | 查看当前使用的令牌
307 | */
308 | public func token() async throws -> V2Response? {
309 | let path = "token"
310 | let (data, _) = try await request(
311 | url: endpointV2 + path,
312 | decodeClass: V2Response.self
313 | )
314 | return data
315 | }
316 |
317 | /**
318 | 创建新的令牌
319 |
320 | 你可以在系统中最多创建 10 个 Personal Access Token。
321 |
322 | - parameter scope: 可选 everything 或者 regular,如果是 regular 类型的 Token 将不能用于进一步创建新的 token
323 | - parameter expiration: 可支持的值:2592000,5184000,7776000 或者 15552000,即 30 天,60 天,90 天或者 180 天的秒数
324 | */
325 | public func createToken(expiration: Int, scope: String? = nil) async throws -> V2Response? {
326 | let path = "token"
327 | var args:[String: Any] = ["expiration": expiration]
328 | if let scope = scope {
329 | args["scope"] = scope
330 | }
331 |
332 | let (data, _) = try await request(
333 | httpMethod: "POST",
334 | url: endpointV2 + path,
335 | args: args,
336 | decodeClass: V2Response.self
337 | )
338 | return data
339 | }
340 | }
341 |
--------------------------------------------------------------------------------