Compare commits

..

2 Commits

Author SHA1 Message Date
Shadowfacts d661870401 Include log data in issue/crash reports 2022-10-09 14:26:44 -04:00
Shadowfacts afa1a733f4 Remove old XCB docs 2022-10-09 13:53:14 -04:00
3 changed files with 71 additions and 378 deletions

View File

@ -1,355 +0,0 @@
# X-Callback-URLs in Tusker
Tusker supports inter-app-communication using the [X-Callback-URL standard](http://x-callback-url.com/).
In short, requests are performed by opening the URL `tusker://x-callback-url/[request]` (where `[request]` is one of the requests listed below) with a variety of parameters.
## Callbacks
X-Callback-URLs support three types of callbacks: on success, on cancellation, and on error. Callbacks are specified as query parameters whose keys identify which callback (`x-success`, `x-cancel`, and `x-error`) and whose values are other URLs that should be opened to run the callback.
Data is passed to callbacks by adding additional query parameters to the callback URL. The `x-error` callback always returns a description of the error in the `error` parameter. Other data is provided depending on the request.
### JSON Responses
By default, callback data is included in URL query parameters of the callback URL. If the `json=true` parameter is provided, the response data will be encoded as JSON, converted to [Base64](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding), and provided in the `response` query parameter of the callback.
## Silent Requests
Tusker X-Callback-URL requests can be performed silently, without user confirmation. Each source app requires user permission on the first attempted silent action.
To perform a silent request:
1. Provide the `silent=true` URL query parameter in the request.
2. Specify the `x-source` parameter. It must be a (human interpretable) name of the source application/service. If `x-source` is not specified, the error callback will be invoked with the error message:
```
Cannot perform silent action without source app, x-source parameter must be specified.
```
3. Depending on the current permission state of the source app, one of several things will happen:
1. If the permission is **undecided** (i.e. the user has neither accepted nor rejected the silent action request), an alert will be displayed notifying the user that the source app has requested permission to silently perform actions. After the user either accepts or rejects the request, execution will continue with that permission state.
2. **Accepted**: the request will be carried out silently and the appropriate callback executed.
3. **Rejected**: the request will be performed with the confirmation UI, as if the `silent` parameter had been false/unprovided.
The silent actions permission state of a given source app is not exposed in the callback.
## Other Notes
#### Instance-Local IDs
Instance-local IDs are provided for many responses and accept in place of URLs/URIs/qualified names in many requests. When possible, instance-local IDs should be preferred requests using them can often be performed faster because there's no need to perform a search query or make requests to remote instances.
#### Qualified Usernames
Qualified username refers to the domain-qualified identifier of an account. For example, `shadowfacts@social.shadowfacts.net`. They do not include a leading `@`.
#### Dates
Dates in responses are encoded as Unix timestamps.
## Requests
- [Accounts](#accounts)
- [`showAccount`](#showaccount)
- [`getCurrentUser`](#getcurrentuser)
- [`getAccount`](#getaccount)
- [`followUser`](#followuser)
- [Statuses](#statuses)
- [`showStatus`](#showstatus)
- [`getStatus`](#getstatus)
- [`postStatus`](#poststatus)
- [`favoriteStatus`](#favoritestatus)
- [`reblogStatus`](#reblogstatus)
- [Notifications](#notifications)
- [`getNotification`](#getnotification)
- [`getNotifications`](#getnotifications)
- [`dismissNotification`](#dismissnotification)
- [`dismissAllNotifications`](#dismissallnotifications)
- [Instances](#instances)
- [`getCurrentInstance`](#getcurrentinstance)
- [Misc](#misc)
- [`search`](#search)
### Accounts
#### `showAccount`
Presents the given account in Tusker.
##### Request
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `accountID` (string) | The instance-local ID of the account | Yes |
| `accountURL` (URL) | The URL of the remote account | Yes |
| `acct` (string) | The [qualified username](#qualifiedusernames) of the account | Yes |
##### Response
No data if successful.
#### `getCurrentUser`
Retrieves the currently logged-in user.
##### Request
No parameters.
##### Response:
| Parameter (type) | Description | Optional |
| ---------------------- | --------------------------------------------- | -------- |
| `username` (string) | The [qualified username](#qualifiedusernames) | No |
| `displayName` (string) | The display name | No |
| `locked` (bool) | Whether the user's account is locked | No |
| `followers` (int) | The number of followers the user has | No |
| `following` (int) | The number of accounts user is following | No |
| `url` (URL) | The URL of the user's account | No |
| `avatarURL` (URL) | The URL of the user's avatar image | No |
| `headerURL` (URL) | The URL of the user's header image | No |
#### `getAccount`
Retrieves the given account details. One of `accountID`, `accountURL`, or `acct` must be provided.
##### Request
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `accountID` (string) | The instance-local ID of the account | Yes |
| `accountURL` (URL) | The URL/URI of the account | Yes |
| `acct` (string) | The [qualified username](#qualifiedusernames) of the account | Yes |
##### Response
| Parameter (type) | Description | Optional |
| ---------------------- | ------------------------------------------- | -------- |
| `username` (string) | The qualified username | No |
| `displayName` (string) | The display name | No |
| `locked` (bool) | Whether the account is locked | No |
| `followers` (int) | The number of followers the account has | No |
| `following` (int) | The number of accounts account is following | No |
| `url` (URL) | The URL of the account | No |
| `avatarURL` (URL) | The URL of the account's avatar image | No |
| `headerURL` (URL) | The URL of the account's header image | No |
#### `followUser`
Follows the given account from the logged-in user's account. One of `accountID`, `accountURL`, or `acct` must be provided.
##### Request
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `accountID` (string) | The instance-local ID of the account | Yes |
| `accountURL` (URL) | The URL/URI of the account | Yes |
| `acct` (string) | The [qualified username](#qualifiedusernames) of the account | Yes |
##### Response
| Parameter (type) | Description | Optional |
| ---------------- | ------------------------------- | -------- |
| `url` (URL) | The URL of the followed account | No |
### Statuses
#### `showStatus`
Presents the given status in Tusker.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ----------------------------------- | -------- |
| `statusID` (string) | The instance-local ID of the status | Yes |
| `statusURL` (URL) | The URL of a remote status | Yes |
##### Response
No data if successful.
#### `getStatus`
Retrieves the given status details. One of `statusID` or `statusURL` must be provided.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ------------------------------------------------------------ | -------- |
| `statusID` (string) | The instance-local ID of the status | Yes |
| `statusURL` (URL) | The URL/URI of the status | Yes |
| `html` (bool) | Whether to return the content as HTML or plain-text only. Default: `false` (plain-text). | Yes |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `url` (URL) | The URL of the status | Yes |
| `uri` (string) | The URI of the status | No |
| `id` (string) | The instance-local ID of the status | |
| `account` (string) | The [qualified username](#qualifiedusernames) of the account that posted (or reblogged if `reblog` is present) the status | No |
| `inReplyTo` (string) | The instance-local ID of the status that this status is a reply to | Yes |
| `posted` (date) | The date the status was posted | No |
| `content` (string) | The content of the status (HTML if the `html` parameter was true, plain-text otherwise) | No |
| `reblog` (string) | The **instance-local** ID of the status that this is a reblog of. If not present, this status was not a reblog. | Yes |
#### `postStatus`
Posts a status from the logged-in user's account.
Can be performed silently.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ------------------------------------------------------------ | -------- |
| `mentioning` (bool) | The [qualified username](#qualifiedusernames) to mention in the status | Yes |
| `text` (string) | The text to post/pre-fill the status text field with | Yes |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ---------------------------- | -------- |
| `statusURL` (URL) | The URL of the posted status | Yes |
| `statusURI` (string) | The URI of the posted status | No |
#### `favoriteStatus`
Favorites the given status from the logged-in user's account. One of `statusID` or `statusURL` must be provided.
Can be performed silently.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ----------------------------------- | -------- |
| `statusID` (string) | The instance-local ID of the status | Yes |
| `statusURL` (URL) | The URL/URI of a status | Yes |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------- | -------- |
| `statusURL` (URL) | The URL of the favorited status | Yes |
| `statusURI` (string) | The URI of the favorited status | No |
#### `reblogStatus`
Reblogs the given status from the logged-in user's account. One of `statusID` or `statusURL` must be provided.
Can be performed silently.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ----------------------------------- | -------- |
| `statusID` (string) | The instance-local ID of the status | Yes |
| `statusURL` (URL) | The URL/URI of a status | Yes |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------- | -------- |
| `statusURL` (URL) | The URL of the reblogged status | Yes |
| `statusURI` (string) | The URI of the reblogged status | No |
### Notifications
#### `getNotification`
Retrieves the given notification details.
##### Request
| Parameter (type) | Description | Optional |
| ------------------------- | ----------------------------------------- | -------- |
| `notificationID` (string) | The instance-local ID of the notification | No |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `kind` (string) | One of `mention`, `reblog`, `favourite`, or `follow` | No |
| `date` (date) | The date the notification was created. | No |
| `accountID` (string) | The instance-local ID of the account that sent the notification | No |
| `statusID` (string) | The instance-local ID of the status associated with the notification. Not applicable for `kind=follow`. | Yes |
#### `getNotifications`
Retrieves the most recent notifications.
##### Request
| Parameter (type) | Description | Optional |
| ---------------- | ---------------------------------------------------- | -------- |
| `count` (int) | The number of notifications to retrieve. Default: 20 | Yes |
##### Response
| Parameter (type) | Description | Optional |
| ------------------------ | ---------------------------------------------------------- | -------- |
| `notifications` (string) | A comma-delimited array of instance-local notification IDs | No |
#### `dismissNotification`
Dismisses the given notification.
##### Request
| Parameter (type) | Description | Optional |
| ----------------------- | ----------------------------------------- | -------- |
| `notification` (string) | The instance-local ID of the notification | No |
##### Response
No response data if successful.
#### `dismissAllNotifications`
Dismisses all notifications.
##### Request
No parameters.
##### Response
No data if successful.
### Instances
#### `getCurrentInstance`
Retrieves the current instance details.
##### Request
No parameters.
##### Response
| Parameter (type) | Description | Optional |
| ------------------------- | ------------------------------------------------------- | -------- |
| `uri` (string) | The instance URI | No |
| `name` (string) | The instance name | No |
| `description` (string) | The instance description | No |
| `contactAccount` (string) | The instance-local ID of the instance's contact account | No |
### Misc
#### `search`
Performs a search in Tusker with the given query
##### Request
| Parameter (type) | Description | Optional |
| ---------------- | ------------------------ |--------- |
| `query` (string) | The search query to use. | No |
##### Response
No data if successful.

View File

@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>OSLogPreferences</key>
<dict>
<key>$(PRODUCT_BUNDLE_IDENTIFIER)</key>
<dict>
<key>DEFAULT-OPTIONS</key>
<dict>
<key>Level</key>
<dict>
<key>Persist</key>
<string>Debug</string>
<key>Enable</key>
<string>Debug</string>
</dict>
</dict>
</dict>
</dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
@ -16,19 +32,6 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>net.shadowfacts.Tusker</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tusker</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSApplicationCategoryType</key>
@ -56,7 +59,7 @@
<key>NSMicrophoneUsageDescription</key>
<string>Post videos from the camera.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save photos directly from other people's posts.</string>
<string>Save photos directly from other people&apos;s posts.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Post photos from the photo library.</string>
<key>NSUserActivityTypes</key>

View File

@ -9,6 +9,7 @@
import UIKit
import CrashReporter
import MessageUI
import OSLog
class IssueReporterViewController: UIViewController {
@ -34,6 +35,8 @@ class IssueReporterViewController: UIViewController {
"Tusker Error Report"
}
private let logDataTask: Task<Data?, Never>
@IBOutlet weak var crashReportTextView: UITextView!
@IBOutlet weak var sendReportButton: UIButton!
@ -41,6 +44,15 @@ class IssueReporterViewController: UIViewController {
self.reportText = reportText
self.reportFilename = reportFilename
self.dismiss = dismiss
self.logDataTask = Task(priority: .userInitiated) {
return await withCheckedContinuation({ continuation in
DispatchQueue.global().async {
continuation.resume(returning: getLogData())
}
})
}
super.init(nibName: "IssueReporterViewController", bundle: .main)
}
@ -107,6 +119,7 @@ class IssueReporterViewController: UIViewController {
@IBAction func sendReportTouchUpInside(_ sender: Any) {
updateSendReportButtonColor(lightened: false, animate: true)
Task {
let composeVC = MFMailComposeViewController()
composeVC.mailComposeDelegate = self
composeVC.setToRecipients(["me@shadowfacts.net"])
@ -115,8 +128,14 @@ class IssueReporterViewController: UIViewController {
let data = reportText.data(using: .utf8)!
composeVC.addAttachmentData(data, mimeType: "text/plain", fileName: reportFilename)
if let logData = await logDataTask.value {
let timestamp = ISO8601DateFormatter().string(from: Date())
composeVC.addAttachmentData(logData, mimeType: "text/plain", fileName: "Tusker-\(timestamp).log")
}
self.present(composeVC, animated: true)
}
}
@objc func sendReportButtonLongPressed() {
let dir = FileManager.default.temporaryDirectory
@ -139,3 +158,29 @@ extension IssueReporterViewController: MFMailComposeViewControllerDelegate {
}
}
}
fileprivate func getLogData() -> Data? {
do {
let store = try OSLogStore(scope: .currentProcessIdentifier)
// past hour
let position = store.position(date: Date().addingTimeInterval(-60 * 60))
let entries = try store.getEntries(at: position, matching: NSPredicate(format: "subsystem = %@", Bundle.main.bundleIdentifier!))
var data = Data()
for entry in entries {
guard let entry = entry as? OSLogEntryLog else {
continue
}
data.append(contentsOf: entry.date.formatted(.iso8601).utf8)
data.append(32) // ' '
data.append(91) // '['
data.append(contentsOf: entry.category.utf8)
data.append(93) // ']'
data.append(32) // ' '
data.append(contentsOf: entry.composedMessage.utf8)
data.append(10) // '\n'
}
return data
} catch {
return nil
}
}