mirror of
https://github.com/Finb/Bark.git
synced 2025-12-08 21:36:01 +00:00
Compare commits
8 Commits
e29283113f
...
8b49d6cc96
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b49d6cc96 | ||
|
|
3513d2afb3 | ||
|
|
6a98d93031 | ||
|
|
11233dac73 | ||
|
|
c0dfb5d2b4 | ||
|
|
294f8224f0 | ||
|
|
3cf72b107e | ||
|
|
23cc968647 |
@ -104,80 +104,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
}
|
||||
|
||||
private func notificatonHandler(userInfo: [AnyHashable: Any]) {
|
||||
let viewController = Client.shared.currentSnackbarController
|
||||
func presentController() {
|
||||
let alert = (userInfo["aps"] as? [String: Any])?["alert"] as? [String: Any]
|
||||
let title = alert?["title"] as? String
|
||||
let subtitle = alert?["subtitle"] as? String
|
||||
let body = alert?["body"] as? String
|
||||
let url: URL? = {
|
||||
if let url = userInfo["url"] as? String {
|
||||
return URL(string: url)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
if let action = userInfo["action"] as? String, action == "none" {
|
||||
return
|
||||
}
|
||||
|
||||
// URL 直接打开
|
||||
if let url = url {
|
||||
Client.shared.openUrl(url: url)
|
||||
return
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("CopyContent"), style: .default, handler: { _ in
|
||||
if let copy = userInfo["copy"] as? String {
|
||||
UIPasteboard.general.string = copy
|
||||
} else {
|
||||
UIPasteboard.general.string = body
|
||||
}
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("MoreActions"), style: .default, handler: { _ in
|
||||
var shareContent = ""
|
||||
if let title = title {
|
||||
shareContent += "\(title)\n"
|
||||
}
|
||||
if let subtitle = subtitle {
|
||||
shareContent += "\(subtitle)\n"
|
||||
}
|
||||
if let body = body {
|
||||
shareContent += "\(body)\n"
|
||||
}
|
||||
for (key, value) in userInfo {
|
||||
if ["aps", "title", "subtitle", "body", "url"].contains((key as? String) ?? "") {
|
||||
continue
|
||||
}
|
||||
shareContent += "\(key): \(value) \n"
|
||||
}
|
||||
var items: [Any] = []
|
||||
items.append(shareContent)
|
||||
if let url = url {
|
||||
items.append(url)
|
||||
}
|
||||
let controller = Client.shared.window?.rootViewController
|
||||
let activityController = UIActivityViewController(activityItems: items,
|
||||
applicationActivities: nil)
|
||||
if let popover = activityController.popoverPresentationController {
|
||||
popover.sourceView = controller?.view
|
||||
popover.sourceRect = CGRect(x: controller?.view.bounds.midX ?? 0, y: controller?.view.bounds.midY ?? 0, width: 0, height: 0)
|
||||
}
|
||||
controller?.present(activityController, animated: true, completion: nil)
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: nil))
|
||||
|
||||
viewController?.present(alertController, animated: true, completion: nil)
|
||||
if let action = userInfo["action"] as? String, action == "none" {
|
||||
return
|
||||
}
|
||||
|
||||
if let presentedController = viewController?.presentedViewController {
|
||||
presentedController.dismiss(animated: false) {
|
||||
presentController()
|
||||
}
|
||||
} else {
|
||||
presentController()
|
||||
|
||||
// URL 直接打开
|
||||
if let url = try? (userInfo["url"] as? String)?.asURL() {
|
||||
Client.shared.openUrl(url: url)
|
||||
return
|
||||
}
|
||||
|
||||
alertNotification(userInfo: userInfo)
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
@ -219,3 +156,64 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate {
|
||||
func alertNotification(userInfo: [AnyHashable: Any]) {
|
||||
let alert = (userInfo["aps"] as? [String: Any])?["alert"] as? [String: Any]
|
||||
let title = alert?["title"] as? String
|
||||
let subtitle = alert?["subtitle"] as? String
|
||||
let body = alert?["body"] as? String
|
||||
let url = try? (userInfo["url"] as? String)?.asURL()
|
||||
|
||||
let alertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("CopyContent"), style: .default, handler: { _ in
|
||||
if let copy = userInfo["copy"] as? String {
|
||||
UIPasteboard.general.string = copy
|
||||
} else {
|
||||
UIPasteboard.general.string = body
|
||||
}
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("MoreActions"), style: .default, handler: { _ in
|
||||
var shareContent = ""
|
||||
if let title = title {
|
||||
shareContent += "\(title)\n"
|
||||
}
|
||||
if let subtitle = subtitle {
|
||||
shareContent += "\(subtitle)\n"
|
||||
}
|
||||
if let body = body {
|
||||
shareContent += "\(body)\n"
|
||||
}
|
||||
for (key, value) in userInfo {
|
||||
if ["aps", "title", "subtitle", "body", "url"].contains((key as? String) ?? "") {
|
||||
continue
|
||||
}
|
||||
shareContent += "\(key): \(value) \n"
|
||||
}
|
||||
var items: [Any] = []
|
||||
items.append(shareContent)
|
||||
if let url = url {
|
||||
items.append(url)
|
||||
}
|
||||
let controller = Client.shared.window?.rootViewController
|
||||
let activityController = UIActivityViewController(activityItems: items,
|
||||
applicationActivities: nil)
|
||||
if let popover = activityController.popoverPresentationController {
|
||||
popover.sourceView = controller?.view
|
||||
popover.sourceRect = CGRect(x: controller?.view.bounds.midX ?? 0, y: controller?.view.bounds.midY ?? 0, width: 0, height: 0)
|
||||
}
|
||||
controller?.present(activityController, animated: true, completion: nil)
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: nil))
|
||||
|
||||
let viewController = Client.shared.currentSnackbarController
|
||||
|
||||
if let presentedController = viewController?.presentedViewController {
|
||||
presentedController.dismiss(animated: false) {
|
||||
viewController?.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
} else {
|
||||
viewController?.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2605,19 +2605,19 @@
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Keep playing the ringtone for 30 seconds. "
|
||||
"value" : "Keep playing the ringtone for 30 seconds. With the level=critical parameter, it can ring continuously even in silent mode."
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Zili 30 saniye boyunca çalmaya devam ettir. "
|
||||
"value" : "Zil sesi 30 saniye boyunca kesintisiz çalar. level=critical parametresi ile sessiz modda bile sürekli çalabilir."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "持续播放铃声 30 秒。"
|
||||
"value" : "持续播放铃声 30 秒,配合 level=critical 参数可在静音模式下持续响铃。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3337,4 +3337,4 @@
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
}
|
||||
@ -51,7 +51,7 @@ class HomeViewController: BaseViewController<HomeViewModel> {
|
||||
|
||||
navigationItem.setBarButtonItems(items: [
|
||||
UIBarButtonItem(customView: newButton),
|
||||
UIBarButtonItem(customView: serversButton),
|
||||
UIBarButtonItem(customView: serversButton)
|
||||
], position: .right)
|
||||
|
||||
self.view.addSubview(self.tableView)
|
||||
@ -87,7 +87,7 @@ class HomeViewController: BaseViewController<HomeViewModel> {
|
||||
let startRequestAuthorization: () -> Observable<Bool> = {
|
||||
Single<Bool>.create { single -> Disposable in
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound, .badge, .criticalAlert], completionHandler: { (_ granted: Bool, _: Error?) -> Void in
|
||||
center.requestAuthorization(options: [.alert, .sound, .badge, .criticalAlert], completionHandler: { (_ granted: Bool, _: Error?) in
|
||||
single(.success(granted))
|
||||
})
|
||||
return Disposables.create()
|
||||
@ -166,6 +166,13 @@ class HomeViewController: BaseViewController<HomeViewModel> {
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
// 弹出服务器错误提示,引导用户跳转FAQ
|
||||
output.alertServerError
|
||||
.drive(onNext: { [weak self] error in
|
||||
self?.alertServerError(error: error)
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
// startButton是否可点击
|
||||
output.startButtonEnable
|
||||
.drive(self.startButton.rx.isEnabled)
|
||||
@ -198,11 +205,9 @@ class HomeViewController: BaseViewController<HomeViewModel> {
|
||||
var viewController: UIViewController?
|
||||
if let viewModel = viewModel as? NewServerViewModel {
|
||||
viewController = NewServerViewController(viewModel: viewModel)
|
||||
}
|
||||
else if let viewModel = viewModel as? SoundsViewModel {
|
||||
} else if let viewModel = viewModel as? SoundsViewModel {
|
||||
viewController = SoundsViewController(viewModel: viewModel)
|
||||
}
|
||||
else if let viewModel = viewModel as? CryptoSettingViewModel {
|
||||
} else if let viewModel = viewModel as? CryptoSettingViewModel {
|
||||
self.navigationController?.present(BarkNavigationController(rootViewController: CryptoSettingController(viewModel: viewModel)), animated: true)
|
||||
return
|
||||
}
|
||||
@ -219,4 +224,16 @@ class HomeViewController: BaseViewController<HomeViewModel> {
|
||||
self.navigationController?.present(controller, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func alertServerError(error: String) {
|
||||
let alertController = UIAlertController(title: NSLocalizedString("ServerError"), message: error, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("faq"), style: .default, handler: { [weak self] _ in
|
||||
guard let url = try? NSLocalizedString("faqUrl").asURL() else {
|
||||
return
|
||||
}
|
||||
self?.navigationController?.present(BarkSFSafariViewController(url: url), animated: true, completion: nil)
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: nil))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
let clienStateChanged: Driver<Client.ClienState>
|
||||
let tableViewHidden: Driver<Bool>
|
||||
let showSnackbar: Driver<String>
|
||||
let alertServerError: Driver<String>
|
||||
let startButtonEnable: Driver<Bool>
|
||||
let copy: Driver<String>
|
||||
let preview: Driver<URL>
|
||||
@ -119,6 +120,9 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
)
|
||||
]
|
||||
|
||||
/// 记录服务器错误的次数,如果错误次数大于2次,弹出提示引导用户查看FAQ。
|
||||
private var serverErrorCount = 0
|
||||
|
||||
func transform(input: Input) -> Output {
|
||||
let title = BehaviorRelay(value: ServerManager.shared.currentServer.host)
|
||||
|
||||
@ -166,6 +170,7 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
.asDriver(onErrorJustReturn: false)
|
||||
|
||||
let showSnackbar = PublishRelay<String>()
|
||||
let alertServerError = PublishRelay<String>()
|
||||
|
||||
// 点击注册按钮后,如果不允许推送,弹出提示
|
||||
tableViewHidden
|
||||
@ -187,11 +192,18 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
.map { _ in () }
|
||||
|
||||
// client state 变化时,发出相应错误提醒
|
||||
input.clientState.drive(onNext: { state in
|
||||
input.clientState.drive(onNext: { [weak self] state in
|
||||
guard let self else { return }
|
||||
|
||||
switch state {
|
||||
case .ok: break
|
||||
case .serverError(let error):
|
||||
showSnackbar.accept("\(NSLocalizedString("ServerError")): \(error.rawString())")
|
||||
if serverErrorCount < 2 {
|
||||
showSnackbar.accept("\(NSLocalizedString("ServerError")): \(error.rawString())")
|
||||
} else {
|
||||
alertServerError.accept(error.rawString())
|
||||
}
|
||||
serverErrorCount += 1
|
||||
default: break
|
||||
}
|
||||
// 主要用于 url scheme 添加服务器时会有state状态改变事件,顺便更新下标题
|
||||
@ -220,6 +232,7 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
clienStateChanged: clienState.asDriver(onErrorDriveWith: .empty()),
|
||||
tableViewHidden: tableViewHidden,
|
||||
showSnackbar: showSnackbar.asDriver(onErrorDriveWith: .empty()),
|
||||
alertServerError: alertServerError.asDriver(onErrorDriveWith: .empty()),
|
||||
startButtonEnable: Driver.just(true),
|
||||
copy: Driver.merge(sectionModel.items.map { $0.copy.asDriver(onErrorDriveWith: .empty()) }),
|
||||
preview: Driver.merge(sectionModel.items.map { $0.preview.asDriver(onErrorDriveWith: .empty()) }),
|
||||
|
||||
@ -91,7 +91,7 @@ class SoundsViewModel: ViewModel, ViewModelType {
|
||||
guard case SoundItem.sound(let model) = item else {
|
||||
return
|
||||
}
|
||||
self.dependencies.soundFileStorage.deleteSound(url: model.model.url)
|
||||
self.dependencies.soundFileStorage.deleteSound(name: model.model.url.lastPathComponent)
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
// 铃声列表有更新
|
||||
@ -169,7 +169,7 @@ class SoundsViewModel: ViewModel, ViewModelType {
|
||||
/// 保存铃声文件协议
|
||||
protocol SoundFileStorageProtocol {
|
||||
func saveSound(url: URL)
|
||||
func deleteSound(url: URL)
|
||||
func deleteSound(name: String)
|
||||
}
|
||||
|
||||
/// 用于将铃声文件保存在 /Library/Sounds 文件夹中
|
||||
@ -189,9 +189,16 @@ class SoundFileStorage: SoundFileStorageProtocol {
|
||||
try? fileManager.copyItem(at: url, to: soundUrl)
|
||||
}
|
||||
|
||||
func deleteSound(url: URL) {
|
||||
// 删除sounds目录铃声文件
|
||||
try? fileManager.removeItem(at: url)
|
||||
func deleteSound(name: String) {
|
||||
guard let soundsDirectoryUrl = getSoundsDirectory() else {
|
||||
return
|
||||
}
|
||||
let soundUrl = soundsDirectoryUrl.appendingPathComponent(name)
|
||||
let callSoundUrl = soundsDirectoryUrl.appendingPathComponent("\(kBarkSoundPrefix).\(name)")
|
||||
// 删除 sounds 目录铃声文件
|
||||
try? fileManager.removeItem(at: soundUrl)
|
||||
// 删除 call=1 生成的铃声文件
|
||||
try? fileManager.removeItem(at: callSoundUrl)
|
||||
}
|
||||
|
||||
/// 获取 Library 目录下的 Sounds 文件夹
|
||||
|
||||
@ -40,7 +40,7 @@ extension CallProcessor {
|
||||
}
|
||||
|
||||
if let longSoundUrl = getLongSound(soundName: soundName, soundType: soundType) {
|
||||
if let level = content.userInfo["level"] as? String, level == "critical" {
|
||||
if content.isCritical {
|
||||
LevelProcessor.setCriticalSound(content: content, soundName: longSoundUrl.lastPathComponent)
|
||||
} else {
|
||||
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: longSoundUrl.lastPathComponent))
|
||||
|
||||
@ -15,7 +15,7 @@ class LevelProcessor: NotificationContentProcessor {
|
||||
return bestAttemptContent
|
||||
}
|
||||
|
||||
if let level = bestAttemptContent.userInfo["level"] as? String, level == "critical" {
|
||||
if bestAttemptContent.isCritical {
|
||||
// 设置重要警告音效
|
||||
LevelProcessor.setCriticalSound(content: bestAttemptContent)
|
||||
return bestAttemptContent
|
||||
@ -39,7 +39,7 @@ class LevelProcessor: NotificationContentProcessor {
|
||||
|
||||
extension LevelProcessor {
|
||||
class func setCriticalSound(content bestAttemptContent: UNMutableNotificationContent, soundName: String? = nil) {
|
||||
guard let level = bestAttemptContent.userInfo["level"] as? String, level == "critical" else {
|
||||
guard bestAttemptContent.isCritical else {
|
||||
return
|
||||
}
|
||||
// 默认音量
|
||||
|
||||
@ -28,7 +28,6 @@ APP在维持期间,不会有任何形式的收费与广告,各位彦祖放
|
||||
- [常见问题](/faq)
|
||||
- **服务端**
|
||||
- [部署服务](/deploy)
|
||||
- [直接推送](/apns)
|
||||
- [批量推送](/batch)
|
||||
- [编译代码](/build)
|
||||
- [推送证书](/cert)
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
- [常见问题](/faq)
|
||||
- **服务端**
|
||||
- [部署服务](/deploy)
|
||||
- [直接推送](/apns)
|
||||
- [批量送送](/batch)
|
||||
- [编译代码](/build)
|
||||
- [推送证书](/cert)
|
||||
|
||||
55
docs/apns.md
55
docs/apns.md
@ -1,55 +0,0 @@
|
||||
### 直接调用APNS接口
|
||||
如果有设备的 DeviceToken(可在APP中查看),就可以调用苹果APNS接口直接给设备发推送,APP中也无需添加服务器。<br>
|
||||
以下是命令行发推送示例:
|
||||
|
||||
```shell
|
||||
# 设置环境变量
|
||||
# 下载 key https://raw.githubusercontent.com/Finb/bark-server/master/deploy/AuthKey_LH4T9V5U4R_5U8LBRXG3A.p8
|
||||
# 将 key 文件路径填到下面
|
||||
TOKEN_KEY_FILE_NAME=
|
||||
# 从 app 设置中复制 DeviceToken 到这
|
||||
DEVICE_TOKEN=
|
||||
|
||||
#下面的不要修改
|
||||
TEAM_ID=5U8LBRXG3A
|
||||
AUTH_KEY_ID=LH4T9V5U4R
|
||||
TOPIC=me.fin.bark
|
||||
APNS_HOST_NAME=api.push.apple.com
|
||||
|
||||
# 生成TOKEN
|
||||
JWT_ISSUE_TIME=$(date +%s)
|
||||
JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
|
||||
JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
|
||||
JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
|
||||
JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
|
||||
# 如果有条件,最好改进脚本缓存此 Token。Token 30分钟内复用同一个,每过30分钟重新生成
|
||||
# 苹果文档指明 TOKEN 生成间隔最短20分钟,TOKEN 有效期最长60分钟
|
||||
# 间隔过短重复生成会生成失败,TOKEN 超过1小时不重新生成就不能推送
|
||||
# 但经我不负责任的简单测试可以短时间内正常生成
|
||||
# 此处仅提醒,或许可能因频繁生成 TOKEN 导致推送失败
|
||||
AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"
|
||||
|
||||
#发送推送
|
||||
curl -v --header "apns-topic: $TOPIC" --header "apns-push-type: alert" --header "authorization: bearer $AUTHENTICATION_TOKEN" --data '{"aps":{"alert":"test"}}' --http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}
|
||||
|
||||
```
|
||||
|
||||
### 推送参数格式
|
||||
参考 https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification<br>
|
||||
一定要带上 "mutable-content" : 1 ,否则推送扩展不执行,不会保存推送。<br>
|
||||
|
||||
示例:
|
||||
```js
|
||||
{
|
||||
"aps": {
|
||||
"mutable-content": 1,
|
||||
"alert": {
|
||||
"title" : "title",
|
||||
"body": "body"
|
||||
},
|
||||
"category": "myNotificationCategory",
|
||||
"sound": "minuet.caf"
|
||||
},
|
||||
"icon": "https://day.app/assets/images/avatar.jpg"
|
||||
}
|
||||
```
|
||||
@ -28,7 +28,6 @@ Sponsors:[https://github.com/sponsors/Finb](https://github.com/sponsors/Finb)
|
||||
- [FAQs](/en-us/faq)
|
||||
- **Server**
|
||||
- [Deploy](/en-us/deploy)
|
||||
- [Direct Push](/en-us/apns)
|
||||
- [Build](/en-us/build)
|
||||
- [Certificate](/en-us/cert)
|
||||
- [Privacy](/en-us/privacy)
|
||||
@ -1,3 +1,12 @@
|
||||
#### 杭州移动无法访问 https://api.day.app
|
||||
杭州移动运营商阻断了请求(通常可能会持续一段时间,几天或几周都有可能),可以自部署服务端或暂时替换下域名使用 <br>
|
||||
https://api.day.app <br>
|
||||
替换为 <br>
|
||||
https://api.bbark.top <br>
|
||||
|
||||
这两个域名是同一台服务器,通常只需简单的在发送端修改下域名即可(key不用改), app 只是个接收端改不改无所谓。 <br>
|
||||
也可以在 APP 内新增 https://api.bbark.top 服务器长期使用。
|
||||
|
||||
#### 无法收到推送
|
||||
在 App 设置中检查 Device Token 是否正常。如果不正常,参考 [这里](#DeviceToken显示未知)<br/>
|
||||
如果正常,可以重启下设备,如果还不能接收到推送,检查推送请求返回状态码是否为 code 200。<br/>
|
||||
|
||||
@ -28,7 +28,6 @@ Sponsorlar:[https://github.com/sponsors/Finb](https://github.com/sponsors/Finb
|
||||
- [SSS](/tr/faq)
|
||||
- **Sunucu**
|
||||
- [Dağıtım](/tr/deploy)
|
||||
- [Doğrudan Gönderim](/tr/apns)
|
||||
- [Oluşturmak](/tr/build)
|
||||
- [Sertifika](/tr/cert)
|
||||
- [Gizlilik](/tr/privacy)
|
||||
@ -5,7 +5,6 @@
|
||||
- [SSS](/tr/faq)
|
||||
- **Sunucu**
|
||||
- [Dağıtım](/tr/deploy)
|
||||
- [Doğrudan Gönderim](/tr/apns)
|
||||
- [Oluşturmak](/tr/build)
|
||||
- [Sertifika](/tr/cert)
|
||||
- [Gizlilik](/tr/privacy)
|
||||
Loading…
x
Reference in New Issue
Block a user