Skip to content

Matrix

Matrix is an open protocol allowing for end-to-end encryption, federation, and open freedom for communication. So you know I had to have one of my own. You'll find it at https://matrix.hylianux.com

You might be a little bit confused as to how to do everything, so I've made a quick-start guide for common things you may want to do.

Creating an account

I tried to make this as simple and intuitive as possible. You can choose either element or cinny, but I recommend doing it via the element side.

Where is element?

It pushes you right to the authentication server. Click sign up.

It's a small little link at the bottom

Once you type your information in, it'll submit you into my system for me to manually approve.

You're not getting in until I say so

Once I approve you, you can log in and use Matrix.

Where's the party at? How do I get to Vulgar Realities?

I'll be honest, this part's not the best user experience, but we're gonna go through it.

On your home screen, you'll see 3 green buttons. Ignore them entirely and click the plus sign on the left-side of the screen where the servers are, and click search for public spaces.

Oh of course, who wouldn't think to click this first?

And there's Vulgar Realities.

Amazing UX

Awww but where's all the good stuff in there? All I see is like "general".

Well of course. I keep shit locked down. Come find me and I'll pull you into the real inner sanctum.

Ok, what's this about "joining" a room? And once I've joined it, why do they all appear under my Vulgar Reality list?

Yup, bad user experience ahoy. Here's how you're "meant" to use it, and it'll make more sense if you follow this path: completely ignore the 2nd panel from the left. This panel is meant to show you all the rooms in the whole area. On the far-left panel, click "Vulgar Realities" (or the big blue V). You may have to click it twice, but once you do, it will show you all the chat rooms and inner spaces available. From here you can join a room, or even a whole "space" (Members Only, for example). Rooms and spaces in Matrix aren't like in Discord where everything is in drawers... they're more like "tags" that can be applied to multiple rooms. For example - I can make a room and apply it to 3 different spaces for different reasons... like memes.

It's all complicated, but all you need to understand is this simple message: click join on the rooms you wanna join. they'll all show up as one big-ass list in your 2nd-from-left panel. Click the whole space to see all the rooms. Savvy?

Where are the "room settings" at?

They bury them, I hate it. Bad UX. on both mobile and web/desktop element, you can click the "title" part of the room you're in (the bar in the middle at the top of the screen that displays the room's name), and a menu will slide out to the right where you can edit the room/space settings.

Dumb question: I thought matrix.hylianux.com was your server... why isn't Vulgar Realities just defaulted when I log in?

Ah, welcome to the world of self-hosted. Just because I'm hosting this myself doesn't mean your options are limited.

My server isn't a house - it's an undeveloped neighborhood with one street and one house in it already (Vulgar Realities). You can make your own houses and streets and build out this neighborhood.

Compared to Discord - Discord is the neighborhood. You either join a server, or make one yourself. But Discord controls it all. In Matrix - it's like having multiple instances of Discord, each with their own sets of servers. My server and their servers "federate" to each other, allowing cross-talk between them. Our communities each decide who we want in, and who we want out.

So, with that in mind, "Vulgar Realities" is a "space" within my Matrix server, just like it's a "server" inside Discord's overarching server. Just because you join Discord doesn't mean it'll default to Vulgar Realities, and Matrix is no different.

Enable Encryption Yo!

We'll be able to see what you write, but you won't be able to see what we write, unless you enable encryption in encrypted rooms. This is what Matrix was made for.

Click your username at the top-left of the screen, and go to all settings (not to be confused with the settings icon at the bottom-left of the screen. ugh.)

There'll be a red dot next to encryption, so click it, and "set up recovery". Follow the directions, and ‼️‼️‼️‼️SAVE THAT RECOVERY KEY OH MY GOD SAVE IT‼️‼️‼️‼️. If you don't save that key, you lose all chat history. Do not fuck around with this, save it someplace super safe.

Set up TOTP Authentication

Other names for this are 2fa (2-factor authentication, which is technically wrong), Multi-factor authentication (also technically wrong), and 2-step authentication (accurate).

Anyway, go to https://id.hylianux.com.

Log in with your credentials, assuming it doesn't just log you right in anyway.

At the top-right, you'll see a gear icon. Click it, and go to MFA Devices.

Click "Enroll >> TOTP Device" and that will grant you the ability to scan a qr code so you can log in with your authenticator app of choice.

How else can I connect to this? Any other apps?

Of course. You're likely connected via https://element.hylianux.com right?

FYI: the version of element I host is modified a little bit compared to the official version

This is a client called "element" and it's also got a desktop and mobile variant here: https://element.io/en/download.

As for you android kings, be sure to also check out this other Matrix client if you're dissatisfied with element X:

https://f-droid.org/en/packages/chat.schildi.android/

Other apps you can use to connect to Matrix

Element Desktop

When you first boot it up, click sign in, and it'll ask you what server you wanna connect to. By default, matrix.org is selected. Click edit, and type in "matrix.hylianux.com". Then connect as normal.

Cinny

This is a discord-like experience that runs in your browser only. https://cinny.hylianux.com

Element X and Schildi Chat Next

When it first boots up, it'll assume you want to connect to matrix.org. We don't want it to do that.

Choose "sign in manually". It'll say "hey you're about to sign into matrix.org" at the beginning. Do not click continue. Click 'change account provider'. Then click other, and type "matrix.hylianux.com". Select my server. NOW you can click continue.

Confirm your digital identity

Hey, this step's important, it'll happen no matter what thing you use. You have 2 options

  • Remember that big ol' recovery key I told you to hold on to tightly? You can copypaste that in there if you wanna.
  • Use another device <- This one's easier.

Choose use another device, and leave the app open here on your pc, whether it be through the website, or the desktop app. Click start verification. You'll get a popup on your other devices to "start verification". Click continue on both your phone and the pc, and verify the emojis are the same on both, keep clicking continue on both. Congratulations - you're in.

This also happens if you try to connect from another PC - you can use element or schildi next to verify you so you can log in on that other machine.

Technically there are 2 other android clients you could use, but they don't work with my server. They might work with older ones though.

So why would I use this over Discord?

This is perhaps the most important question to ask. Discord reads everything you write (even in DMs) and sells it to advertisers and more. You don't have privacy there.

Worse yet - they're beginning to implement ID verifications where they'll need pictures of your driver's license and some kind of photo verification of you personally.

"No big deal, I have nothing to hide." Yes you do - anyone who gets that information can create credit cards in your name. This is a huge fast-forward path for identity theft.

"Oh but surely they keep that information secure, right?" NO! There are data breaches ALL THE TIME that spill everyone's personal information out to the internet. Bad actors take it and masquerade as you while they do terrible things, or steal your money, or impose credit on you. Other companies watch what you're talking about and see if they can advertise a product to you that you might like. For example - if you're a depressed or traumatized 14-year-old girl, they might advertise beauty products and weight loss drugs to "make you prettier" because they know people with low self-esteem tend to buy stuff like this in the hopes that it'll make them happy. It's malevolent at worst, and rude/judgmental at best, and I'll have no part of it.

But Discord is so much easier...

Well then go use Cinny. It's a Matrix client designed to look almost exactly like Discord, and lucky for you I happen to host it right here. You're welcome.

Yeah but how am I supposed to use this while all my friends are still on Discord?

I have a solution to that problem too.

Use my self-hosted Discord Bridge

On the left side of the screen in element, click the "home" icon, then click the little pen-to-paper icon and select "start chat".

It's time to talk to a bot

In the search box, type @discordbot:hylianux.com and click Go.

Ah there it is

Create a new room with this bot (you may have to type a message like "hi") and it'll greet you with some basic instructions.

Enter login-qr and use your phone to log into Discord via the qr code.

The first thing it should do is generate a space for you with all your recent DM's. You'll have to click "accept" on them all, because this is how Matrix works.

Now you'll want to bring over the discord servers you'll actually talk on.

Enter guilds status and it'll give you a list of servers you're in, and assign them some long-ass string of numbers to identify them.

... And I'm going to warn you right now, this is the part where things get tricky, and stupid, and bad user experience I know it sucks.

First - figure out what order you'd like to view your servers in now, because you'll be stuck with it once you start adding them.

Next - enter guilds bridge thatLongStringOfNumbersForTheServerYouWant

Keep that number handy now, because you're about to use it again.

Enter guilds bridging-mode thatLongStringOfNumbersForTheServerYouWant everything

Almost done. Now enter disconnect, followed immediately by reconnect. I know, this is asinine, but if you don't do it this way, it'll get lost trying to put chat rooms inside their respective "category/spaces" where they belong, and will be orphaned rooms that are super hard to find.

NOW you can start clicking accept to join the rooms you want to chat in. Me, I just joined all of them, and then muted the ones I don't care about, but you do whatever you wanna do.

So what appearance settings should I set?

Element Desktop/Web

The settings gear icon is at the bottom-left. Click it, and open "All Settings". Then click Preferences. These are the settings I like to have on, which I think create the best user experience:

Preferences

  • Room list
    • Show Message Previews - ON
    • Spaces
      • Show all rooms in Home - ON
    • Presence
      • Send read receipts - ON
      • Send typing notifications - ON
    • Composer
      • Automatically replace plain text Emoji - OFF
      • Enable Markdown - ON
      • Enable Emoji suggestions while typing - ON
      • Use Ctrl+Enter to send a message - OFF
      • Surround selected text when typing special characters - OFF
      • Show stickers button - ON
      • Insert a trailing colon after user mentions at the start of a message - ON
    • Code Blocks
      • Enable automatic language detection for syntax highlighting - ON
      • Expand code blocks by default - OFF
      • Show line numbers in code blocks - ON
    • Link Previews
      • Enable previes - ON
      • Enable previews in encrypted rooms - ON
    • Images, GIFs and Videos
      • Autoplay GIFs - ON
      • Autoplay videos - OFF
    • Timeline
      • ... Turn all of them to ON
    • Moderation and safety
      • Show media in timeline - Always show
      • Allow users to invite you to rooms - ON
    • Room Directory
      • Show NSFW content - OFF

Encryption

  • In encrypted rooms, only send messages to verified users - OFF!

Labs

  • Join all ... both video rooms and notification settings

How do I send animated gifs?

Ah... well, if you're using mobile, you can probably use whatever your mobile device prefers as far as gif-pickers go. If you're using element desktop or web, then you'll have to do one little extra step.

Click to view the Linux script

#!/usr/bin/env bash
#
# update-gifproxy.sh
#
# Checks upstream for changes, replays the local m.image patch on top,
# rebuilds, restarts, and verifies. Rolls back the binary if the new one
# fails its health check.
#
# The local patch MUST be committed to git for this to work -- the rebase
# is what carries it forward. Run with no arguments to check and apply.
#
# Usage:
#   ./update-gifproxy.sh            check for updates, prompt, apply
#   ./update-gifproxy.sh --check    check only, report, change nothing
#   ./update-gifproxy.sh --force    rebuild and restart even if up to date

set -euo pipefail

REPO="/opt/giphytenormatrixproxy"
BUILD_SUBDIR="giphytenormatrixproxy"     # where go.mod lives
BINARY="${REPO}/gifproxy"
SERVICE="gifproxy"
PORT="8090"
GOPATH_BIN="/usr/local/go/bin"

# Strings that must be present in the source after a rebase. If any are
# missing, the local patch was lost and the build is aborted.
PATCH_MARKERS=(
  "notify_capabilities"
  "send_event"
  "sendStickerLegacy"
  "Math.round"
)

GREEN=$'\033[0;32m'; RED=$'\033[0;31m'; YELLOW=$'\033[0;33m'
BOLD=$'\033[1m'; RESET=$'\033[0m'

ok()   { printf '%s✓%s %s\n' "$GREEN" "$RESET" "$*"; }
warn() { printf '%s!%s %s\n' "$YELLOW" "$RESET" "$*" >&2; }
die()  { printf '%s✗%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; }

export PATH="${PATH}:${GOPATH_BIN}"

MODE="apply"
case "${1:-}" in
  --check) MODE="check" ;;
  --force) MODE="force" ;;
  "")      ;;
  *)       die "Unknown argument: $1 (use --check or --force)" ;;
esac

command -v git >/dev/null || die "git not found."
command -v go  >/dev/null || die "go not found. Is ${GOPATH_BIN} correct?"
[[ -d "$REPO/.git" ]] || die "$REPO is not a git checkout."

cd "$REPO"

# ---------------------------------------------------------------------------
# Refuse to run with uncommitted work -- the rebase would eat it
# ---------------------------------------------------------------------------

if [[ -n "$(git status --porcelain)" ]]; then
  warn "You have uncommitted changes in ${REPO}:"
  git status --short
  echo
  die "Commit them first:  git add -A && git commit -m 'local changes'"
fi

# ---------------------------------------------------------------------------
# Check upstream
# ---------------------------------------------------------------------------

echo "Fetching upstream ..."
git fetch --quiet origin

BRANCH="$(git rev-parse --abbrev-ref HEAD)"
UPSTREAM="origin/$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null | sed 's|^origin/||' || echo master)"

BEHIND="$(git rev-list --count "HEAD..${UPSTREAM}" 2>/dev/null || echo 0)"
AHEAD="$(git rev-list --count "${UPSTREAM}..HEAD" 2>/dev/null || echo 0)"

echo
echo "  branch:   ${BRANCH}"
echo "  upstream: ${UPSTREAM}"
echo "  local commits (your patch): ${AHEAD}"
echo "  new upstream commits:       ${BEHIND}"
echo

if [[ "$AHEAD" == "0" ]]; then
  warn "No local commits found. Your m.image patch may not be committed,"
  warn "which means an update would silently discard it."
fi

if [[ "$BEHIND" == "0" ]]; then
  ok "Already up to date."
  if [[ "$MODE" != "force" ]]; then
    exit 0
  fi
  echo "Continuing anyway (--force)."
else
  echo "${BOLD}New upstream commits:${RESET}"
  git --no-pager log --oneline --no-decorate "HEAD..${UPSTREAM}" | sed 's/^/  /'
  echo

  # Flag changes to the file carrying the local patch.
  if git diff --name-only "HEAD..${UPSTREAM}" | grep -q 'index\.html'; then
    warn "Upstream modified index.html -- the file your patch lives in."
    warn "A rebase conflict is likely. Resolve carefully."
    echo
  fi

  if [[ "$MODE" == "check" ]]; then
    echo "Check-only mode; nothing applied."
    exit 0
  fi

  read -rp "Apply these updates and rebuild? [y/N] " REPLY
  [[ "$REPLY" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
fi

# ---------------------------------------------------------------------------
# Rebase the local patch on top of upstream
# ---------------------------------------------------------------------------

PREV_HEAD="$(git rev-parse HEAD)"

if [[ "$BEHIND" != "0" ]]; then
  echo "Rebasing local patch onto ${UPSTREAM} ..."
  if ! git rebase "${UPSTREAM}"; then
    git rebase --abort || true
    echo
    die "Rebase conflict. Nothing changed. Resolve by hand:
       cd ${REPO}
       git rebase ${UPSTREAM}
       # fix conflicts in index.html, then:
       git add -A && git rebase --continue
     Then re-run this script with --force."
  fi
  ok "Rebase clean."
fi

# ---------------------------------------------------------------------------
# Confirm the patch survived
# ---------------------------------------------------------------------------

echo "Verifying local patch is still present ..."
MISSING=()
for marker in "${PATCH_MARKERS[@]}"; do
  if ! grep -rq "$marker" "${REPO}/${BUILD_SUBDIR}"/*.html 2>/dev/null; then
    MISSING+=("$marker")
  fi
done

if (( ${#MISSING[@]} > 0 )); then
  git reset --hard "$PREV_HEAD"
  die "Patch markers missing after rebase: ${MISSING[*]}
     Rolled back to ${PREV_HEAD}. Nothing was rebuilt."
fi
ok "All patch markers present."

# ---------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------

BACKUP="${BINARY}.prev"
[[ -f "$BINARY" ]] && cp -f "$BINARY" "$BACKUP"

echo "Building ..."
cd "${REPO}/${BUILD_SUBDIR}"
if ! go build -o "${BINARY}.new" . ; then
  cd "$REPO"
  git reset --hard "$PREV_HEAD"
  die "Build failed. Source rolled back; running binary untouched."
fi
mv -f "${BINARY}.new" "$BINARY"
ok "Built."

# ---------------------------------------------------------------------------
# Restart and health check
# ---------------------------------------------------------------------------

echo "Restarting ${SERVICE} ..."
systemctl restart "$SERVICE"
sleep 2

HEALTHY=true

if ! systemctl is-active --quiet "$SERVICE"; then
  HEALTHY=false
  warn "Service is not active."
fi

if ! curl -sf "localhost:${PORT}/_matrix/key/v2/server" >/dev/null; then
  HEALTHY=false
  warn "Federation key endpoint did not respond."
fi

# The served HTML is embedded in the binary, so this proves the rebuild
# actually picked up the patched source.
if ! curl -sf "localhost:${PORT}/" | grep -q "notify_capabilities"; then
  HEALTHY=false
  warn "Served HTML does not contain the patch. Build did not embed it."
fi

if [[ "$HEALTHY" != true ]]; then
  echo
  warn "Health check failed. Rolling back."
  if [[ -f "$BACKUP" ]]; then
    mv -f "$BACKUP" "$BINARY"
    systemctl restart "$SERVICE"
    warn "Previous binary restored. Source is at the new commit --"
    warn "run 'git reset --hard ${PREV_HEAD}' in ${REPO} to revert that too."
  fi
  journalctl -u "$SERVICE" -n 30 --no-pager
  exit 1
fi

rm -f "$BACKUP"

ok "Service healthy on port ${PORT}."
echo
echo "${BOLD}Remaining manual step:${RESET} purge the Cloudflare cache for"
echo "gifs.hylianux.com, then hard-reload Element (Ctrl-Shift-R)."
echo

Click to view the Windows script

<#
.SYNOPSIS
    Registers the GIF picker widget on a Matrix account.

.DESCRIPTION
    Writes the `m.widgets` account data event via the Matrix client-server API.
    This is per-user account data stored on the homeserver, so it follows the
    user to every device and every Element instance they log into. It only
    needs to be run once per person.

.PARAMETER Remove
    Unregisters the picker instead of installing it.

.EXAMPLE
    .\set-gif-picker.ps1

.EXAMPLE
    .\set-gif-picker.ps1 -Remove

.NOTES
    Works on Windows PowerShell 5.1 and PowerShell 7+.
    If Windows blocks the script, run it as:
        powershell -ExecutionPolicy Bypass -File .\set-gif-picker.ps1
#>

[CmdletBinding()]
param(
    [switch]$Remove
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

# ---------------------------------------------------------------------------
# Site configuration -- edit these to match your deployment
# ---------------------------------------------------------------------------

$Homeserver = 'https://matrix.hylianux.com'   # client-server API base URL
$ServerName = 'hylianux.com'                  # the domain in your MXIDs
$WidgetName = 'GIFs'                          # label shown in Element

# NOTE: single quotes are required. $theme is a literal placeholder that
# Element substitutes at load time -- it must NOT be expanded by PowerShell.
$PickerUrl = 'https://gifs.hylianux.com/?theme=$theme'

# ---------------------------------------------------------------------------

function Write-Ok    { param($m) Write-Host "[OK] $m"   -ForegroundColor Green }
function Write-Warn  { param($m) Write-Host "[!]  $m"   -ForegroundColor Yellow }
function Stop-Fail   { param($m) Write-Host "[X]  $m"   -ForegroundColor Red; exit 1 }

# Windows PowerShell 5.1 defaults to TLS 1.0 in some configurations.
if ($PSVersionTable.PSVersion.Major -lt 6) {
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}

# ---------------------------------------------------------------------------
# Gather credentials
# ---------------------------------------------------------------------------

Write-Host ''
Write-Host 'Matrix GIF picker setup' -ForegroundColor White
Write-Host ''
Write-Host 'This registers the GIF picker widget on your Matrix account.'
Write-Host 'You will need an access token.'
Write-Host ''
Write-Host 'Where to find your access token in Element:' -ForegroundColor White
Write-Host ''
Write-Host '  Element Web / Desktop'
Write-Host '    Settings -> Help & About -> Advanced -> Access Token'
Write-Host '    (click "<click to reveal>", then copy it)'
Write-Host ''
Write-Host '  Element mobile'
Write-Host '    Settings -> Help & About -> Advanced -> Access Token'
Write-Host ''
Write-Host 'Treat this token like a password.' -ForegroundColor Yellow
Write-Host 'It grants full access to your account. Close this window when you'
Write-Host 'are done, and never paste it into a chat or an issue tracker.'
Write-Host ''

$Username = Read-Host "Username (just the local part, e.g. 'alice')"
$Username = $Username.TrimStart('@')          # tolerate a leading @
$Username = $Username.Split(':')[0]           # tolerate a pasted full MXID
if ([string]::IsNullOrWhiteSpace($Username)) { Stop-Fail 'No username entered.' }

$SecureToken = Read-Host 'Access token' -AsSecureString
if ($PSVersionTable.PSVersion.Major -ge 7) {
    $Token = ConvertFrom-SecureString $SecureToken -AsPlainText
} else {
    $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureToken)
    try   { $Token = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) }
    finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
}
if ([string]::IsNullOrWhiteSpace($Token)) { Stop-Fail 'No token entered.' }

$Mxid    = "@$Username`:$ServerName"
$Headers = @{ Authorization = "Bearer $Token" }

function Get-Endpoint {
    param([string]$Id)
    "$Homeserver/_matrix/client/v3/user/$([uri]::EscapeDataString($Id))/account_data/m.widgets"
}

# ---------------------------------------------------------------------------
# Verify the token actually belongs to this account
# ---------------------------------------------------------------------------

Write-Host ''
Write-Host "Verifying token against $Homeserver ..."

try {
    $whoami = Invoke-RestMethod -Method Get -Headers $Headers `
        -Uri "$Homeserver/_matrix/client/v3/account/whoami"
} catch {
    $detail = $null
    if ($_.ErrorDetails) { $detail = $_.ErrorDetails.Message }
    if ($detail -and $detail -match 'M_UNKNOWN_TOKEN') {
        Stop-Fail 'Homeserver rejected the token. It may be expired -- grab a fresh one from Element.'
    }
    Stop-Fail "Could not reach the homeserver. Check `$Homeserver in this script. ($($_.Exception.Message))"
}

$ActualMxid = $whoami.user_id

if ($ActualMxid -ne $Mxid) {
    Write-Warn "Token belongs to $ActualMxid, but you entered $Mxid."
    Write-Warn "A token can only write its own account data, so $Mxid would fail."
    $reply = Read-Host "Use $ActualMxid instead? [y/N]"
    if ($reply -notmatch '^[Yy]$') { Stop-Fail 'Aborted. Re-run with the correct username.' }
    $Mxid = $ActualMxid
}

Write-Ok "Authenticated as $Mxid"

$Endpoint = Get-Endpoint $Mxid

# ---------------------------------------------------------------------------
# Build the payload
# ---------------------------------------------------------------------------

if ($Remove) {
    $Payload = '{}'
    $Action  = 'Removing'
} else {
    # Built as an object and serialized, so quoting and escaping are handled
    # correctly. $PickerUrl is single-quoted above, so $theme stays literal.
    $Payload = [ordered]@{
        stickerpicker = [ordered]@{
            content = [ordered]@{
                type          = 'm.stickerpicker'
                url           = $PickerUrl
                name          = $WidgetName
                creatorUserId = $Mxid
                data          = @{}
            }
            sender    = $Mxid
            state_key = 'stickerpicker'
            type      = 'm.widget'
            id        = 'stickerpicker'
        }
    } | ConvertTo-Json -Depth 6 -Compress
    $Action = 'Registering'
}

Write-Host "$Action the picker for $Mxid ..."

try {
    Invoke-RestMethod -Method Put -Headers $Headers -Uri $Endpoint `
        -ContentType 'application/json; charset=utf-8' -Body $Payload | Out-Null
} catch {
    $detail = $_.Exception.Message
    if ($_.ErrorDetails) { $detail = $_.ErrorDetails.Message }
    Write-Host ''
    Stop-Fail "Homeserver returned an error: $detail"
}

Write-Ok 'Account data written.'

# ---------------------------------------------------------------------------
# Read it back
# ---------------------------------------------------------------------------

try {
    $verify = Invoke-RestMethod -Method Get -Headers $Headers -Uri $Endpoint
} catch {
    $verify = $null
}

if ($Remove) {
    Write-Ok 'Picker removed.'
} elseif ($verify -and ($verify | ConvertTo-Json -Depth 6 -Compress) -match 'm\.stickerpicker') {
    Write-Ok 'Verified: the widget is present on the account.'
} else {
    Write-Warn 'Write succeeded but read-back looks wrong.'
}

Write-Host ''
Write-Host 'Next step:' -ForegroundColor White -NoNewline
Write-Host ' hard-reload Element (Ctrl-Shift-R, or fully quit and reopen'
Write-Host "the desktop/mobile app). A $WidgetName button should appear in the message composer."
Write-Host ''
Write-Host 'If the button does not appear, the account data is set correctly but' -ForegroundColor DarkGray
Write-Host 'Element has cached the old state -- log out and back in as a last resort.' -ForegroundColor DarkGray
Write-Host 'To undo this entirely, re-run with:  .\set-gif-picker.ps1 -Remove' -ForegroundColor DarkGray
Write-Host ''

Remove-Variable Token, SecureToken, Headers -ErrorAction SilentlyContinue