-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Report StackOverflow questions (#119)
- Loading branch information
Showing
17 changed files
with
401 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import Logging | ||
import DiscordBM | ||
import Markdown | ||
import Foundation | ||
|
||
actor SOChecker { | ||
|
||
struct Storage: Sendable, Codable { | ||
var lastCheckDate: Date? | ||
} | ||
|
||
var storage = Storage() | ||
|
||
let soService: any SOService | ||
let discordService: DiscordService | ||
let logger = Logger(label: "SOChecker") | ||
|
||
init(soService: any SOService, discordService: DiscordService) { | ||
self.soService = soService | ||
self.discordService = discordService | ||
} | ||
|
||
nonisolated func run() { | ||
Task { [self] in | ||
if Task.isCancelled { return } | ||
do { | ||
try await self.check() | ||
} catch { | ||
logger.report("Couldn't check SO questions", error: error) | ||
} | ||
try await Task.sleep(for: .seconds(60 * 5)) /// 5 mins | ||
self.run() | ||
} | ||
} | ||
|
||
func check() async throws { | ||
let after = storage.lastCheckDate ?? Date().addingTimeInterval(-60 * 60) | ||
let questions = try await soService.listQuestions(after: after) | ||
storage.lastCheckDate = Date() | ||
|
||
for question in questions { | ||
await discordService.sendMessage( | ||
channelId: Constants.Channels.stackOverflow.id, | ||
payload: .init(embeds: [.init( | ||
title: question.title.htmlDecoded().unicodesPrefix(256), | ||
url: question.link, | ||
timestamp: Date(timeIntervalSince1970: Double(question.creationDate)), | ||
color: .mint, | ||
footer: .init( | ||
text: "By \(question.owner.displayName)", | ||
icon_url: question.owner.profileImage.map { .exact($0) } | ||
) | ||
)]) | ||
) | ||
} | ||
} | ||
|
||
func consumeCachesStorageData(_ storage: Storage) { | ||
self.storage = storage | ||
} | ||
|
||
func getCachedDataForCachesStorage() -> Storage { | ||
return self.storage | ||
} | ||
} | ||
|
||
// MARK: +String | ||
private extension String { | ||
func htmlDecoded() -> String { | ||
Document(parsing: self).format() | ||
} | ||
} | ||
|
||
// MARK: - SOQuestions | ||
struct SOQuestions: Codable { | ||
|
||
struct Item: Codable { | ||
|
||
struct Owner: Codable { | ||
let accountID: Int? | ||
let reputation: Int? | ||
let userID: Int? | ||
let userType: String | ||
let acceptRate: Int? | ||
let profileImage: String? | ||
let displayName: String | ||
let link: String? | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case accountID = "account_id" | ||
case reputation | ||
case userID = "user_id" | ||
case userType = "user_type" | ||
case acceptRate = "accept_rate" | ||
case profileImage = "profile_image" | ||
case displayName = "display_name" | ||
case link | ||
} | ||
} | ||
|
||
let tags: [String] | ||
let owner: Owner | ||
let isAnswered: Bool | ||
let viewCount: Int | ||
let acceptedAnswerID: Int? | ||
let answerCount: Int | ||
let score: Int | ||
let lastActivityDate: Int | ||
let creationDate: Int | ||
let questionID: Int | ||
let contentLicense: String? | ||
let link: String | ||
let title: String | ||
let lastEditDate: Int? | ||
let closedDate: Int? | ||
let closedReason: String? | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case tags, owner | ||
case isAnswered = "is_answered" | ||
case viewCount = "view_count" | ||
case acceptedAnswerID = "accepted_answer_id" | ||
case answerCount = "answer_count" | ||
case score | ||
case lastActivityDate = "last_activity_date" | ||
case creationDate = "creation_date" | ||
case questionID = "question_id" | ||
case contentLicense = "content_license" | ||
case link, title | ||
case lastEditDate = "last_edit_date" | ||
case closedDate = "closed_date" | ||
case closedReason = "closed_reason" | ||
} | ||
} | ||
|
||
let items: [Item] | ||
let hasMore: Bool | ||
let quotaMax: Int | ||
let quotaRemaining: Int | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case items | ||
case hasMore = "has_more" | ||
case quotaMax = "quota_max" | ||
case quotaRemaining = "quota_remaining" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
Sources/Penny/Services/StackoverflowService /DefaultSOService.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import AsyncHTTPClient | ||
import NIOCore | ||
import Foundation | ||
|
||
struct DefaultSOService: SOService { | ||
let httpClient: HTTPClient | ||
let decoder = JSONDecoder() | ||
|
||
func listQuestions(after: Date) async throws -> [SOQuestions.Item] { | ||
let queries: KeyValuePairs = [ | ||
"site": "stackoverflow", | ||
"tagged": "vapor", | ||
"nottagged": "laravel", /// Don't be a "laravel" questin | ||
"page": "1", | ||
"sort": "creation", | ||
"order": "desc", | ||
"fromdate": "\(Int(after.timeIntervalSince1970))", | ||
"pagesize": "100", | ||
"key": Constants.StackOverflow.apiKey, | ||
] | ||
let url = "https://api.stackexchange.com/2.3/search/advanced" + queries.makeForURLQueryUnchecked() | ||
let request = HTTPClientRequest(url: url) | ||
let response = try await httpClient.execute(request, deadline: .now() + .seconds(15)) | ||
let buffer = try await response.body.collect(upTo: 1 << 25) /// 32 MB | ||
let questions = try decoder.decode( | ||
SOQuestions.self, | ||
from: buffer | ||
).items | ||
return questions | ||
} | ||
} | ||
|
||
private extension KeyValuePairs<String, String> { | ||
/// Doesn't do url-query encoding. | ||
/// Assumes the values are already safe. | ||
func makeForURLQueryUnchecked() -> String { | ||
"?" + self.map { "\($0)=\($1)" }.joined(separator: "&") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Foundation | ||
|
||
protocol SOService { | ||
func listQuestions(after: Date) async throws -> [SOQuestions.Item] | ||
} |
Oops, something went wrong.