mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
- add option to select the network reader (interface based or process based)
- small refactoring in Network reader
This commit is contained in:
@@ -28,11 +28,8 @@ public struct Network_interface {
|
||||
}
|
||||
|
||||
public struct Network_Usage: value_t {
|
||||
var download: Int64 = 0
|
||||
var upload: Int64 = 0
|
||||
|
||||
var totalDownload: Int64 = 0
|
||||
var totalUpload: Int64 = 0
|
||||
var bandwidth: Bandwidth = (0, 0)
|
||||
var total: Bandwidth = (0, 0)
|
||||
|
||||
var laddr: String? = nil // local ip
|
||||
var raddr: String? = nil // remote ip
|
||||
@@ -44,8 +41,7 @@ public struct Network_Usage: value_t {
|
||||
var ssid: String? = nil
|
||||
|
||||
mutating func reset() {
|
||||
self.download = 0
|
||||
self.upload = 0
|
||||
self.bandwidth = (0, 0)
|
||||
|
||||
self.laddr = nil
|
||||
self.raddr = nil
|
||||
@@ -142,7 +138,7 @@ public class Network: Module {
|
||||
|
||||
self.popupView.usageCallback(value!)
|
||||
if let widget = self.widget as? SpeedWidget {
|
||||
widget.setValue(upload: value!.upload, download: value!.download)
|
||||
widget.setValue(upload: value!.bandwidth.upload, download: value!.bandwidth.download)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,12 +306,12 @@ internal class Popup: NSView, Popup_p {
|
||||
public func usageCallback(_ value: Network_Usage) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if (self.window?.isVisible ?? false) || !self.initialized {
|
||||
self.uploadValue = value.upload
|
||||
self.downloadValue = value.download
|
||||
self.uploadValue = value.bandwidth.upload
|
||||
self.downloadValue = value.bandwidth.download
|
||||
self.setUploadDownloadFields()
|
||||
|
||||
self.totalUploadField?.stringValue = Units(bytes: value.totalUpload).getReadableMemory()
|
||||
self.totalDownloadField?.stringValue = Units(bytes: value.totalDownload).getReadableMemory()
|
||||
self.totalUploadField?.stringValue = Units(bytes: value.total.upload).getReadableMemory()
|
||||
self.totalDownloadField?.stringValue = Units(bytes: value.total.download).getReadableMemory()
|
||||
|
||||
if let interface = value.interface {
|
||||
self.interfaceField?.stringValue = "\(interface.displayName) (\(interface.BSDName))"
|
||||
@@ -345,7 +345,7 @@ internal class Popup: NSView, Popup_p {
|
||||
self.initialized = true
|
||||
}
|
||||
|
||||
self.chart?.addValue(upload: Double(value.upload), download: Double(value.download))
|
||||
self.chart?.addValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -24,15 +24,11 @@ struct ipResponse: Decodable {
|
||||
}
|
||||
|
||||
internal class UsageReader: Reader<Network_Usage> {
|
||||
typealias BandwidthUsage = (upload: Int64, download: Int64)
|
||||
|
||||
public var store: UnsafePointer<Store>? = nil
|
||||
|
||||
private var reachability: Reachability? = nil
|
||||
private var usage: Network_Usage = Network_Usage()
|
||||
|
||||
private var shouldReportSelectedInterfaceBandwidthOnly = true
|
||||
|
||||
private var primaryInterface: String {
|
||||
get {
|
||||
if let global = SCDynamicStoreCopyValue(nil, "State:/Network/Global/IPv4" as CFString), let name = global["PrimaryInterface"] as? String {
|
||||
@@ -51,6 +47,12 @@ internal class UsageReader: Reader<Network_Usage> {
|
||||
}
|
||||
}
|
||||
|
||||
private var reader: String {
|
||||
get {
|
||||
return self.store?.pointee.string(key: "Network_reader", defaultValue: "interface") ?? "interface"
|
||||
}
|
||||
}
|
||||
|
||||
public override func setup() {
|
||||
do {
|
||||
self.reachability = try Reachability()
|
||||
@@ -73,23 +75,108 @@ internal class UsageReader: Reader<Network_Usage> {
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
let currentUsage: BandwidthUsage
|
||||
if shouldReportSelectedInterfaceBandwidthOnly {
|
||||
currentUsage = interfaceBandwidthUsage()
|
||||
} else {
|
||||
currentUsage = allProcessesBandwidthUsage()
|
||||
let current: Bandwidth = self.reader == "interface" ? self.readInterfaceBandwidth() : self.readProcessBandwidth()
|
||||
|
||||
// allows to reset the value to 0 when first read
|
||||
if self.usage.bandwidth.upload != 0 {
|
||||
self.usage.bandwidth.upload = current.upload - self.usage.bandwidth.upload
|
||||
}
|
||||
if self.usage.bandwidth.download != 0 {
|
||||
self.usage.bandwidth.download = current.download - self.usage.bandwidth.download
|
||||
}
|
||||
|
||||
self.usage.upload = max(currentUsage.upload - self.usage.upload, 0)
|
||||
self.usage.download = max(currentUsage.download - self.usage.download, 0)
|
||||
self.usage.bandwidth.upload = max(self.usage.bandwidth.upload, 0) // prevent negative upload value
|
||||
self.usage.bandwidth.download = max(self.usage.bandwidth.download, 0) // prevent negative download value
|
||||
|
||||
self.usage.totalUpload += self.usage.upload
|
||||
self.usage.totalDownload += self.usage.download
|
||||
self.usage.total.upload += self.usage.bandwidth.upload
|
||||
self.usage.total.download += self.usage.bandwidth.download
|
||||
|
||||
self.callback(self.usage)
|
||||
|
||||
self.usage.upload = currentUsage.upload
|
||||
self.usage.download = currentUsage.download
|
||||
self.usage.bandwidth.upload = current.upload
|
||||
self.usage.bandwidth.download = current.download
|
||||
}
|
||||
|
||||
private func readInterfaceBandwidth() -> Bandwidth {
|
||||
var interfaceAddresses: UnsafeMutablePointer<ifaddrs>? = nil
|
||||
var totalUpload: Int64 = 0
|
||||
var totalDownload: Int64 = 0
|
||||
guard getifaddrs(&interfaceAddresses) == 0 else {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
var pointer = interfaceAddresses
|
||||
while pointer != nil {
|
||||
defer { pointer = pointer?.pointee.ifa_next }
|
||||
|
||||
if String(cString: pointer!.pointee.ifa_name) != self.interfaceID {
|
||||
continue
|
||||
}
|
||||
|
||||
if let ip = getLocalIP(pointer!), self.usage.laddr != ip {
|
||||
self.usage.laddr = ip
|
||||
}
|
||||
|
||||
if let info = getBytesInfo(pointer!) {
|
||||
totalUpload += info.upload
|
||||
totalDownload += info.download
|
||||
}
|
||||
}
|
||||
freeifaddrs(interfaceAddresses)
|
||||
|
||||
return (totalUpload, totalDownload)
|
||||
}
|
||||
|
||||
private func readProcessBandwidth() -> Bandwidth {
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/bin/nettop"
|
||||
task.arguments = ["-P", "-L", "1", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"]
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
} catch let error {
|
||||
os_log(.error, log: log, "read bandwidth from processes %s", "\(error)")
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(decoding: outputData, as: UTF8.self)
|
||||
_ = String(decoding: errorData, as: UTF8.self)
|
||||
|
||||
if output.isEmpty {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
var totalUpload: Int64 = 0
|
||||
var totalDownload: Int64 = 0
|
||||
var firstLine = false
|
||||
output.enumerateLines { (line, _) -> () in
|
||||
if !firstLine {
|
||||
firstLine = true
|
||||
return
|
||||
}
|
||||
|
||||
let parsedLine = line.split(separator: ",")
|
||||
guard parsedLine.count >= 3 else {
|
||||
return
|
||||
}
|
||||
|
||||
if let download = Int64(parsedLine[1]) {
|
||||
totalDownload += download
|
||||
}
|
||||
if let upload = Int64(parsedLine[2]) {
|
||||
totalUpload += upload
|
||||
}
|
||||
}
|
||||
|
||||
return (totalUpload, totalDownload)
|
||||
}
|
||||
|
||||
public func getDetails() {
|
||||
@@ -172,89 +259,6 @@ internal class UsageReader: Reader<Network_Usage> {
|
||||
let data: UnsafeMutablePointer<if_data>? = unsafeBitCast(pointer.pointee.ifa_data, to: UnsafeMutablePointer<if_data>.self)
|
||||
return (upload: Int64(data?.pointee.ifi_obytes ?? 0), download: Int64(data?.pointee.ifi_ibytes ?? 0))
|
||||
}
|
||||
|
||||
private func interfaceBandwidthUsage() -> BandwidthUsage {
|
||||
var interfaceAddresses: UnsafeMutablePointer<ifaddrs>? = nil
|
||||
var totalUpload: Int64 = 0
|
||||
var totalDownload: Int64 = 0
|
||||
guard getifaddrs(&interfaceAddresses) == 0 else {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
var pointer = interfaceAddresses
|
||||
while pointer != nil {
|
||||
defer { pointer = pointer?.pointee.ifa_next }
|
||||
|
||||
if String(cString: pointer!.pointee.ifa_name) != self.interfaceID {
|
||||
continue
|
||||
}
|
||||
|
||||
if let ip = getLocalIP(pointer!), self.usage.laddr != ip {
|
||||
self.usage.laddr = ip
|
||||
}
|
||||
|
||||
if let info = getBytesInfo(pointer!) {
|
||||
totalUpload += info.upload
|
||||
totalDownload += info.download
|
||||
}
|
||||
}
|
||||
freeifaddrs(interfaceAddresses)
|
||||
|
||||
return (totalUpload, totalDownload)
|
||||
}
|
||||
|
||||
private func allProcessesBandwidthUsage() -> BandwidthUsage {
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/bin/nettop"
|
||||
task.arguments = ["-P", "-L", "1", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"]
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
} catch let error {
|
||||
print(error)
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(decoding: outputData, as: UTF8.self)
|
||||
_ = String(decoding: errorData, as: UTF8.self)
|
||||
|
||||
if output.isEmpty {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
var totalUpload: Int64 = 0
|
||||
var totalDownload: Int64 = 0
|
||||
var firstLine = false
|
||||
output.enumerateLines { (line, _) -> () in
|
||||
if !firstLine {
|
||||
firstLine = true
|
||||
return
|
||||
}
|
||||
|
||||
let parsedLine = line.split(separator: ",")
|
||||
guard parsedLine.count >= 3 else {
|
||||
return
|
||||
}
|
||||
|
||||
if let download = Int(parsedLine[1]) {
|
||||
totalDownload += Int64(download)
|
||||
}
|
||||
if let upload = Int(parsedLine[2]) {
|
||||
totalUpload += Int64(upload)
|
||||
}
|
||||
}
|
||||
|
||||
return (totalUpload, totalDownload)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ProcessReader: Reader<[Network_Process]> {
|
||||
|
||||
@@ -16,6 +16,7 @@ import SystemConfiguration
|
||||
|
||||
internal class Settings: NSView, Settings_v {
|
||||
private var numberOfProcesses: Int = 8
|
||||
private var readerType: String = "interface"
|
||||
|
||||
public var callback: (() -> Void) = {}
|
||||
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
|
||||
@@ -30,6 +31,7 @@ internal class Settings: NSView, Settings_v {
|
||||
self.title = title
|
||||
self.store = store
|
||||
self.numberOfProcesses = store.pointee.int(key: "\(self.title)_processes", defaultValue: self.numberOfProcesses)
|
||||
self.readerType = store.pointee.string(key: "\(self.title)_reader", defaultValue: self.readerType)
|
||||
|
||||
super.init(frame: CGRect(
|
||||
x: 0,
|
||||
@@ -56,22 +58,30 @@ internal class Settings: NSView, Settings_v {
|
||||
self.subviews.forEach{ $0.removeFromSuperview() }
|
||||
|
||||
let rowHeight: CGFloat = 30
|
||||
let num: CGFloat = 1
|
||||
|
||||
self.addNetworkSelector()
|
||||
let num: CGFloat = 2
|
||||
|
||||
self.addSubview(SelectTitleRow(
|
||||
frame: NSRect(x: Constants.Settings.margin, y: rowHeight + (Constants.Settings.margin*2), width: self.frame.width - (Constants.Settings.margin*2), height: 30),
|
||||
frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin + (rowHeight + Constants.Settings.margin) * 2, width: self.frame.width - (Constants.Settings.margin*2), height: 30),
|
||||
title: LocalizedString("Number of top processes"),
|
||||
action: #selector(changeNumberOfProcesses),
|
||||
items: NumbersOfProcesses.map{ "\($0)" },
|
||||
selected: "\(self.numberOfProcesses)"
|
||||
))
|
||||
|
||||
self.addSubview(SelectRow(
|
||||
frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin + (rowHeight + Constants.Settings.margin) * 1, width: self.frame.width - (Constants.Settings.margin*2), height: 30),
|
||||
title: LocalizedString("Reader type"),
|
||||
action: #selector(changeReaderType),
|
||||
items: NetworkReaders,
|
||||
selected: self.readerType
|
||||
))
|
||||
|
||||
self.addInterfaceSelector()
|
||||
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: (rowHeight*(num+1)) + (Constants.Settings.margin*(2+num))))
|
||||
}
|
||||
|
||||
private func addNetworkSelector() {
|
||||
private func addInterfaceSelector() {
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: self.frame.width, height: 30))
|
||||
|
||||
let rowTitle: NSTextField = LabelField(frame: NSRect(x: 0, y: (view.frame.height - 16)/2, width: view.frame.width - 52, height: 17), LocalizedString("Network interface"))
|
||||
@@ -79,8 +89,9 @@ internal class Settings: NSView, Settings_v {
|
||||
rowTitle.textColor = .textColor
|
||||
|
||||
self.button = NSPopUpButton(frame: NSRect(x: view.frame.width - 200 - Constants.Settings.margin*2, y: 0, width: 200, height: 30))
|
||||
self.button!.target = self
|
||||
self.button?.target = self
|
||||
self.button?.action = #selector(self.handleSelection)
|
||||
self.button?.isEnabled = self.readerType == "interface"
|
||||
|
||||
let selectedInterface = self.store.pointee.string(key: "\(self.title)_interface", defaultValue: "")
|
||||
let menu = NSMenu()
|
||||
@@ -130,4 +141,13 @@ internal class Settings: NSView, Settings_v {
|
||||
self.callbackWhenUpdateNumberOfProcesses()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func changeReaderType(_ sender: NSMenuItem) {
|
||||
guard let key = sender.representedObject as? String else {
|
||||
return
|
||||
}
|
||||
self.readerType = key
|
||||
self.store.pointee.set(key: "\(self.title)_reader", value: key)
|
||||
self.button?.isEnabled = self.readerType == "interface"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"Network interface" = "Netzwerkschnittstelle";
|
||||
"Total download" = "Empfangen";
|
||||
"Total upload" = "Gesendet";
|
||||
"Reader type" = "Lesertyp";
|
||||
"Interface" = "Schnittstelle";
|
||||
"Process" = "Prozess";
|
||||
|
||||
// Battery
|
||||
"Level" = "Ladezustand";
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"Network interface" = "Network interface";
|
||||
"Total download" = "Total download";
|
||||
"Total upload" = "Total upload";
|
||||
"Reader type" = "Reader type";
|
||||
"Interface" = "Interface";
|
||||
"Process" = "Process";
|
||||
|
||||
// Battery
|
||||
"Level" = "Level";
|
||||
|
||||
@@ -132,6 +132,9 @@
|
||||
"Network interface" = "Interfaz de red";
|
||||
"Total download" = "Descarga total";
|
||||
"Total upload" = "Carga total";
|
||||
"Reader type" = "Tipo de lector";
|
||||
"Interface" = "Interfaz";
|
||||
"Process" = "Proceso";
|
||||
|
||||
// Battery
|
||||
"Level" = "Nivel";
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"Network interface" = "Interface réseau";
|
||||
"Total download" = "Total download";
|
||||
"Total upload" = "Total upload";
|
||||
"Reader type" = "Type de lecteur";
|
||||
"Interface" = "Interface";
|
||||
"Process" = "Processus";
|
||||
|
||||
// Battery
|
||||
"Level" = "Niveau";
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"Network interface" = "Interfaccia di rete";
|
||||
"Total download" = "Download totale";
|
||||
"Total upload" = "Upload totale";
|
||||
"Reader type" = "Tipo di lettore";
|
||||
"Interface" = "Interfaccia";
|
||||
"Process" = "Processi";
|
||||
|
||||
// Battery
|
||||
"Level" = "Livello";
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"Network interface" = "네트워크 인터페이스";
|
||||
"Total download" = "총 다운로드";
|
||||
"Total upload" = "총 업로드";
|
||||
"Reader type" = "리더 유형";
|
||||
"Interface" = "상호 작용";
|
||||
"Process" = "프로세스";
|
||||
|
||||
// Battery
|
||||
"Level" = "잔량";
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"Network interface" = "Nettverksgrensesnitt";
|
||||
"Total download" = "Totalt lastet ned";
|
||||
"Total upload" = "Totalt lastet opp";
|
||||
"Reader type" = "Lesertype";
|
||||
"Interface" = "Grensesnitt";
|
||||
"Process" = "Prosess";
|
||||
|
||||
// Battery
|
||||
"Level" = "Nivå";
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"Network interface" = "Interfejs sieciowy";
|
||||
"Total download" = "Całkowicie pobrano";
|
||||
"Total upload" = "Całkowicie przesłano";
|
||||
"Reader type" = "Sposób odczytu";
|
||||
"Interface" = "Interfejs";
|
||||
"Process" = "Procesy";
|
||||
|
||||
// Battery
|
||||
"Level" = "Poziom naładowania";
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"Network interface" = "Interface de rede";
|
||||
"Total download" = "Download total";
|
||||
"Total upload" = "Upload total";
|
||||
"Reader type" = "Tipo de leitor";
|
||||
"Interface" = "Interface";
|
||||
"Process" = "Processo";
|
||||
|
||||
// Battery
|
||||
"Level" = "Nível";
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"Network interface" = "Сетевой интерфейс";
|
||||
"Total download" = "Всего скачано";
|
||||
"Total upload" = "Всего выгружено";
|
||||
"Reader type" = "Метод чтения";
|
||||
"Interface" = "Интерфейс";
|
||||
"Process" = "Процессы";
|
||||
|
||||
// Battery
|
||||
"Level" = "Уровень заряда";
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"Network interface" = "Ağ arayüzü";
|
||||
"Total download" = "Tamamen indirildi";
|
||||
"Total upload" = "Tamamen yüklendi";
|
||||
"Reader type" = "Okuma yöntemi";
|
||||
"Interface" = "Arayüz";
|
||||
"Process" = "Süreçler";
|
||||
|
||||
// Battery
|
||||
"Level" = "Doluluk";
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"Network interface" = "Мережевий інтерфейс";
|
||||
"Total download" = "Загально завантажено";
|
||||
"Total upload" = "Загально вислано";
|
||||
"Reader type" = "Метод читання";
|
||||
"Interface" = "Інтерфейс";
|
||||
"Process" = "Процеси";
|
||||
|
||||
// Battery
|
||||
"Level" = "Рівень заряду";
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"Network interface" = "Giao diện mạng";
|
||||
"Total download" = "Tổng tải";
|
||||
"Total upload" = "Tổng upload";
|
||||
"Reader type" = "Loại trình đọc";
|
||||
"Interface" = "Giao diện";
|
||||
"Process" = "Quá trình";
|
||||
|
||||
// Battery
|
||||
"Level" = "Dung lượng Pin";
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
"Network interface" = "网络接口";
|
||||
"Total download" = "共下载";
|
||||
"Total upload" = "总上传";
|
||||
"Reader type" = "读卡器类型";
|
||||
"Interface" = "接口";
|
||||
"Process" = "处理";
|
||||
|
||||
// Battery
|
||||
"Level" = "电量";
|
||||
|
||||
@@ -131,6 +131,9 @@
|
||||
"Network interface" = "網路介面";
|
||||
"Total download" = "共下載";
|
||||
"Total upload" = "共上傳";
|
||||
"Reader type" = "讀卡器類型";
|
||||
"Interface" = "接口";
|
||||
"Process" = "處理";
|
||||
|
||||
// Battery
|
||||
"Level" = "電量";
|
||||
|
||||
@@ -83,6 +83,12 @@ public let ShortLong: [KeyValue_t] = [
|
||||
public let ReaderUpdateIntervals: [Int] = [1, 2, 3, 5, 10, 15, 30]
|
||||
public let NumbersOfProcesses: [Int] = [3, 5, 8, 10, 15]
|
||||
|
||||
public typealias Bandwidth = (upload: Int64, download: Int64)
|
||||
public let NetworkReaders: [KeyValue_t] = [
|
||||
KeyValue_t(key: "interface", value: "Interface based"),
|
||||
KeyValue_t(key: "process", value: "Processes based"),
|
||||
]
|
||||
|
||||
public struct Units {
|
||||
public let bytes: Int64
|
||||
|
||||
|
||||
Reference in New Issue
Block a user