mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-03 10:23:03 +08:00
WIP
This commit is contained in:
parent
d81da96c91
commit
d93b41d6bb
499
docs/src/content/docs/tutorials/02-sparkle-updates.mdx
Normal file
499
docs/src/content/docs/tutorials/02-sparkle-updates.mdx
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
---
|
||||||
|
title: Integrating Sparkle for Self-Updates
|
||||||
|
description: A tutorial on integrating Sparkle for automatic updates in macOS and Windows applications
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Tabs, TabItem, FileTree } from '@astrojs/starlight/components';
|
||||||
|
import {Image } from 'astro:assets';
|
||||||
|
|
||||||
|
import gatekeeper from "../../../assets/sparkle/gatekeeper.png";
|
||||||
|
import openprompt from "../../../assets/sparkle/openprompt.png";
|
||||||
|
import prefs from "../../../assets/sparkle/prefs.png";
|
||||||
|
import touchid from "../../../assets/sparkle/touchid.png";
|
||||||
|
import winsparkle from "../../../assets/sparkle/winsparkle-screenshot.png";
|
||||||
|
import updater3 from "../../../assets/sparkle/updater3.mp4";
|
||||||
|
|
||||||
|
|
||||||
|
[Sparkle](https://sparkle-project.org/) provides all the functionality needed for updating applications. It handles:
|
||||||
|
|
||||||
|
- Checking for updates (automatically or manually)
|
||||||
|
- Downloading updates
|
||||||
|
- Verifying update integrity with cryptographic signatures
|
||||||
|
- Installing updates with user consent
|
||||||
|
|
||||||
|
:::note
|
||||||
|
For this tutorial, an [Apple developer certificate](https://developer.apple.com/help/account/create-certificates/create-developer-id-certificates) is required.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Tutorial: Integrating Sparkle with Wails
|
||||||
|
|
||||||
|
This tutorial will walk you through the complete process of adding self-update capabilities to your Wails application using Sparkle. We'll cover:
|
||||||
|
|
||||||
|
1. Installing the Sparkle framework
|
||||||
|
2. Integrating with go-sparkle
|
||||||
|
3. Setting up a local development server for testing updates
|
||||||
|
4. Creating and signing update packages
|
||||||
|
5. Configuring the appcast.xml file
|
||||||
|
|
||||||
|
|
||||||
|
By the end of the tutorial, you'll know how to integrate Sparkle into your Wails application. Here is a demo:
|
||||||
|
|
||||||
|
<video src={updater3} controls></video>
|
||||||
|
|
||||||
|
|
||||||
|
### Step 1: Installing the Sparkle Framework
|
||||||
|
|
||||||
|
First, we need to download and integrate the Sparkle framework into our application bundle.
|
||||||
|
|
||||||
|
#### Download Sparkle Framework
|
||||||
|
|
||||||
|
1. Download the latest Sparkle package [from GitHub](https://github.com/sparkle-project/Sparkle/releases)
|
||||||
|
2. Extract the downloaded archive
|
||||||
|
3. For this tutorial, we're using [Sparkle 2.6.4](https://github.com/sparkle-project/Sparkle/releases/tag/2.6.4)
|
||||||
|
|
||||||
|
The extracted Sparkle package contains several important directories and files:
|
||||||
|
|
||||||
|
<FileTree>
|
||||||
|
- CHANGELOG
|
||||||
|
- INSTALL
|
||||||
|
- LICENSE
|
||||||
|
- SampleAppcast.xml
|
||||||
|
- Sparkle Test App.app
|
||||||
|
- Sparkle.framework/
|
||||||
|
- Autoupdate -> Versions/Current/Autoupdate
|
||||||
|
- Headers -> Versions/Current/Headers
|
||||||
|
- Modules -> Versions/Current/Modules
|
||||||
|
- PrivateHeaders -> Versions/Current/PrivateHeaders
|
||||||
|
- Resources -> Versions/Current/Resources
|
||||||
|
- Sparkle -> Versions/Current/Sparkle
|
||||||
|
- Updater.app -> Versions/Current/Updater.app
|
||||||
|
- Versions/
|
||||||
|
- XPCServices -> Versions/Current/XPCServices
|
||||||
|
- Symbols/
|
||||||
|
- Various .dSYM files
|
||||||
|
- bin/
|
||||||
|
- BinaryDelta
|
||||||
|
- generate_appcast
|
||||||
|
- generate_keys
|
||||||
|
- old_dsa_scripts
|
||||||
|
- sign_update
|
||||||
|
- sparkle.app
|
||||||
|
</FileTree>
|
||||||
|
|
||||||
|
#### Add Sparkle Framework to Your App
|
||||||
|
|
||||||
|
1. Create a `Frameworks` directory in your `build/darwin` folder:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mkdir -p build/darwin/Frameworks
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Copy the Sparkle framework to your `build/darwin` folder:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cp -R /path/to/Sparkle-2.6.4/Sparkle.framework build/darwin/Frameworks
|
||||||
|
```
|
||||||
|
|
||||||
|
:::note
|
||||||
|
The `Frameworks` directory is the standard location for frameworks within macOS applications. When examining existing applications (via right-click > Show Package Contents), you'll find frameworks in paths like `/Applications/AppName.app/Contents/Frameworks/`.
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### Update Build Configuration
|
||||||
|
|
||||||
|
Modify your `build/darwin/Taskfile.yml` to copy the Sparkle framework into your app bundle:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
create:app:bundle:
|
||||||
|
summary: Creates an `.app` bundle
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources,Frameworks}
|
||||||
|
- cp build/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
|
||||||
|
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
|
||||||
|
- cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
|
||||||
|
+ - cp -R build/darwin/Frameworks/Sparkle.framework {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Frameworks
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Set up CGO_LDFLAGS
|
||||||
|
|
||||||
|
Under the `env` block of the `build` task in `build/darwin/Taskfile.yml`, add:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
CGO_CFLAGS: -mmacosx-version-min=10.13
|
||||||
|
- CGO_LDFLAGS: -mmacosx-version-min=10.13
|
||||||
|
+ CGO_LDFLAGS: -mmacosx-version-min=10.13 -rpath @loader_path/../Frameworks
|
||||||
|
```
|
||||||
|
|
||||||
|
This path is relative to where our binary will be inside the app bundle:
|
||||||
|
|
||||||
|
<FileTree>
|
||||||
|
- YourApp.app/
|
||||||
|
- Contents/
|
||||||
|
- MacOS/
|
||||||
|
- YourApp
|
||||||
|
- Frameworks/
|
||||||
|
- Sparkle.framework/
|
||||||
|
</FileTree>
|
||||||
|
|
||||||
|
#### Update Info.plist
|
||||||
|
|
||||||
|
Add the following keys to your `build/darwin/Info.plist` file:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>SUFeedURL</key>
|
||||||
|
<string>https://localhost/appcast.xml</string>
|
||||||
|
<key>SUEnableAutomaticChecks</key>
|
||||||
|
<false/>
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll generate and add the public key in a later step.
|
||||||
|
|
||||||
|
#### Test Build
|
||||||
|
|
||||||
|
Now, we will test that building the app bundle works. To do this, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
wails3 package
|
||||||
|
```
|
||||||
|
|
||||||
|
If the build was successful, the app bundle should contain the Sparkle framework:
|
||||||
|
|
||||||
|
<FileTree>
|
||||||
|
- YourApp.app/
|
||||||
|
- Contents/
|
||||||
|
- MacOS/
|
||||||
|
- YourApp
|
||||||
|
- Frameworks/
|
||||||
|
- Sparkle.framework/
|
||||||
|
</FileTree>
|
||||||
|
|
||||||
|
You can check this by right clicking on the app bundle and selecting "Show Package Contents".
|
||||||
|
|
||||||
|
|
||||||
|
### Step 2: Integrating with go-sparkle
|
||||||
|
|
||||||
|
Now that we have the Sparkle framework installed, let's integrate it with our Go code using the [go-sparkle](https://github.com/abemedia/go-sparkle) package.
|
||||||
|
|
||||||
|
#### Install go-sparkle
|
||||||
|
|
||||||
|
Add the go-sparkle package to your project:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get github.com/abemedia/go-sparkle
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Basic Integration
|
||||||
|
|
||||||
|
In your `main.go` file, import `go-sparkle` with a leading underscore:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
_ "github.com/abemedia/go-sparkle"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Rebuild your Application
|
||||||
|
|
||||||
|
To rebuild your application, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
wails3 package
|
||||||
|
```
|
||||||
|
|
||||||
|
If all is successful, you should be able to run your application and get a popup dialog asking if you want to enable automatic updates.
|
||||||
|
|
||||||
|
TBD: PICTURE
|
||||||
|
|
||||||
|
|
||||||
|
### Step 3: Setting up a Local Development Server
|
||||||
|
|
||||||
|
To test the update mechanism, we need a server to host our update files. We'll use [Caddy](https://caddyserver.com/) for this purpose as it provides excellent HTTPS support.
|
||||||
|
|
||||||
|
#### Install Caddy
|
||||||
|
|
||||||
|
Install Caddy using Homebrew:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
brew install caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create Updates Directory
|
||||||
|
|
||||||
|
Create an `updates` directory to host your update files:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mkdir -p build/darwin/updates
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create a Caddyfile
|
||||||
|
|
||||||
|
Create a file named `Caddyfile` in your project directory with:
|
||||||
|
|
||||||
|
```
|
||||||
|
localhost {
|
||||||
|
root * ./build/darwin/updates
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create a test html file
|
||||||
|
|
||||||
|
Create an html file to test the server:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
echo "Hello World" > build/darwin/updates/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Start the Server
|
||||||
|
|
||||||
|
Start Caddy with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
caddy run
|
||||||
|
```
|
||||||
|
|
||||||
|
If you now browse to `https://localhost`, you should see the test HTML file.
|
||||||
|
|
||||||
|
### Step 4: Creating and Signing Updates
|
||||||
|
|
||||||
|
Sparkle uses cryptographic signatures to verify update integrity. Let's set up the necessary keys and learn how to sign updates.
|
||||||
|
|
||||||
|
#### Generate Sparkle Keys
|
||||||
|
|
||||||
|
Use the `generate_keys` tool from the Sparkle package:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
/path/to/Sparkle-2.6.4/bin/generate_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
When running this tool for the first time, you may encounter macOS Gatekeeper security prompts:
|
||||||
|
|
||||||
|
<Image src={gatekeeper} alt="Gatekeeper blocking dialog"/>
|
||||||
|
|
||||||
|
To proceed, open System Settings > Privacy & Security and scroll to the bottom:
|
||||||
|
|
||||||
|
:::note
|
||||||
|
The naming above is as of macOS Sequoia 15.3. Previous versions of macOS may differ.
|
||||||
|
:::
|
||||||
|
|
||||||
|
<Image src={prefs} alt="Privacy & Security settings"/>
|
||||||
|
|
||||||
|
Click "Allow Anyway" and then run the tool again. You'll see another prompt:
|
||||||
|
|
||||||
|
<Image src={openprompt} alt="Open prompt"/>
|
||||||
|
|
||||||
|
Click "Open Anyway" and authenticate:
|
||||||
|
|
||||||
|
<Image src={touchid} alt="Touch ID authentication"/>
|
||||||
|
|
||||||
|
After authentication, the tool will generate a key pair and output your public key:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./bin/generate_keys
|
||||||
|
Generating a new signing key. This may take a moment, depending on your machine.
|
||||||
|
A key has been generated and saved in your keychain. Add the `SUPublicEDKey` key to
|
||||||
|
the Info.plist of each app for which you intend to use Sparkle for distributing
|
||||||
|
updates. It should appear like this:
|
||||||
|
|
||||||
|
<key>SUPublicEDKey</key>
|
||||||
|
<string>wiI5O/SGcbX9VdcIN+hBXvV66KI3gpTTlHMelslKsg0=</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate two keys:
|
||||||
|
- A private key (kept secure in your keychain)
|
||||||
|
- A public key (add this to your Info.plist)
|
||||||
|
|
||||||
|
Update your `build/darwin/Info.plist` with the generated public key:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>SUPublicEDKey</key>
|
||||||
|
<string>GENERATED_PUBLIC_KEY</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create a Test Update
|
||||||
|
|
||||||
|
1. Firstly, rename your existing application to `oldversion.app`.
|
||||||
|
2. Make a change to your existing application. For our demo app, we'll update the version number text to 1.0.0.
|
||||||
|
3. Build the updated application using `wails3 package`
|
||||||
|
4. Code sign the updated application:
|
||||||
|
|
||||||
|
Check if you have a valid developer identity by running `security find-identity -v -p codesigning` which should show any valid identities in the format of Developer ID Application: Human Person (TEAMIDHERE)".
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```shell
|
||||||
|
CODE_SIGN_IDENTITY="Developer ID Application: Human Person (TEAMIDHERE)"
|
||||||
|
|
||||||
|
codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime bin/updater3.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc
|
||||||
|
|
||||||
|
# For Sparkle versions >= 2.6
|
||||||
|
codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime --preserve-metadata=entitlements bin/updater3.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc
|
||||||
|
|
||||||
|
# For Sparkle versions < 2.6
|
||||||
|
#codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime --entitlements Entitlements/Downloader.entitlements bin/updater3.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc
|
||||||
|
|
||||||
|
codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime bin/updater3.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate
|
||||||
|
codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime bin/updater3.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app
|
||||||
|
codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime bin/updater3.app/Contents/Frameworks/Sparkle.framework
|
||||||
|
```
|
||||||
|
5. Create a ZIP archive of the updated application:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ditto -c -k --keepParent "bin/YourApp.app" "build/darwin/updates/YourApp_1.0.0.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sign the Update
|
||||||
|
|
||||||
|
Sign the update using the `sign_update` tool:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
/path/to/Sparkle-2.6.4/bin/sign_update updates/YourApp_1.0.0.zip /path/to/your/private/key
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output a signature that you'll need for your appcast.xml file.
|
||||||
|
|
||||||
|
### Step 5: Creating the Appcast.xml File
|
||||||
|
|
||||||
|
The appcast.xml file is an RSS feed that tells Sparkle where to find updates and what versions are available.
|
||||||
|
|
||||||
|
Create an `appcast.xml` file in your `updates` directory:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<channel>
|
||||||
|
<title>YourApp Changelog</title>
|
||||||
|
<link>http://localhost:8000/appcast.xml</link>
|
||||||
|
<description>Most recent changes with links to updates.</description>
|
||||||
|
<language>en</language>
|
||||||
|
<item>
|
||||||
|
<title>Version 1.0.0</title>
|
||||||
|
<sparkle:version>1.0.0</sparkle:version>
|
||||||
|
<description>
|
||||||
|
<![CDATA[
|
||||||
|
<h2>New Features</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Feature 1</li>
|
||||||
|
<li>Feature 2</li>
|
||||||
|
</ul>
|
||||||
|
<h2>Bug Fixes</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Fix 1</li>
|
||||||
|
<li>Fix 2</li>
|
||||||
|
</ul>
|
||||||
|
]]>
|
||||||
|
</description>
|
||||||
|
<pubDate>Wed, 01 Mar 2023 12:00:00 +0000</pubDate>
|
||||||
|
<enclosure
|
||||||
|
url="http://localhost:8000/YourApp_1.0.0.zip"
|
||||||
|
sparkle:version="1.0.0"
|
||||||
|
length="12345678"
|
||||||
|
type="application/octet-stream"
|
||||||
|
sparkle:edSignature="YOUR_SIGNATURE_FROM_SIGN_UPDATE"
|
||||||
|
/>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `YOUR_SIGNATURE_FROM_SIGN_UPDATE` with the signature generated in the previous step.
|
||||||
|
|
||||||
|
#### Automating Appcast Generation
|
||||||
|
|
||||||
|
For convenience, you can use the `generate_appcast` tool to automatically generate the appcast.xml file:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
/path/to/Sparkle-2.6.4/bin/generate_appcast \
|
||||||
|
--download-url-prefix "http://localhost:8000" \
|
||||||
|
--private-key-path /path/to/your/private/key \
|
||||||
|
updates/
|
||||||
|
```
|
||||||
|
|
||||||
|
This will scan the updates directory and generate an appcast.xml file with the correct signatures.
|
||||||
|
|
||||||
|
### Step 6: Testing the Update Process
|
||||||
|
|
||||||
|
Now that everything is set up, let's test the update process:
|
||||||
|
|
||||||
|
1. Build and run your application with version 0.9.0
|
||||||
|
2. The app should check for updates and find version 1.0.0
|
||||||
|
3. Sparkle will prompt the user to update
|
||||||
|
4. After confirmation, Sparkle will download and install the update
|
||||||
|
|
||||||
|
## Additional Configuration
|
||||||
|
|
||||||
|
### Customising Update Checks
|
||||||
|
|
||||||
|
You can customise how Sparkle checks for updates by adding additional keys to your Info.plist:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Check for updates every 24 hours (86400 seconds) -->
|
||||||
|
<key>SUScheduledCheckInterval</key>
|
||||||
|
<integer>86400</integer>
|
||||||
|
|
||||||
|
<!-- Skip minor updates -->
|
||||||
|
<key>SUSkipMinorUpdates</key>
|
||||||
|
<true/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Manual Update Checks
|
||||||
|
|
||||||
|
To provide a manual update check option, add a function like this to your application service:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (a *App) CheckForUpdates() {
|
||||||
|
sparkle.CheckForUpdates()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, expose this function to your frontend:
|
||||||
|
|
||||||
|
```go
|
||||||
|
//go:embed all:frontend/dist
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create application with options
|
||||||
|
app := wails.NewApplication(wails.Options{
|
||||||
|
Assets: assets,
|
||||||
|
Bind: []interface{}{
|
||||||
|
&App{},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
err := app.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For a native macOS experience, add a menu item under `<App Name> -> Check for Updates` that calls this function.
|
||||||
|
|
||||||
|
### Windows Support with WinSparkle
|
||||||
|
|
||||||
|
For Windows applications, you can use WinSparkle, which provides similar functionality with an almost identical API. The go-sparkle package supports both Sparkle and WinSparkle, making it easy to support both platforms with minimal code changes.
|
||||||
|
|
||||||
|
<Image src={winsparkle} alt="WinSparkle"/>
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
You've now successfully integrated Sparkle into your Wails application, providing a robust self-update mechanism for your users. This approach gives you complete control over the update process whilst providing a native experience for your users.
|
||||||
|
|
||||||
|
For production use, you'll want to:
|
||||||
|
|
||||||
|
1. Host your updates on a secure server with HTTPS
|
||||||
|
2. Keep your private key secure
|
||||||
|
3. Consider using a CI/CD pipeline to automate the build, signing, and appcast generation process
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
:::note
|
||||||
|
This tutorial is based on [Marcus Crane's](https://github.com/marcus-crane) [Wails Sparkle guide](https://github.com/marcus-crane/wails3-sparkle-poc)
|
||||||
|
:::
|
||||||
|
|
||||||
|
- [Sparkle Documentation](https://sparkle-project.org/documentation/)
|
||||||
|
- [go-sparkle Repository](https://github.com/abemedia/go-sparkle)
|
||||||
|
- [WinSparkle Documentation](https://github.com/vslavik/winsparkle/wiki)
|
Loading…
Reference in New Issue
Block a user