Skip to content

Commit

Permalink
Support for parsing and displaying hidden comments
Browse files Browse the repository at this point in the history
  • Loading branch information
Ceylo committed Apr 22, 2024
1 parent 02586bc commit e927f15
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 135 deletions.
110 changes: 93 additions & 17 deletions FAKit/Sources/FAKit/FAComment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import FAPages
import SwiftGraph
import OrderedCollections

public struct FAComment: Equatable {
public enum FAComment: Equatable {
case visible(FAVisibleComment)
case hidden(FAHiddenComment)
}

public struct FAVisibleComment: Equatable {
public let cid: Int
public let author: String
public let displayAuthor: String
Expand All @@ -33,27 +38,98 @@ public struct FAComment: Equatable {
}
}

public struct FAHiddenComment: Equatable {
public let cid: Int
public let htmlMessage: String
public let answers: [FAComment]

public init(cid: Int, htmlMessage: String, answers: [FAComment]) {
self.cid = cid
self.htmlMessage = htmlMessage
self.answers = answers
}
}

extension FAComment {
public var cid: Int {
switch self {
case let .visible(comment):
comment.cid
case let .hidden(comment):
comment.cid
}
}

public var htmlMessage: String {
switch self {
case let .visible(comment):
comment.htmlMessage
case let .hidden(comment):
comment.htmlMessage
}
}

public var answers: [FAComment] {
switch self {
case let .visible(comment):
comment.answers
case let .hidden(comment):
comment.answers
}
}
}

extension FAComment {
init(_ comment: FAVisiblePageComment) {
self = .visible(.init(
cid: comment.cid,
author: comment.author,
displayAuthor: comment.displayAuthor,
authorAvatarUrl: comment.authorAvatarUrl,
datetime: comment.datetime,
naturalDatetime: comment.naturalDatetime,
htmlMessage: comment.htmlMessage.selfContainedFAHtmlComment,
answers: []
))
}

init(_ comment: FAHiddenPageComment) {
self = .hidden(.init(
cid: comment.cid,
htmlMessage: comment.htmlMessage.selfContainedFAHtmlComment,
answers: []
))
}

init(_ comment: FAPageComment) {
self.init(cid: comment.cid,
author: comment.author,
displayAuthor: comment.displayAuthor,
authorAvatarUrl: comment.authorAvatarUrl,
datetime: comment.datetime,
naturalDatetime: comment.naturalDatetime,
htmlMessage: comment.htmlMessage.selfContainedFAHtmlComment,
answers: [])
switch comment {
case let .visible(comment):
self.init(comment)
case let .hidden(comment):
self.init(comment)
}
}

func withAnswers(_ answers: [Self]) -> Self {
Self.init(cid: cid,
author: author,
displayAuthor: displayAuthor,
authorAvatarUrl: authorAvatarUrl,
datetime: datetime,
naturalDatetime: naturalDatetime,
htmlMessage: htmlMessage,
answers: answers)
switch self {
case let .visible(comment):
return .visible(.init(
cid: comment.cid,
author: comment.author,
displayAuthor: comment.displayAuthor,
authorAvatarUrl: comment.authorAvatarUrl,
datetime: comment.datetime,
naturalDatetime: comment.naturalDatetime,
htmlMessage: comment.htmlMessage,
answers: answers
))
case let .hidden(comment):
return .hidden(.init(
cid: comment.cid,
htmlMessage: comment.htmlMessage,
answers: answers
))
}
}

static func childrenOf(comment: FAPageComment,
Expand Down
100 changes: 76 additions & 24 deletions FAKit/Sources/FAPages/FAPageComment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,42 @@
import Foundation
import SwiftSoup

public struct FAPageComment: Equatable {
public enum FAPageComment: Equatable {
case visible(FAVisiblePageComment)
case hidden(FAHiddenPageComment)
}

extension FAPageComment {
public var cid: Int {
switch self {
case let .visible(comment):
comment.cid
case let .hidden(comment):
comment.cid
}
}

public var indentation: Int {
switch self {
case let .visible(comment):
comment.indentation
case let .hidden(comment):
comment.indentation
}
}

public var htmlMessage: String {
switch self {
case let .visible(comment):
comment.htmlMessage
case let .hidden(comment):
comment.htmlMessage
}
}
}


public struct FAVisiblePageComment: Equatable {
public let cid: Int
public let indentation: Int
public let author: String
Expand All @@ -19,6 +54,12 @@ public struct FAPageComment: Equatable {
public let htmlMessage: String
}

public struct FAHiddenPageComment: Equatable {
public let cid: Int
public let indentation: Int
public let htmlMessage: String
}

enum CommentType {
case comment
case shout
Expand All @@ -28,21 +69,25 @@ extension CommentType {
struct DecodingConfig {
let commentIdRegex: String
let messageQuery: String
let fallbackMessageQuery: String
let displayAuthorQuery: String
}

var decodingDonfig: DecodingConfig {
let defaultMessageQuery = "comment-container div.comment-content comment-user-text"
switch self {
case .comment:
return DecodingConfig(
commentIdRegex: "cid:(.+)",
messageQuery: "comment-container div.comment-content comment-user-text div.user-submitted-links",
fallbackMessageQuery: defaultMessageQuery,
displayAuthorQuery: "comment-container div.comment-content comment-username a.inline strong.comment_username"
)
case .shout:
return DecodingConfig(
commentIdRegex: "shout-(.+)",
messageQuery: "comment-container div.comment-content comment-user-text",
messageQuery: defaultMessageQuery,
fallbackMessageQuery: defaultMessageQuery,
displayAuthorQuery: "comment-container div.comment-content comment-username div.comment_username a.inline h3"
)
}
Expand All @@ -51,31 +96,38 @@ extension CommentType {

extension FAPageComment {
init?(_ node: SwiftSoup.Element, type: CommentType) throws {
let usernameNode = try node.select("comment-container div.comment-content comment-username")
guard !usernameNode.isEmpty() else {
let html = try? node.html()
logger.warning("Skipping comment: \(html ?? "", privacy: .public)")
return nil
}

let config = type.decodingDonfig
let widthStr = try node.attr("style").substring(matching: "width:(.+)%").unwrap()
let indentation = try 100 - Int(widthStr).unwrap()
let authorNode = try node.select("comment-container div.avatar a")
let author = try authorNode.attr("href").substring(matching: "/user/(.+)/").unwrap()
let authorAvatarUrlString = try authorNode.select("img").attr("src")
let authorAvatarUrl = try URL(unsafeString: "https:" + authorAvatarUrlString)
let displayAuthor = try node.select(config.displayAuthorQuery).text()
let rawCidString = try node.select("a").attr("id")
let cid = try Int(rawCidString.substring(matching: config.commentIdRegex).unwrap()).unwrap()
let datetimeNode = try node.select("comment-container div.comment-content comment-date span.popup_date")
let naturalDatetime = try datetimeNode.text()
let datetime = try datetimeNode.attr("title")
let htmlMessage = try node.select(config.messageQuery).first().unwrap().html()
let widthStr = try node.attr("style").substring(matching: "width:(.+)%").unwrap()
let indentation = try 100 - Int(widthStr).unwrap()
let htmlMessage: String
if let htmlMessageAttempt = try node.select(config.messageQuery).first() {
htmlMessage = try htmlMessageAttempt.html()
} else {
htmlMessage = try node.select(config.fallbackMessageQuery).first()
.unwrap().html()
}

self.init(cid: cid, indentation: indentation,
author: author, displayAuthor: displayAuthor,
authorAvatarUrl: authorAvatarUrl, datetime: datetime,
naturalDatetime: naturalDatetime, htmlMessage: htmlMessage)
do {
let authorNode = try node.select("comment-container div.avatar a")
let author = try authorNode.attr("href").substring(matching: "/user/(.+)/").unwrap()
let authorAvatarUrlString = try authorNode.select("img").attr("src")
let authorAvatarUrl = try URL(unsafeString: "https:" + authorAvatarUrlString)
let displayAuthor = try node.select(config.displayAuthorQuery).text()
let datetimeNode = try node.select("comment-container div.comment-content comment-date span.popup_date")
let naturalDatetime = try datetimeNode.text()
let datetime = try datetimeNode.attr("title")

self = .visible(.init(
cid: cid, indentation: indentation,
author: author, displayAuthor: displayAuthor,
authorAvatarUrl: authorAvatarUrl, datetime: datetime,
naturalDatetime: naturalDatetime, htmlMessage: htmlMessage
))
} catch {
logger.warning("Caught error: \(error)\nWhile parsing: \((try? node.html()) ?? "")")
self = .hidden(.init(cid: cid, indentation: indentation, htmlMessage: htmlMessage))
}
}
}
4 changes: 2 additions & 2 deletions FAKit/Tests/FAKitTests/FACommentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import XCTest

extension FAPageComment {
init(cid: Int, indentation: Int) {
self.init(
self = .visible(.init(
cid: cid, indentation: indentation, author: "t", displayAuthor: "T",
authorAvatarUrl: URL(string: "https://some.url/")!, datetime: "Aug 12, 2022 04:08 AM", naturalDatetime: "Today", htmlMessage: "Msg"
)
))
}
}

Expand Down
46 changes: 23 additions & 23 deletions FAKit/Tests/FAPagesTests/FAJournalPageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,29 @@ final class FAJournalPageTests: XCTestCase {
XCTAssertNotNil(page)

let expectedComments: [FAPageComment] = [
.init(cid: 59820550, indentation: 0, author: "fukothenimbat", displayAuthor: "FukoTheNimbat",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1643296327/fukothenimbat.gif")!,
datetime: "Apr 3, 2023 12:01 AM", naturalDatetime: "2 weeks ago", htmlMessage: "Ill take one"),
.init(cid: 59820552, indentation: 0, author: "zacharywulf", displayAuthor: "Zacharywulf",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1577257416/zacharywulf.gif")!,
datetime: "Apr 3, 2023 12:01 AM", naturalDatetime: "2 weeks ago", htmlMessage: "I want one!"),
.init(cid: 59820567, indentation: 0, author: "leacrea", displayAuthor: "Leacrea",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1598944742/leacrea.gif")!,
datetime: "Apr 3, 2023 12:11 AM", naturalDatetime: "2 weeks ago", htmlMessage: "I’ll take one"),
.init(cid: 59820573, indentation: 0, author: "thegrapedemon", displayAuthor: "TheGrapeDemon",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1628047319/thegrapedemon.gif")!,
datetime: "Apr 3, 2023 12:17 AM", naturalDatetime: "2 weeks ago", htmlMessage: "I would love one please!! <3"),
.init(cid: 59820579, indentation: 0, author: "shadoweddraco", displayAuthor: "ShadowedDraco",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1647799580/shadoweddraco.gif")!,
datetime: "Apr 3, 2023 12:22 AM", naturalDatetime: "2 weeks ago", htmlMessage: "i will take aslot"),
.init(cid: 59820673, indentation: 0, author: "xaraphiel", displayAuthor: "Xaraphiel",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1628022893/xaraphiel.gif")!,
datetime: "Apr 3, 2023 02:34 AM", naturalDatetime: "2 weeks ago",
htmlMessage: "Awww I keep waking up and finding all slots are taken. I hate time zones 😭😅\n<br> \n<br> Congrats all who got upgrades 😇"),
.init(cid: 59820831, indentation: 3, author: "flamekillaxxx", displayAuthor: "flamekillaXxX",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1680473466/flamekillaxxx.gif")!,
datetime: "Apr 3, 2023 06:37 AM", naturalDatetime: "2 weeks ago",
htmlMessage: "I know that feeling! Best of luck to us if there is another round \n<i class=\"smilie love\"></i>")
.visible(.init(cid: 59820550, indentation: 0, author: "fukothenimbat", displayAuthor: "FukoTheNimbat",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1643296327/fukothenimbat.gif")!,
datetime: "Apr 3, 2023 12:01 AM", naturalDatetime: "2 weeks ago", htmlMessage: "Ill take one")),
.visible(.init(cid: 59820552, indentation: 0, author: "zacharywulf", displayAuthor: "Zacharywulf",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1577257416/zacharywulf.gif")!,
datetime: "Apr 3, 2023 12:01 AM", naturalDatetime: "2 weeks ago", htmlMessage: "I want one!")),
.visible(.init(cid: 59820567, indentation: 0, author: "leacrea", displayAuthor: "Leacrea",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1598944742/leacrea.gif")!,
datetime: "Apr 3, 2023 12:11 AM", naturalDatetime: "2 weeks ago", htmlMessage: "I’ll take one")),
.visible(.init(cid: 59820573, indentation: 0, author: "thegrapedemon", displayAuthor: "TheGrapeDemon",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1628047319/thegrapedemon.gif")!,
datetime: "Apr 3, 2023 12:17 AM", naturalDatetime: "2 weeks ago", htmlMessage: "I would love one please!! &lt;3")),
.visible(.init(cid: 59820579, indentation: 0, author: "shadoweddraco", displayAuthor: "ShadowedDraco",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1647799580/shadoweddraco.gif")!,
datetime: "Apr 3, 2023 12:22 AM", naturalDatetime: "2 weeks ago", htmlMessage: "i will take aslot")),
.visible(.init(cid: 59820673, indentation: 0, author: "xaraphiel", displayAuthor: "Xaraphiel",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1628022893/xaraphiel.gif")!,
datetime: "Apr 3, 2023 02:34 AM", naturalDatetime: "2 weeks ago",
htmlMessage: "Awww I keep waking up and finding all slots are taken. I hate time zones 😭😅\n<br> \n<br> Congrats all who got upgrades 😇")),
.visible(.init(cid: 59820831, indentation: 3, author: "flamekillaxxx", displayAuthor: "flamekillaXxX",
authorAvatarUrl: URL(string: "https://a.furaffinity.net/1680473466/flamekillaxxx.gif")!,
datetime: "Apr 3, 2023 06:37 AM", naturalDatetime: "2 weeks ago",
htmlMessage: "I know that feeling! Best of luck to us if there is another round \n<i class=\"smilie love\"></i>"))
]

let expectedHtmlDescription = """
Expand Down
Loading

0 comments on commit e927f15

Please sign in to comment.