Merge pull request #686 from mastodon/mute_block_delete_data

This commit is contained in:
Marcus Kida 2022-11-28 08:15:02 +01:00 committed by GitHub
commit 097c99cc65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 294 additions and 3 deletions

View File

@ -16,10 +16,17 @@ extension DataSourceFacade {
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await dependency.context.apiService.toggleBlock(
let apiService = dependency.context.apiService
let authBox = dependency.authContext.mastodonAuthenticationBox
_ = try await apiService.toggleBlock(
user: user,
authenticationBox: dependency.authContext.mastodonAuthenticationBox
authenticationBox: authBox
)
try await dependency.context.apiService.getBlocked(
authenticationBox: authBox
)
} // end func
}

View File

@ -86,6 +86,8 @@ extension HomeTimelineViewModel.LoadLatestState {
await enter(state: Idle.self)
viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(.finished)
viewModel.context.instanceService.updateMutesAndBlocks()
// stop refresher if no new statuses
let statuses = response.value
let newStatuses = statuses.filter { !latestStatusIDs.contains($0.id) }

View File

@ -112,6 +112,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// trigger authenticated user account update
AppContext.shared.authenticationService.updateActiveUserAccountPublisher.send()
// update mutes and blocks and remove related data
AppContext.shared.instanceService.updateMutesAndBlocks()
if let shortcutItem = savedShortCutItem {
Task {

View File

@ -22,6 +22,45 @@ extension APIService {
let isFollowing: Bool
}
@discardableResult
public func getBlocked(
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
try await _getBlocked(sinceID: nil, limit: nil, authenticationBox: authenticationBox)
}
private func _getBlocked(
sinceID: Mastodon.Entity.Status.ID?,
limit: Int?,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
let managedObjectContext = backgroundManagedObjectContext
let response = try await Mastodon.API.Account.blocks(
session: session,
domain: authenticationBox.domain,
sinceID: sinceID,
limit: limit,
authorization: authenticationBox.userAuthorization
).singleOutput()
let userIDs = response.value.map { $0.id }
let predicate = MastodonUser.predicate(domain: authenticationBox.domain, ids: userIDs)
let fetchRequest = MastodonUser.fetchRequest()
fetchRequest.predicate = predicate
fetchRequest.includesPropertyValues = false
try await managedObjectContext.performChanges {
let users = try managedObjectContext.fetch(fetchRequest) as! [MastodonUser]
for user in users {
user.deleteStatusAndNotificationFeeds(in: managedObjectContext)
}
}
return response
}
public func toggleBlock(
user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
@ -110,3 +149,21 @@ extension APIService {
}
}
extension MastodonUser {
func deleteStatusAndNotificationFeeds(in context: NSManagedObjectContext) {
statuses.map {
$0.feeds
.union($0.reblogFrom.map { $0.feeds }.flatMap { $0 })
.union($0.notifications.map { $0.feeds }.flatMap { $0 })
}
.flatMap { $0 }
.forEach(context.delete)
notifications.map {
$0.feeds
}
.flatMap { $0 }
.forEach(context.delete)
}
}

View File

@ -21,6 +21,45 @@ extension APIService {
let isMuting: Bool
}
@discardableResult
public func getMutes(
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
try await _getMutes(sinceID: nil, limit: nil, authenticationBox: authenticationBox)
}
private func _getMutes(
sinceID: Mastodon.Entity.Status.ID?,
limit: Int?,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
let managedObjectContext = backgroundManagedObjectContext
let response = try await Mastodon.API.Account.mutes(
session: session,
domain: authenticationBox.domain,
sinceID: sinceID,
limit: limit,
authorization: authenticationBox.userAuthorization
).singleOutput()
let userIDs = response.value.map { $0.id }
let predicate = MastodonUser.predicate(domain: authenticationBox.domain, ids: userIDs)
let fetchRequest = MastodonUser.fetchRequest()
fetchRequest.predicate = predicate
fetchRequest.includesPropertyValues = false
try await managedObjectContext.performChanges {
let users = try managedObjectContext.fetch(fetchRequest) as! [MastodonUser]
for user in users {
user.deleteStatusAndNotificationFeeds(in: managedObjectContext)
}
}
return response
}
public func toggleMute(
user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
@ -58,6 +97,7 @@ extension APIService {
accountID: muteContext.targetUserID,
authorization: authenticationBox.userAuthorization
).singleOutput()
try await getMutes(authenticationBox: authenticationBox)
result = .success(response)
} else {
let response = try await Mastodon.API.Account.mute(
@ -66,6 +106,7 @@ extension APIService {
accountID: muteContext.targetUserID,
authorization: authenticationBox.userAuthorization
).singleOutput()
try await getMutes(authenticationBox: authenticationBox)
result = .success(response)
}
} catch {

View File

@ -101,3 +101,25 @@ extension InstanceService {
.store(in: &disposeBag)
}
}
public extension InstanceService {
func updateMutesAndBlocks() {
Task {
for authBox in authenticationService?.mastodonAuthenticationBoxes ?? [] {
do {
try await apiService?.getMutes(
authenticationBox: authBox
)
try await apiService?.getBlocked(
authenticationBox: authBox
)
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Instance] update mutes and blocks succeeded")
} catch {
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Instance] update mutes and blocks failure: \(error.localizedDescription)")
}
}
}
}
}

View File

@ -215,6 +215,70 @@ extension Mastodon.API.Account {
}
public extension Mastodon.API.Account {
static func blocksEndpointURL(domain: String) -> URL {
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("blocks")
}
/// Block
///
/// Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline).
///
/// - Since: 0.0.0
/// - Version: 3.3.0
/// # Last Update
/// 2021/4/1
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/blocks/)
/// - Parameters:
/// - session: `URLSession`
/// - domain: Mastodon instance domain. e.g. "example.com"
/// - authorization: User token.
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
static func blocks(
session: URLSession,
domain: String,
sinceID: Mastodon.Entity.Status.ID?,
limit: Int?,
authorization: Mastodon.API.OAuth.Authorization
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
let request = Mastodon.API.get(
url: blocksEndpointURL(domain: domain),
query: BlocksQuery(sinceID: sinceID, limit: limit),
authorization: authorization
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Account].self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
private struct BlocksQuery: GetQuery {
private let sinceID: Mastodon.Entity.Status.ID?
private let limit: Int?
public init(
sinceID: Mastodon.Entity.Status.ID?,
limit: Int?
) {
self.sinceID = sinceID
self.limit = limit
}
var queryItems: [URLQueryItem]? {
var items: [URLQueryItem] = []
sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) }
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
guard !items.isEmpty else { return nil }
return items
}
}
}
extension Mastodon.API.Account {
static func blockEndpointURL(domain: String, accountID: Mastodon.Entity.Account.ID) -> URL {
@ -414,3 +478,70 @@ extension Mastodon.API.Account {
}
}
extension Mastodon.API.Account {
static func mutesEndpointURL(
domain: String
) -> URL {
return Mastodon.API.endpointURL(domain: domain)
.appendingPathComponent("mutes")
}
/// View all mutes
///
/// View your mutes. See also accounts/:id/{mute,unmute}.
///
/// - Since: 0.0.0
/// - Version: 3.3.0
/// # Last Update
/// 2021/4/1
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/accounts/)
/// - Parameters:
/// - session: `URLSession`
/// - domain: Mastodon instance domain. e.g. "example.com"
/// - accountID: id for account
/// - authorization: User token.
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
public static func mutes(
session: URLSession,
domain: String,
sinceID: Mastodon.Entity.Status.ID? = nil,
limit: Int?,
authorization: Mastodon.API.OAuth.Authorization
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
let request = Mastodon.API.get(
url: mutesEndpointURL(domain: domain),
query: MutesQuery(sinceID: sinceID, limit: limit),
authorization: authorization
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Account].self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
struct MutesQuery: GetQuery {
private let sinceID: Mastodon.Entity.Status.ID?
private let limit: Int?
public init(
sinceID: Mastodon.Entity.Status.ID?,
limit: Int?
) {
self.sinceID = sinceID
self.limit = limit
}
var queryItems: [URLQueryItem]? {
var items: [URLQueryItem] = []
sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) }
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
guard !items.isEmpty else { return nil }
return items
}
}
}
}

View File

@ -106,6 +106,7 @@ extension Mastodon.Response {
public struct Link {
public let maxID: Mastodon.Entity.Status.ID?
public let minID: Mastodon.Entity.Status.ID?
public let linkIDs: [String: Mastodon.Entity.Status.ID]
public let offset: Int?
init(link: String) {
@ -135,6 +136,33 @@ extension Mastodon.Response {
let offset = link[range]
return Int(offset)
}()
self.linkIDs = {
var linkIDs = [String: Mastodon.Entity.Status.ID]()
let links = link.components(separatedBy: ", ")
for link in links {
guard let regex = try? NSRegularExpression(pattern: "<(.*)>; *rel=\"(.*)\"") else { return [:] }
let results = regex.matches(in: link, options: [], range: NSRange(link.startIndex..<link.endIndex, in: link))
for match in results {
guard
let labelRange = Range(match.range(at: 2), in: link),
let linkRange = Range(match.range(at: 1), in: link)
else {
continue
}
linkIDs[String(link[labelRange])] = String(link[linkRange])
}
}
return linkIDs
}()
}
}
}
public extension Mastodon.Entity.Status.ID {
static let linkPrev = "prev"
static let linkNext = "next"
var sinceId: String? {
components(separatedBy: "&since_id=").last
}
}