Compare commits

...

59 Commits

Author SHA1 Message Date
Florian Metz
664e5c3e73 chore: release v1.1.3 2024-02-28 14:04:01 +01:00
Florian Metz
34bf4daa6a chore: improve memory usage 2024-02-28 14:03:42 +01:00
Florian Metz
b2f4051e11 chore: release v1.1.2 2024-02-28 12:06:47 +01:00
Florian Metz
03cc674601 feat: add ratelimit environment variables 2024-02-28 12:06:25 +01:00
Florian Metz
da3d007a35 chore: release v1.1.1 2024-02-28 08:23:07 +01:00
Florian Metz
80d9888b37 chore: pd stuff 2024-02-28 08:19:34 +01:00
Florian Metz
3ae265d6fe chore: release v1.1.0 2024-02-27 16:52:15 +01:00
Florian Metz
3c9e7708f8 feat: cache ratelimit in redis 2024-02-27 16:51:57 +01:00
Florian Metz
7663eacb58 chore: release v1.0.1 2024-02-27 16:33:42 +01:00
Florian Metz
85d5fa14fa chore: update readme 2024-02-27 16:33:03 +01:00
Florian Metz
2b55d34c19 chore: release v1.0.1 2024-02-27 16:32:19 +01:00
Florian Metz
9d1c2167be chore: release v1.0.1 2024-02-27 16:25:37 +01:00
Florian Metz
ce52ce9a97 fix: redis url passing 2024-02-27 16:25:11 +01:00
Florian Metz
d73db7e5a9 chore: update funding 2024-02-26 08:29:44 +01:00
Florian Metz
98ce2ff939 chore: readme stuff 2024-02-26 06:34:16 +01:00
Florian Metz
f98d615e7b fix: ci 2024-02-26 06:18:39 +01:00
Florian Metz
9c461c83ff chore: release v1.0.0 2024-02-26 06:16:43 +01:00
Florian Metz
dc8dacd5f5 chore: add release instructions 2024-02-26 06:16:31 +01:00
Florian Metz
db47ae9043 chore: release v1.0.0 2024-02-26 06:16:13 +01:00
Florian Metz
a0f4e141b5 chore: release v0.0.6 2024-02-26 06:12:05 +01:00
Florian Metz
5bc4c0e5c0 chore: testing 2024-02-26 06:11:39 +01:00
Florian Metz
c4765b8eeb chore: testing 2024-02-26 06:07:19 +01:00
Florian Metz
ebc953b549 chore: testing 2024-02-26 06:05:02 +01:00
Florian Metz
b407003450 chore: testing 2024-02-26 06:03:28 +01:00
Florian Metz
3857702040 chore: release v0.0.5 2024-02-26 06:02:21 +01:00
Florian Metz
e997e946a0 chore: testing 2024-02-26 06:02:11 +01:00
Florian Metz
2bcb87ec9c chore: release v0.0.4 2024-02-26 05:53:40 +01:00
Florian Metz
1d5af93390 chore: testing 2024-02-26 05:53:30 +01:00
Florian Metz
8a0e10eab0 chore: release v0.0.3 2024-02-26 05:50:59 +01:00
Florian Metz
13e413329c chore: testing 2024-02-26 05:50:46 +01:00
Florian Metz
d0fac8c495 chore: release v0.0.2 2024-02-26 05:41:01 +01:00
Florian Metz
74525de586 fix: token 2024-02-26 05:39:13 +01:00
Florian Metz
115923b935 fix: add login 2024-02-26 05:36:29 +01:00
Florian Metz
b7af6bb80e chore: release v0.0.2 2024-02-26 05:32:27 +01:00
Florian Metz
b053e6395c chore: release v0.0.2 2024-02-26 05:31:33 +01:00
Florian Metz
698ea34848 chore: testing 2024-02-26 05:31:06 +01:00
Florian Metz
d689324e62 chore: release v0.0.2 2024-02-26 05:27:59 +01:00
Florian Metz
fee8133965 chore: release v0.0.2 2024-02-26 05:24:03 +01:00
Florian Metz
b93db93236 chore: testing 2024-02-26 05:23:31 +01:00
Florian Metz
1efa4b9bb0 feat: add 1.10 schema 2024-02-26 04:59:32 +01:00
Florian Metz
4cf00f5ca9 fix: post-checkout 2024-02-26 04:59:32 +01:00
Florian Metz
94303a9bca chore: rename container 2024-02-16 08:53:44 +00:00
Florian Metz
fa611923b4 chore: change post-checkout 2024-02-16 08:45:28 +00:00
Florian Metz
c57d4db5a3 chore: devcontainer 2024-02-16 08:16:06 +01:00
Florian Metz
5d7760caa3 test: give up on index 2024-02-13 01:09:48 +01:00
Florian Metz
0d5fe8e5ab test: mock better 2024-02-13 01:00:43 +01:00
Florian Metz
1ac552f3cd chore: tests 2024-02-13 00:51:36 +01:00
Florian Metz
4cf4e01286 chore: reusable action 2024-02-13 00:18:55 +01:00
Florian Metz
e96e2c0cb6 ci: fix docker build 2024-02-13 00:03:58 +01:00
Florian Metz
8fb93a09f9 ci: add docker build 2024-02-12 23:45:50 +01:00
Florian Metz
4d20ba95b1 chore: update schemas 2024-02-12 23:45:32 +01:00
Florian Metz
85f428a295 chore: update gitignore 2024-02-12 21:59:49 +01:00
Florian Metz
a3018f528d chore: schemas 2024-02-12 21:59:31 +01:00
Florian Metz
3d8431c4d5 chore: tests 2024-02-12 21:01:24 +01:00
Florian Metz
074d55b6d4 chore: update readme 2024-02-11 04:02:27 +01:00
Florian Metz
d22d88b3a6 wip: docs 2024-02-11 04:01:28 +01:00
Florian Metz
f55a83977b wip: pd 2024-02-10 05:12:56 +01:00
Florian Metz
4ccc02f16c chore: commit-msg hook 2024-02-08 23:51:30 +01:00
Florian Metz
759b2abef9 wip: repo refactor 2024-02-08 23:42:28 +01:00
143 changed files with 9687 additions and 5217 deletions

1
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1 @@
FROM mcr.microsoft.com/devcontainers/base:bullseye

View File

@@ -0,0 +1,24 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
{
"name": "PreMiD",
"dockerComposeFile": ["docker-compose.yml"],
"service": "app",
"workspaceFolder": "/workspaces",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "lts",
"nvmVersion": "latest"
},
"ghcr.io/joshuanianji/devcontainer-features/mount-pnpm-store:1": {},
"ghcr.io/dhoeric/features/act:1": {}
},
"overrideFeatureInstallOrder": ["ghcr.io/devcontainers/features/node:1", "ghcr.io/joshuanianji/devcontainer-features/mount-pnpm-store:1"],
"postCreateCommand": "pnpm i --frozen-lockfile",
"customizations": {
"vscode": {
"extensions": ["Gruntfuggly.todo-tree", "YoavBls.pretty-ts-errors", "EditorConfig.EditorConfig", "DeepScan.vscode-deepscan", "esbenp.prettier-vscode"]
}
},
"shutdownAction": "stopCompose"
}

View File

@@ -0,0 +1,32 @@
version: "3.8"
services:
# Update this to the name of the service you want to work with in your docker-compose.yml file
app:
build:
context: .
dockerfile: Dockerfile
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
# array). The sample below assumes your primary file is in the root of your project.
#
# build:
# context: .
# dockerfile: .devcontainer/Dockerfile
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
- ..:/workspaces:cached
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
# cap_add:
# - SYS_PTRACE
# security_opt:
# - seccomp:unconfined
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "while sleep 1000; do :; done"
redis:
image: redis
ports:
- "6379:6379"

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
.vscode
.DS_Store
.Trashes
.nuxt
dist
node_modules
.env

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
dist
node_modules
coverage

12
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
extends: ["@recodive/eslint-config"],
overrides: [
{
extends: ["plugin:@typescript-eslint/disable-type-checked"],
files: ["./**/*.{cjs,js,jsx}"],
},
],
parserOptions: {
project: "./tsconfig.json",
},
};

View File

@@ -1,30 +0,0 @@
# Contributing
## Requiered knowledge
- JavaScript
- html5
- NodeJS
Additional:
- CSS
- [VueJS](https://vuejs.org/)
- [ElectronJS](https://electronjs.org/)
- [NPMjs](https://www.npmjs.com/)
A source code editor is also requiered. We recommend [Visual Studio Code](https://code.visualstudio.com/).
### Installing the components
1. Install [Git](https://git-scm.com/)
2. Install [Node](https://nodejs.org/en/)
### Cloning the project
1. Fork the [repository](https://github.com/PreMiD/PreMiD)
2. Open a terminal and type `git clone https://github.com/PreMiD/PreMiD`
### Coding your vision
Please keep the structure. We don't want to disorganize our project. Chaotic files may not be accepted.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 KiB

2
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,3 @@
github: Timeraa
github: [PreMiD, Timeraa]
patreon: Timeraa
ko_fi: Timeraa

View File

@@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
---
<!-- NOTE -->
<!-- Keep all presence related bugs in our Presences repository! -->
<!-- https://github.com/PreMiD/Presences/issues -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1.
2.
3.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,16 +0,0 @@
---
name: Feature request
about: Suggest an idea for PreMiD
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

BIN
.github/Logo.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
.github/Patreon.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

1
.github/PayPal.svg vendored

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.1 KiB

13
.github/SUPPORT.md vendored
View File

@@ -1,13 +0,0 @@
# How to get support
## Take a look at the [wiki](https://wiki.premid.app)
Our GitHub wiki is full of information around PreMiD.<br>
Take a look and feel free to contribute if you want to add something new.
## [Open a issue](https://github.com/PreMiD/PreMiD/issues/new/choose) on [GitHub](https://github.com/PreMiD/PreMiD)
Simply open a issue if you don't feel allright.<br>
*Aand there he goes...*
## Ask a staff member in [#support](https://discord.premid.app)
The team is ready to tell you the secrets of the underworld.<br>
Join our [Discord server](https://discord.premid.app) and find out what we're hiding.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,36 @@
name: "Build and Push Docker Image"
description: "Builds a Docker image and pushes it to GitHub Container Registry"
inputs:
app:
description: "Name of the app"
required: true
token:
description: "GitHub token"
required: true
outputs:
version:
description: "Version of the app"
value: ${{ steps.get_version.outputs.version }}
runs:
using: "composite"
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get package.json version
id: get_version
run: echo ::set-output name=version::$(node -p "require('./apps/${{ inputs.app }}/package.json').version")
shell: bash
- name: Convert repository owner to lowercase
id: repo
run: echo "::set-output name=lowercase::$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')"
shell: bash
- name: Build and Push Docker Image
uses: premid/premid/.github/actions/build-docker@monorepo
with:
dockerfile: ./apps/${{ inputs.app }}/Dockerfile
push: true
token: ${{ inputs.token }}
tags: ghcr.io/${{ steps.repo.outputs.lowercase }}/${{ inputs.app }}:${{ steps.get_version.outputs.version }},ghcr.io/${{ steps.repo.outputs.lowercase }}/${{ inputs.app }}:latest

View File

@@ -0,0 +1,46 @@
name: "Build Docker Image"
description: "Builds a Docker image using Docker Buildx"
inputs:
dockerfile:
description: "Path to the Dockerfile"
required: true
tags:
description: "Comma-separated list of tags for the Docker image"
required: true
push:
description: "Whether to push the Docker image to the registry"
required: false
default: "false"
token:
description: "GitHub Token"
required: false
runs:
using: "composite"
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
if: ${{ inputs.push == 'true' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ inputs.token }}
- name: Build Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ${{ inputs.dockerfile }}
platforms: linux/amd64,linux/arm64
push: ${{ inputs.push }}
tags: ${{ inputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,20 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: chore
include: scope
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: chore
include: scope
labels:
- "dependencies"

BIN
.github/example.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

5
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>Recodive/Recodive:renovate-config"],
"automerge": false
}

17
.github/workflows/cd-pd.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Build pd
on:
push:
tags:
- "pd-v*"
permissions:
packages: write
jobs:
build:
name: Build pd
runs-on: ubuntu-latest
steps:
- name: Build and Push Docker Image
uses: premid/premid/.github/actions/build-and-push-docker@monorepo
with:
app: pd
token: ${{ secrets.GITHUB_TOKEN }}

17
.github/workflows/cd-schemas.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Build schema-server
on:
push:
tags:
- "schema-server-v*"
permissions:
packages: write
jobs:
build:
name: Build schema-server
runs-on: ubuntu-latest
steps:
- name: Build and Push Docker Image
uses: premid/premid/.github/actions/build-and-push-docker@monorepo
with:
app: schema-server
token: ${{ secrets.GITHUB_TOKEN }}

44
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Build, Lint and Test
on: [push, pull_request]
jobs:
build:
name: Build, Lint and Test
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
- name: Setup Node
uses: actions/setup-node@v4
with:
cache: pnpm
node-version-file: package.json
- name: Install Dependencies
run: pnpm install
- name: Lint
run: pnpm run lint
- name: Build
run: pnpm run build
- name: Test
run: pnpm test
build-docker:
name: Build Docker Images
runs-on: ubuntu-latest
strategy:
matrix:
project: [pd, schema-server]
fail-fast: false
steps:
- name: Build Docker Image
uses: premid/premid/.github/actions/build-docker@monorepo
with:
dockerfile: ./apps/pd/Dockerfile
tags: ${{ matrix.project }}

View File

@@ -1,98 +0,0 @@
name: DePloY
on:
release:
types: [published]
env:
NODE_ENV: DePloY
jobs:
package:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macOS-latest, windows-latest]
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
- name: Install Dependencies
run: |
npm i
npm i -g typescript rimraf
- name: Prepare to package
run: npm run init
- name: Package
run: |
npm run pkg
rimraf dist/app
node util/zip dist ${{ matrix.os }}.zip --zip
- name: Upload bundle
env:
SSHHOST: ${{ secrets.MAIN_HOST }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_KEY: ${{ secrets.SSH_KEY }}
run: |
tsc util/uploadFile util/zip
node util/uploadFile ${{ matrix.os }}.zip /home/PreMiD/download/util/${{ matrix.os }}.zip
createInstallers:
runs-on: "ubuntu-latest"
needs: package
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
- name: Install dependencies
run: |
sudo npm i
sudo npm i -g typescript
- name: Download InstallBuilder
run: |
wget https://clients.bitrock.com/installbuilder/installbuilder-enterprise-20.12.0-linux-x64-installer.run
chmod u+x installbuilder-enterprise-20.12.0-linux-x64-installer.run
- name: Install InstallBuilder
run: |
./installbuilder-enterprise-20.12.0-linux-x64-installer.run --installer-language en --prefix ./installbuilder --mode unattended
echo "${{ secrets.IBLICENSE }}" > ./installbuilder/license.xml
- name: Prepare Upgrade Installer
run: |
tsc util/prepare
node util/prepare
- name: Create Upgrade Installer (MacOS 64bit)
run: |
installbuilder/bin/builder build installer_assets/PreMiD-Upgrade.xml osx
- name: Create Upgrade Installer (Windows)
run: |
installbuilder/bin/builder build installer_assets/PreMiD-Upgrade.xml windows
- name: Upload files
env:
SSHHOST: ${{ secrets.MAIN_HOST }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_KEY: ${{ secrets.SSH_KEY }}
run: |
tsc util/uploadFile util/zip
node util/uploadFile dist/installer/upgrader.exe /home/PreMiD/download/upgrader.exe
node util/uploadFile dist/installer/upgrader.app.zip /home/PreMiD/download/util/upgrader.app.zip
- name: Finalize build
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.MAIN_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /home/PreMiD/download/util
unzip upgrader.app.zip
tar -czvf upgrader.app.tgz upgrader.app
mv upgrader.app.tgz ../
rm -rf upgrader.app upgrader.app.zip
unzip windows-latest.zip
cd windows-latest/PreMiD-win32-x64/
zip -r ../../PreMiD-win32-x64.zip .
mv ../../PreMiD-win32-x64.zip /home/PreMiD/download/
cd ../PreMiD-win32-ia32/
zip -r ../../PreMiD-win32-x86.zip .
mv ../../PreMiD-win32-x86.zip /home/PreMiD/download/
cd ../..
rm -rf windows-latest windows-latest.zip
unzip macOS-latest.zip
cd macOS-latest/PreMiD-darwin-x64/
zip -r ../../PreMiD-darwin-x64.zip .
mv ../../PreMiD-darwin-x64.zip /home/PreMiD/download/
cd ../..
rm -rf macOS-latest macOS-latest.zip

3
.gitignore vendored
View File

@@ -18,3 +18,6 @@ src/update.ini
*.app
*.xml.backup
*.js
coverage
*.tsbuildinfo

2
.husky/commit-msg Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
pnpm exec commitlint --edit $1

2
.husky/post-checkout Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
if ! diff --quiet HEAD@{1} HEAD -- package.json; then pnpm i --frozen-lockfile; fi

2
.husky/pre-commit Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
pnpm run lint

2
.husky/pre-push Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
pnpm run test

8
.prettierignore Normal file
View File

@@ -0,0 +1,8 @@
.pnpm-store
*.js
*.cjs
*.ts
*.vue
pnpm-lock.yaml
cache

1
.prettierrc.cjs Normal file
View File

@@ -0,0 +1 @@
module.exports = require("@recodive/configs").default.prettier;

View File

@@ -1,22 +0,0 @@
export default interface ExtensionSettings {
/**
* If extension is enabled
*/
enabled: boolean;
/**
* Autolaunch enabled
*/
autoLaunch: boolean;
/**
* Media keys enabled
*/
mediaKeys: boolean;
/**
* title menubar (TrayTitle)
*/
titleMenubar: boolean;
/**
* language of extension
*/
language: string;
}

View File

@@ -1,16 +0,0 @@
import * as Discord from "discord-rpc";
export default interface Presence {
/**
* Client ID of presence
*/
clientId: string;
/**
* Rich Procedual call connection
*/
rpc: Discord.Client;
/**
* Connection ready?
*/
ready: Boolean;
}

View File

@@ -1,33 +0,0 @@
import * as Discord from "discord-rpc";
export default interface PresenceData {
/**
* Client ID of presence
*/
clientId: string;
/**
* Tray title to be shown in Mac OS tray
*/
trayTitle: string;
/**
* service name of presence
* @deprecated
*/
service: string;
/**
* Determines if the service is currently playing something back or not, if false it will automatically hide after 1 minute
*/
playback: boolean;
/**
* Discord Presence which gets sent directly to Discord app
*/
presenceData: Discord.Presence;
/**
* Determines if the service should be hidden (clearActivity)
*/
hidden: boolean;
/**
* Determines if the service is mediaKey able / uses them
*/
mediaKeys: boolean;
}

View File

@@ -1 +1 @@
* @Timeraa
* @Timeraa

View File

@@ -2,65 +2,38 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at contact@premid.app or by contacting a staff member on our [Discord server](https://discord.premid.app). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@premid.app or by contacting a staff member on our [Discord server](https://discord.premid.app). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

View File

@@ -1,46 +1,50 @@
<div align="center">
<img src=".github/Logo.png" width="150px" draggable="false"><br>
<img src="https://cdn.rcd.gg/PreMiD.png" width="150px" />
# PreMiD
## Your Rich Presence for web services!
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/PreMiD/PreMiD)
![GitHub](https://img.shields.io/github/license/PreMiD/PreMiD?style=for-the-badge)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/premid/premid?label=Application&style=for-the-badge)
![Chrome Web Store](https://img.shields.io/chrome-web-store/v/agjnjboanicjcpenljmaaigopkgdnihi?label=Extension&style=for-the-badge)
[![Chrome Web Store](https://img.shields.io/chrome-web-store/d/agjnjboanicjcpenljmaaigopkgdnihi.svg?label=Chrome&logo=google%20chrome&logoColor=white&colorA=4285F4&style=for-the-badge)](https://chrome.google.com/webstore/detail/premid/agjnjboanicjcpenljmaaigopkgdnihi)
![Website](https://img.shields.io/website?down_message=offline&label=PreMiD.app&style=for-the-badge&up_message=online&url=https%3A%2F%2Fpremid.app)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FPreMiD%2FPreMiD.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FPreMiD%2FPreMiD?ref=badge_shield)
This is the monorepo for PreMiD. PreMiD is a simple, configurable utility that allows you to show what you're watching/listening to on your Discord profile.
<img src=".github/example.png" draggable="false"><br>
## Getting Started
# About
**If you are a user looking to install PreMiD, please visit the [official website](https://premid.app).**
**PreMiD** is a simple, configurable utility that allows you to show what you're doing on the web in your Discord **now playing status**. It supports many different websites, and will support multiple users watching the same content simultaneously in an upcoming update.
If you are a developer looking to contribute to PreMiD, read along.
# Features
## Table of Contents
· Displays your current web service in Discord as your status.<br>
· Grants full control over Presences.<br>
· Supports over 1,000 web services, still rising!<br>
· _Watch parties and more are coming soon!_
- [Packages](#packages)
- [License](#license)
# Installation/Troubleshooting
## Packages
### Installation instructions, Troubleshooting guides etc. can be located at our [**docs**](https://docs.premid.app).
This monorepo is split into multiple packages / projects. Here's a list of them:
# Support us
- [apps/docs](apps/docs) - The official documentation for PreMiD.
- [apps/pd](apps/pd/README.md) - A simple url shortener service to shorten urls longer than 256 characters.
- [apps/schema-server](apps/schema-server) - Simple Schema server for the Presence manifest.
<div>
<a target="_blank" href="https://www.patreon.com/bePatron?u=4610890" data-patreon-widget-type="become-patron-button" title="Support me on Patreon!">
<img height="75px" draggable="false" src=".github/Patreon.png">
</a>
<a target="_blank" href="https://discord.premid.app/" title="Join our Discord!">
<img src="https://discordapp.com/api/guilds/493130730549805057/widget.png?style=banner2" height="76px" draggable="false" alt="Join our Discord!">
</a>
</div>
## Development
### Release
To release a new version of a package, run the following command:
```bash
cd apps/<app>
pnpm bumpp -y -t <app>-v
```
Replace `<app>` with the name of the package you want to release. For example, to release a new version of the `schema-server` package, you would run:
```bash
cd apps/schema-server
pnpm bumpp -y -t schema-server-v
```
This will use bumpp to bump the version of the package in the `package.json` file, create a tag for the new version, and push the changes to the remote repository.
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FPreMiD%2FPreMiD.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FPreMiD%2FPreMiD?ref=badge_large)
This project is licensed under the [MPL-2.0 License](LICENSE).

1
apps/docs/.vitepress/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
cache

View File

@@ -0,0 +1,129 @@
import { defineConfig } from "vitepress";
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Documentation",
description: "Official Documentation",
locales: {
root: {
label: "English",
lang: "en-US",
},
de: {
label: "Deutsch",
lang: "de-DE",
},
},
themeConfig: {
nav: [
{
text: "Presence Development",
link: "/presence-development/",
},
{
text: "Reference",
link: "/reference/presence",
},
],
sidebar: {
"/default": {
base: "/",
items: [
{
text: "Getting Started",
link: "/",
items: [
{
text: "Introduction",
link: "/introduction/",
},
{
text: "Installation",
link: "/installation/",
},
{
text: "Setup",
link: "/setup/",
},
{
text: "FAQ",
link: "/faq/",
},
],
},
{
text: "Development",
link: "/development/",
items: [
{
text: "Presence Development",
link: "/presence-development/",
collapsed: true,
items: [
{
text: "Getting Started",
link: "/presence-development/getting-started/",
},
{
text: "Creating a Presence",
link: "/presence-development/creating-a-presence/",
},
],
},
],
},
{
text: "Contribute",
link: "/contribute/",
items: [
{
text: "Report a Bug",
link: "https://github.com/PreMiD",
},
{
text: "Submit a Feature",
link: "https://discord.premid.app",
},
{
text: "Donate",
link: "https://patreon.com/Timeraa",
},
{
text: "Translate",
link: "https://crowdin.com/project/premid",
},
],
},
],
},
"/reference/": {
base: "/reference",
items: [
{
text: "Reference",
link: "/presence",
items: [
{
text: "Presence",
link: "/presence",
},
{
text: "Iframe",
link: "/iframe",
},
],
},
],
},
},
socialLinks: [
{ icon: "github", link: "https://github.com/PreMiD" },
{ icon: "discord", link: "https://discord.premid.app" },
{ icon: "x", link: "https://x.com/PreMiDapp" },
],
i18nRouting: true,
logo: "/logo.svg",
search: { provider: "local" },
},
lastUpdated: true,
});

View File

@@ -0,0 +1,5 @@
# Getting Started
Welcome to the official documentation for PreMiD! This guide will help you get started with PreMiD.
## Installation

51
apps/docs/index.md Normal file
View File

@@ -0,0 +1,51 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "PreMiD"
text: "Documentation"
tagline: "The official documentation for PreMiD."
image:
src: /logo.svg
alt: PreMiD Logo
actions:
- theme: brand
text: Get Started
link: /getting-started
- theme: alt
text: Presence Development
link: /presence-development
features:
- icon: 🛠️
title: Extensible
details: Add Presences for your favorite websites and services. Or create your own!
- icon: 🌐
title: Cross-Platform
details: PreMiD is available for all major browsers and platforms.
- icon: 🚀
title: Lightweight
details: PreMiD is designed to be as lightweight as possible, so it won't slow down your system.
---
<style>
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, rgb(209, 122, 254) 30%, rgb(89, 195, 246));
--vp-home-hero-image-background-image: linear-gradient(-45deg, rgb(209, 122, 254) 50%, rgb(89, 195, 246) 50%);
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
</style>

14
apps/docs/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "@premid/docs",
"version": "0.0.0",
"description": "Documentation for Premid",
"private": true,
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "1.0.0-rc.42"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 288 KiB

View File

@@ -0,0 +1,68 @@
# Presence Class
The `Presence` class is the main class used to create a Presence.
## Overview
The `Presence` class is the main class used to create a Presence. It is used to interact with the PreMiD Extension.
### Example
```javascript
const presence = new Presence({
clientId: "<Your Client ID>",
});
presence.on("UpdateData", () => {
// Logic to update the presence data
presence.setActivity({
details: "Example Presence",
state: "Example State",
});
});
```
## Constructor
### `new Presence(options: PresenceOptions)`
Creates a new `Presence` instance.
#### Parameters
- `options` (`PresenceOptions`): The options for the Presence.
#### Returns
- `Presence`: The new `Presence` instance.
## Properties
### `clientId: string`
The Client ID of the Presence.
## Methods
### `setActivity(activity: PresenceActivity)`
Sets the activity of the Presence.
#### Parameters
- `activity` (`PresenceActivity`): The activity to set.
### `clearActivity()`
Clears the activity of the Presence.
### `on(event: string, listener: Function)`
Adds a listener to an event.
#### Parameters
- `event` (`string`): The event to listen to.
- `listener` (`Function`): The listener to add.

26
apps/pd/Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM node:20-alpine as build
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml /app/
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch --frozen-lockfile
COPY . /app
RUN pnpm install --frozen-lockfile --offline
RUN pnpm --filter @premid/pd build && \
pnpm deploy --filter @premid/pd deploy
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/deploy /app
USER node
CMD ["npm", "run", "start"]

3
apps/pd/README.md Normal file
View File

@@ -0,0 +1,3 @@
# @premid/pd
A simple url shortener service to shorten urls longer than 256 characters.

12
apps/pd/environment.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/* eslint-disable unicorn/prevent-abbreviations */
declare namespace NodeJS {
export interface ProcessEnv {
NODE_ENV?: "development" | "production" | "test";
REDIS_URL?: string;
MAX_FILE_SIZE?: string;
PORT?: string;
HOST?: string;
RATELIMIT_MAX?: string;
RATELIMIT_WINDOW?: string;
}
}

BIN
apps/pd/fixtures/1x1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

BIN
apps/pd/fixtures/test.mp4 Normal file

Binary file not shown.

35
apps/pd/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "@premid/pd",
"version": "1.1.3",
"description": "A small service to shorten image urls to get around Discord's 256 character limit",
"main": "dist/index.js",
"type": "module",
"files": [
"dist"
],
"scripts": {
"start": "node --enable-source-maps .",
"dev": "node --watch --enable-source-maps .",
"build": "tsc -p tsconfig.app.json"
},
"private": true,
"license": "MPL-2.0",
"dependencies": {
"@fastify/cors": "^9.0.1",
"@fastify/multipart": "^8.1.0",
"@fastify/rate-limit": "^9.1.0",
"@keyv/redis": "^2.8.4",
"fastify": "^4.26.0",
"file-type": "^19.0.0",
"got": "^14.2.0",
"ioredis": "^5.3.2",
"ipaddr.js": "^2.1.0",
"keyv": "^4.5.4",
"mime-types": "^2.1.35",
"nanoid": "^5.0.5"
},
"devDependencies": {
"@types/mime-types": "^2.1.4",
"form-data": "^4.0.0"
}
}

View File

@@ -0,0 +1,9 @@
import { expect, test } from "vitest";
import createKeyv from "./createKeyv.js";
test("should return keyv instance", async () => {
const keyv = await createKeyv();
expect(keyv).toStrictEqual(expect.any(Object));
});

View File

@@ -0,0 +1,27 @@
import KeyvRedis from "@keyv/redis";
import Keyv from "keyv";
import redis from "../redis.js";
export default async function createKeyv() {
let options: Keyv.Options<string> | undefined;
/* c8 ignore next 8 */
if (process.env.REDIS_URL) {
options = {
namespace: "pd",
store: new KeyvRedis(redis),
};
}
const keyv = new Keyv<string>(
options,
);
/* c8 ignore next 3 */
keyv.on("error", (error) => {
console.error("Keyv connection error:", error);
});
return keyv;
}

View File

@@ -0,0 +1,22 @@
import { hostname } from "node:os";
import { Redis } from "ioredis";
/* c8 ignore start */
export default function createRedis(): Redis {
const redis = new Redis(process.env.REDIS_URL ?? "redis://127.0.0.1:6379", {
name: `pd-${hostname()}`,
});
/* c8 ignore next 3 */
redis.on("error", (error) => {
console.error("Redis error", error);
});
/* c8 ignore next 3 */
redis.on("connect", () => {
console.log("Redis connected");
});
return redis;
}

View File

@@ -0,0 +1,10 @@
import { describe, expect, it } from "vitest";
import { createServer } from "../functions/createServer.js";
describe("createServer", () => {
it("should return a fastify instance", async () => {
const server = await createServer();
expect(server).toBeDefined();
});
});

View File

@@ -0,0 +1,61 @@
import cors from "@fastify/cors";
import fastifyMultipart from "@fastify/multipart";
import ratelimit from "@fastify/rate-limit";
import fastify from "fastify";
import { Redis } from "ioredis";
import createFromBase64 from "../routes/createFromBase64.js";
import createFromImage from "../routes/createFromImage.js";
import createShortenedLink from "../routes/createShortenedLink.js";
import getFullLink from "../routes/getFullLink.js";
export async function createServer(redis?: Redis) {
const server = fastify({
trustProxy: true,
});
await server.register(cors, {
methods: ["GET"],
origin: "*",
});
await server.register(ratelimit, {
max: Number.parseInt(process.env.RATELIMIT_MAX ?? "25"),
nameSpace: "pd-ratelimit-",
redis,
timeWindow: process.env.RATELIMIT_WINDOW ?? "1 minute",
});
await server.register(fastifyMultipart, {
limits: {
fileSize: Number.parseInt(process.env.MAX_FILE_SIZE ?? (5 * 1024 * 1024).toString()),
files: 1,
},
});
server.post("/create/image", createFromImage);
server.post("/create/base64", createFromBase64);
server.get("/create/*", createShortenedLink);
server.get(
"/*",
{
config: {
rateLimit: false,
},
},
getFullLink,
);
server.get(
"/health",
{
config: {
rateLimit: false,
},
},
(_, reply) => reply.status(204).send(),
);
return server;
}

View File

@@ -0,0 +1,30 @@
import got from "got";
import { describe, expect, it, vi } from "vitest";
describe.concurrent("getGoogleAddresses", async () => {
it("should return an array of CIDR objects", async () => {
const { default: getGoogleAddresses } = await import("./getGoogleAddresses.js");
vi.spyOn(got, "get").mockResolvedValue({
body: JSON.stringify({
prefixes: [
{ ipv4Prefix: "0.0.0.0" },
{ ipv6Prefix: "0000:0000:0000::/16" },
],
}),
});
const result = await getGoogleAddresses();
expect(result).toMatchInlineSnapshot(`
[
{
"ipv4Prefix": "0.0.0.0",
},
{
"ipv6Prefix": "0000:0000:0000::/16",
},
]
`);
});
});

View File

@@ -0,0 +1,24 @@
import got from "got";
import { CIDR } from "./isInCidRange.js";
export default async function getGoogleAddresses(): Promise<CIDR> {
const { body } = await got.get("https://www.gstatic.com/ipranges/cloud.json"),
result: GoogleResult = JSON.parse(body);
return result.prefixes.map(({ ipv4Prefix, ipv6Prefix }) => {
return ipv6Prefix ? { ipv6Prefix } : { ipv4Prefix };
});
}
interface GoogleResult {
syncToken: string;
creationTime: string;
prefixes: GoogleIP[];
}
interface GoogleIP {
ipv6Prefix: string;
ipv4Prefix: string;
service: string;
scope: string;
}

View File

@@ -0,0 +1,31 @@
import { expect, test } from "vitest";
import isInCIDRRange from "./isInCidRange.js";
test("isInCIDRRange - IPv4 - in range", async () => {
const CIDRs = [{ ipv4Prefix: "192.0.2.0/24" }],
ip = "192.0.2.123",
result = isInCIDRRange(CIDRs, ip);
expect(result).toBe(true);
});
test("isInCIDRRange - IPv4 - not in range", async () => {
const CIDRs = [{ ipv4Prefix: "192.0.2.0/24" }],
ip = "192.0.3.123",
result = isInCIDRRange(CIDRs, ip);
expect(result).toBe(false);
});
test("isInCIDRRange - IPv6 - in range", async () => {
const CIDRs = [{ ipv6Prefix: "2001:db8::/32" }],
ip = "2001:db8::1234",
result = isInCIDRRange(CIDRs, ip);
expect(result).toBe(true);
});
test("isInCIDRRange - IPv6 - not in range", async () => {
const CIDRs = [{ ipv6Prefix: "2001:db8::/32" }],
ip = "2001:db9::1234",
result = isInCIDRRange(CIDRs, ip);
expect(result).toBe(false);
});

View File

@@ -0,0 +1,26 @@
import ipaddr from "ipaddr.js";
export default function isInCIDRRange(CIDRs: CIDR, ip: string) {
const parsed = ipaddr.parse(ip);
for (const CIDR of CIDRs.filter((c) => {
if (parsed.kind() === "ipv4" && "ipv4Prefix" in c) return true;
else if (parsed.kind() === "ipv6" && "ipv6Prefix" in c) return true;
else return false;
})) {
const check = parsed.match(ipaddr.parseCIDR("ipv4Prefix" in CIDR ? CIDR.ipv4Prefix : CIDR.ipv6Prefix));
if (check) return check;
}
return false;
}
export type CIDR = (
| {
ipv4Prefix: string;
}
| {
ipv6Prefix: string;
}
)[];

View File

@@ -0,0 +1,4 @@
/* eslint-disable unicorn/filename-case */
import getGoogleAddresses from "./functions/getGoogleAddresses.js";
export default await getGoogleAddresses();

14
apps/pd/src/index.test.ts Normal file
View File

@@ -0,0 +1,14 @@
import { expect, test } from "vitest";
import { createServer } from "./functions/createServer.js";
test("/health", async () => {
const server = await createServer(),
result = await server.inject({
method: "GET",
url: "/health",
});
expect(result.statusCode).toBe(204);
expect(result.body).toBe("");
});

15
apps/pd/src/index.ts Normal file
View File

@@ -0,0 +1,15 @@
/* c8 ignore start */
import { createServer } from "./functions/createServer.js";
import redis from "./redis.js";
if (!process.env.REDIS_URL) console.log("WARNING: No REDIS_URL environment variable set");
export const server = await createServer(redis);
const url = await server.listen({
host: process.env.HOST ?? "0.0.0.0",
port: Number.parseInt(process.env.PORT ?? "80"),
});
console.log(`Server listening at ${url}`);
// TODO Make proper error codes & json responses

3
apps/pd/src/keyv.ts Normal file
View File

@@ -0,0 +1,3 @@
import createKeyv from "./functions/createKeyv.js";
export default await createKeyv();

3
apps/pd/src/redis.ts Normal file
View File

@@ -0,0 +1,3 @@
import createRedis from "./functions/createRedis.js";
export default createRedis();

View File

@@ -0,0 +1,98 @@
import { describe, it } from "vitest";
import { createServer } from "../functions/createServer.js";
describe.concurrent("createFromBase64", async () => {
const server = await createServer();
it("should return a 400 when the body is not present", async ({ expect }) => {
const result = await server.inject({
method: "POST",
url: "/create/base64",
});
expect(result.statusCode).toBe(400);
expect(result.body).toMatchInlineSnapshot("\"Invalid body\"");
});
it("should return a 400 when the body is not a string", async ({ expect }) => {
const result = await server.inject({
method: "POST",
payload: new Blob([]),
url: "/create/base64",
});
expect(result.statusCode).toBe(400);
expect(result.body).toMatchInlineSnapshot("\"Invalid body\"");
});
it("should return a 400 when the body is not a valid base64 string", async ({ expect }) => {
const result = await server.inject({
headers: {
"Content-Type": "text/plain",
},
method: "POST",
payload: "data:image/png;base64t",
url: "/create/base64",
});
expect(result.statusCode).toBe(400);
expect(result.body).toMatchInlineSnapshot("\"Invalid base64 string\"");
});
it("should return a 400 when the base64 string is not a valid image", async ({ expect }) => {
const result = await server.inject({
headers: {
"Content-Type": "text/plain",
},
method: "POST",
payload: "data:image/sv;base64,a",
url: "/create/base64",
});
expect(result.statusCode).toBe(400);
expect(result.body).toMatchInlineSnapshot("\"Invalid base64 string\"");
});
it("should return a 400 when the base64 string is not a valid image", async ({ expect }) => {
const result = await server.inject({
headers: {
"Content-Type": "text/plain",
},
method: "POST",
payload: "data:image/svg+xml;base64,s",
url: "/create/base64",
});
expect(result.statusCode).toBe(400);
expect(result.body).toMatchInlineSnapshot("\"Supported types: png, jpeg, jpg, gif, webp\"");
});
it("should return a 200 when the base64 string is valid", async ({ expect }) => {
const result = await server.inject({
headers: {
"Content-Type": "text/plain",
},
method: "POST",
payload: "data:image/png;base64,s",
url: "/create/base64",
});
expect(result.statusCode).toBe(200);
expect(result.body).toStrictEqual(expect.any(String));
});
it("should return a 200 when the base64 string is valid", async ({ expect }) => {
const result = await server.inject({
headers: {
"Content-Type": "text/plain",
},
method: "POST",
payload: "data:image/png;base64,s",
url: "/create/base64",
});
expect(result.statusCode).toBe(200);
expect(result.body).toStrictEqual(expect.any(String));
});
});

View File

@@ -0,0 +1,43 @@
import crypto from "node:crypto";
import { RouteHandlerMethod } from "fastify";
import mime from "mime-types";
import { nanoid } from "nanoid";
import keyv from "../keyv.js";
const handler: RouteHandlerMethod = async (request, reply) => {
const { body } = request;
if (!body) return reply.status(400).send("Invalid body");
if (typeof body !== "string") return reply.status(400).send("Invalid body");
const matches = body.match(/^data:(.+);base64,(.+)/);
if (!matches || matches.length === 0) return reply.status(400).send("Invalid base64 string");
const type = mime.extension(matches.at(1) as string);
if (!type) return reply.status(400).send("Invalid base64 string");
if (!["png", "jpeg", "jpg", "gif", "webp"].includes(type)) return reply.status(400).send("Supported types: png, jpeg, jpg, gif, webp");
const hash = crypto.createHash("sha256").update(body).digest("hex"),
existingUrl = await keyv.get(hash);
if (existingUrl) {
reply.header("Cache-control", `public, max-age=${30 * 60}`);
return reply.send(process.env.HOST + existingUrl);
}
const uniqueId = `${nanoid(10)}.${type}`;
await keyv.set(hash, uniqueId, 30 * 60 * 1000);
await keyv.set(uniqueId, body, 30 * 60 * 1000);
reply.header("Cache-control", `public, max-age=${30 * 60}`);
return reply.send(process.env.BASE_URL + uniqueId);
};
export default handler;

View File

@@ -0,0 +1,107 @@
import { readFile } from "node:fs/promises";
import { RequestOptions } from "node:http";
import { AddressInfo } from "node:net";
import { afterAll, beforeAll, describe, it } from "vitest";
import { createServer } from "../functions/createServer.js";
describe.concurrent("createFromImage", async () => {
const server = await createServer(),
form = new FormData(),
defaultRequestOptions: RequestOptions = {
hostname: "localhost",
method: "POST",
path: "/create/image",
protocol: "http:",
};
let url: string;
beforeAll(async () => {
url = await server.listen();
defaultRequestOptions.port = (server.server.address() as AddressInfo).port;
});
afterAll(() => {
server.close();
});
it("should return a 400 when request is not multipart", async ({ expect }) => {
const result = await fetch(`${url}/create/image`, {
method: "POST",
});
expect(result.status).toBe(400);
expect(await result.text()).toMatchInlineSnapshot("\"Request is not multipart\"");
});
it("should return a 400 status code when no file is provided", async ({ expect }) => {
const result = await fetch(`${url}/create/image`, {
body: form,
method: "POST",
});
expect(result.status).toBe(400);
expect(await result.text()).toMatchInlineSnapshot("\"Invalid file\"");
});
it("should return a 400 status code when the file is invalid", async ({ expect }) => {
form.set("file", Buffer.alloc(1024 * 1024 * 2));
const result = await fetch(`${url}/create/image`, {
body: form,
method: "POST",
});
expect(result.status).toBe(400);
expect(await result.text()).toMatchInlineSnapshot("\"Invalid file\"");
});
it("should return a 400 status code when the file is invalid", async ({ expect }) => {
form.set("file", new Blob([new Uint8Array(1024 * 1024 * 2)]));
const result = await fetch(`${url}/create/image`, {
body: form,
method: "POST",
});
expect(result.status).toBe(400);
expect(await result.text()).toMatchInlineSnapshot("\"Invalid file\"");
});
it("should return a 400 status code when the file is not an image", async ({ expect }) => {
form.set("file", new Blob([await readFile(new URL("../../fixtures/test.mp4", import.meta.url))]));
const result = await fetch(`${url}/create/image`, {
body: form,
method: "POST",
});
expect(result.status).toBe(400);
expect(await result.text()).toMatchInlineSnapshot("\"Only png, jpeg, jpg, gif and webp are supported\"");
});
it("should return a 200 status code when the file is valid", async ({ expect }) => {
form.set("file", new Blob([await readFile(new URL("../../fixtures/1x1.png", import.meta.url))]));
const result = await fetch(`${url}/create/image`, {
body: form,
method: "POST",
});
expect(result.status).toBe(200);
expect(await result.text()).toStrictEqual(expect.any(String));
});
it("should return a 200 status code when the file is valid and the same file is uploaded again", async ({ expect }) => {
form.set("file", new Blob([await readFile(new URL("../../fixtures/1x1.png", import.meta.url))]));
const result = await fetch(`${url}/create/image`, {
body: form,
method: "POST",
});
expect(result.status).toBe(200);
expect(await result.text()).toStrictEqual(expect.any(String));
});
});

View File

@@ -0,0 +1,50 @@
import crypto from "node:crypto";
import { RouteHandlerMethod } from "fastify";
import { fileTypeFromBuffer } from "file-type";
import { nanoid } from "nanoid";
import keyv from "../keyv.js";
const handler: RouteHandlerMethod = async (request, reply) => {
if (!request.isMultipart()) return reply.status(400).send("Request is not multipart");
const file = await request.file();
if (!file) return reply.status(400).send("Invalid file");
const type = await fileTypeFromBuffer(await file.toBuffer());
if (!type) return reply.status(400).send("Invalid file");
if (![
"image/png",
"image/jpeg",
"image/jpg",
"image/gif",
"image/webp",
].includes(type.mime))
return reply.status(400).send("Only png, jpeg, jpg, gif and webp are supported");
const buffer = await file.toBuffer(),
body = `data:${type.mime};base64,${buffer.toString("base64")}`,
hash = crypto.createHash("sha256").update(body).digest("hex"),
existingUrl = await keyv.get(hash);
if (existingUrl) {
reply.header("Cache-control", `public, max-age=${30 * 60}`);
return reply.send(process.env.HOST + existingUrl);
}
const uniqueId = `${nanoid(10)}.${type.ext}`;
await Promise.all([
keyv.set(hash, uniqueId, 30 * 60 * 1000),
keyv.set(uniqueId, body, 30 * 60 * 1000),
]);
reply.header("Cache-control", `public, max-age=${30 * 60}`);
return reply.send(process.env.BASE_URL + uniqueId);
};
export default handler;

View File

@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { createServer } from "../functions/createServer.js";
describe.concurrent("/create", async () => {
const server = await createServer();
it("should return a 400 status code when no URL is provided", async () => {
const result = await server.inject({
method: "GET",
url: "/create/",
});
expect(result.statusCode).toBe(400);
expect(result.body).toMatchInlineSnapshot('"Invalid URL"');
});
it("should return a 400 status code when the URL is too short", async () => {
const result = await server.inject({
method: "GET",
url: "/create/https://www.google.com",
});
expect(result.statusCode).toBe(400);
expect(result.body).toMatchInlineSnapshot('"URL is too short"');
});
it("should return a 400 status code when the URL is invalid", async () => {
const result = await server.inject({
method: "GET",
url: `/create/file://www.googl${"e".repeat(256)}`,
});
expect(result.statusCode).toBe(400);
expect(result.body).toMatchInlineSnapshot('"Invalid URL"');
});
it("should return a 200 status code when the URL is valid", async () => {
const result = await server.inject({
method: "GET",
url: `/create/https://www.googl${"e".repeat(256)}.com`,
});
expect(result.statusCode).toBe(200);
expect(result.body).toStrictEqual(expect.any(String));
});
it("should return a 200 status code when the URL is valid and already exists", async () => {
const result = await server.inject({
method: "GET",
url: `/create/https://www.googl${"d".repeat(256)}.com`,
});
expect(result.statusCode).toBe(200);
expect(result.body).toStrictEqual(expect.any(String));
const { body } = result,
result2 = await server.inject({
method: "GET",
url: `/create/https://www.googl${"d".repeat(256)}.com`,
});
expect(result2.statusCode).toBe(200);
expect(result2.body).toStrictEqual(body);
});
});

View File

@@ -0,0 +1,35 @@
import crypto from "node:crypto";
import { RouteHandlerMethod } from "fastify";
import { nanoid } from "nanoid";
import keyv from "../keyv.js";
const handler: RouteHandlerMethod = async (request, reply) => {
const url = request.url.replace("/create/", "").trim();
if (url.length === 0) return reply.status(400).send("Invalid URL");
if (url.length < 256) return reply.status(400).send("URL is too short");
const urlObject = new URL(url);
if (!["http:", "https:"].includes(urlObject.protocol)) return reply.status(400).send("Invalid URL");
const hash = crypto.createHash("sha256").update(url).digest("hex"),
existingShortenedUrl = await keyv.get(hash);
void reply.header("Cache-control", "public, max-age=1800");
if (existingShortenedUrl) {
await Promise.all([keyv.set(hash, existingShortenedUrl, 1800), keyv.set(existingShortenedUrl, url, 1800)]);
return reply.send(process.env.BASE_URL + existingShortenedUrl);
}
const uniqueId = nanoid(10);
await Promise.all([keyv.set(hash, uniqueId, 1800), keyv.set(uniqueId, url, 1800)]);
return reply.send(process.env.BASE_URL + uniqueId);
};
export default handler;

View File

@@ -0,0 +1,100 @@
import { readFile } from "node:fs/promises";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { createServer } from "../functions/createServer.js";
import * as isInCIDRRange from "../functions/isInCidRange.js";
describe("getFullLink", async () => {
const server = await createServer();
let url: string;
beforeAll(async () => {
url = await server.listen();
});
afterAll(async () => {
await server.close();
});
it("should fail if not a Google Cloud IP", async () => {
const result = await server.inject({
headers: {
"cf-connecting-ip": "",
},
url: "/1234567890",
});
expect(result.statusCode).toBe(401);
});
it("should fail if not a valid ID", async () => {
vi.spyOn(isInCIDRRange, "default").mockReturnValueOnce(true);
const result = await server.inject({
headers: {
"cf-connecting-ip": "",
},
url: "/123",
});
vi.spyOn(isInCIDRRange, "default").mockReturnValueOnce(true);
const result2 = await server.inject({
headers: {
"cf-connecting-ip": "",
},
url: "/1234567890.",
});
expect(result.statusCode).toBe(404);
expect(result2.statusCode).toBe(404);
});
it("should redirect to the correct URL", async () => {
vi.spyOn(isInCIDRRange, "default").mockReturnValueOnce(true);
const { body } = await server.inject({
url: `/create/https://${"a".repeat(256)}`,
});
expect(body).toStrictEqual(expect.any(String));
const result = await server.inject({
headers: {
"cf-connecting-ip": "",
},
url: body,
});
expect(result.statusCode).toBe(302);
expect(result.headers.location).toBe(`https://${"a".repeat(256)}`);
});
it("should return the correct image", async () => {
const imageBuffer = await readFile(new URL("../../fixtures/test.mp4", import.meta.url)),
imageBase64 = `data:image/png;base64,${imageBuffer.toString("base64")}`,
{ body } = await server.inject({
headers: {
"Content-Type": "text/plain",
},
method: "POST",
payload: imageBase64,
url: "/create/base64",
});
expect(body).toStrictEqual(expect.any(String));
vi.spyOn(isInCIDRRange, "default").mockReturnValueOnce(true);
const result = await fetch(`${url}${body}`, {
headers: {
"cf-connecting-ip": "",
},
});
expect(result.status).toBe(200);
expect(result.headers.get("content-type")).toBe("image/png");
expect(Buffer.from(await result.arrayBuffer())).toStrictEqual(imageBuffer);
});
});

View File

@@ -0,0 +1,46 @@
import crypto from "node:crypto";
import { RouteHandlerMethod } from "fastify";
import isInCIDRRange from "../functions/isInCidRange.js";
import googleCIDRs from "../googleCIDRs.js";
import keyv from "../keyv.js";
const handler: RouteHandlerMethod = async (request, reply) => {
/* c8 ignore next 1 */
const ip = request.headers["cf-connecting-ip"]?.toString() || request.socket.remoteAddress || request.ip;
if (
!isInCIDRRange(
googleCIDRs,
ip,
)
)
return reply.status(401).send("Not a Google Cloud IP");
const id = (request.params as { "*": string })["*"].trim();
if (id.split(".")[0]?.length !== 10) return reply.code(404).send("Invalid ID");
const url = await keyv.get(id);
if (!url) return reply.code(404).send("Unknown ID");
const hash = crypto.createHash("sha256").update(url).digest("hex");
await Promise.all([keyv.set(hash, id, 30 * 60 * 1000), keyv.set(id, url, 30 * 60 * 1000)]);
reply.header("Cache-control", "public, max-age=1800");
//* If it is not a base64 string, redirect to it
if (!url.startsWith("data:image")) return reply.redirect(url);
const image = Buffer.from(
url.replace(/^data:image\/\w+;base64,/, ""),
"base64",
),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mime = url.split(";")[0]!.split(":")[1]!;
return reply.type(mime).send(image);
};
export default handler;

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true
}
}

8
apps/pd/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.app.json",
"include": ["environment.d.ts", "src"],
"compilerOptions": {
"noEmit": true,
"types": ["@types/node"]
}
}

View File

@@ -0,0 +1,26 @@
FROM node:20-alpine as build
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml /app/
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch --frozen-lockfile
COPY . /app
RUN pnpm install --frozen-lockfile --offline
RUN pnpm --filter @premid/schema-server build && \
pnpm deploy --filter @premid/schema-server deploy
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/deploy /app
USER node
CMD ["npm", "run", "start"]

View File

@@ -0,0 +1,3 @@
# @premid/schema-server
This is a simple schema server which serves JSON schemas for Presence Developers.

8
apps/schema-server/environment.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/* eslint-disable unicorn/prevent-abbreviations */
declare namespace NodeJS {
export interface ProcessEnv {
NODE_ENV?: "development" | "production" | "test";
PORT?: string;
HOST?: string;
}
}

View File

@@ -0,0 +1,23 @@
{
"name": "@premid/schema-server",
"version": "1.0.0",
"description": "A small service to serve the JSON schemas for PreMiD",
"main": "dist/index.js",
"type": "module",
"files": [
"dist",
"schemas"
],
"scripts": {
"start": "node --enable-source-maps .",
"dev": "node --watch --enable-source-maps .",
"build": "tsc -p tsconfig.app.json"
},
"private": true,
"license": "MPL-2.0",
"dependencies": {
"@fastify/helmet": "^11.1.1",
"fastify": "^4.26.0",
"globby": "^14.0.1"
}
}

View File

@@ -0,0 +1,241 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.0",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(_[A-Z]{2})?$"
},
"patternProperties": {
"^[a-z]{2}(_[A-Z]{2})?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?:\\/\\/?(?:[a-z0-9-]+\\.)*[0-9a-z_-]+(?:\\.[a-z]+)+\\/.*$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?:\\/\\/?([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+\\/.*$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa[bs] fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,252 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.0",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(_[A-Z]{2})?$"
},
"patternProperties": {
"^[a-z]{2}(_[A-Z]{2})?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?:\\/\\/?(?:[a-z0-9-]+\\.)*[0-9a-z_-]+(?:\\.[a-z]+)+\\/.*$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?:\\/\\/?([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+\\/.*$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa[bs] fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,277 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.10",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$"
},
"patternProperties": {
"^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"readLogs": {
"type": "boolean",
"description": "Whether or not the extension should be reading logs."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"matches": {
"type": "array",
"description": "A glob pattern required to match Google's match pattern (https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns). This is required for Presences.",
"items": {
"type": "string",
"description": "A glob pattern.",
"pattern": "^(https|http|[*])://.*/.*"
}
},
"iFrameMatches": {
"type": "array",
"description": "A glob pattern required to match Google's match pattern (https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns). This is required for Presences.",
"items": {
"type": "string",
"description": "A glob pattern.",
"pattern": "^(https|http|[*])://.*/.*"
}
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa([bsdrlt]|([-](brands|solid|duotone|regular|light|thin))) fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,257 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.0",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(_[A-Z]{2})?$"
},
"patternProperties": {
"^[a-z]{2}(_[A-Z]{2})?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?:\\/\\/?(?:[a-z0-9-]+\\.)*[0-9a-z_-]+(?:\\.[a-z]+)+\\/.*$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?:\\/\\/?([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+\\/.*$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"readLogs": {
"type": "boolean",
"description": "Whether or not the extension should be reading logs."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa[bs] fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,257 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.3",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(_[A-Z]{2})?$"
},
"patternProperties": {
"^[a-z]{2}(_[A-Z]{2})?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?:\\/\\/?(?:[a-z0-9-]+\\.)*[0-9a-z_-]+(?:\\.[a-z]+)+\\/.*$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?:\\/\\/?([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+\\/.*$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"readLogs": {
"type": "boolean",
"description": "Whether or not the extension should be reading logs."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa[bsd] fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,257 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.4",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(_[A-Z]{2})?$"
},
"patternProperties": {
"^[a-z]{2}(_[A-Z]{2})?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?://(i).(imgur).(com)/(.*?).(png|jpg)$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?://(i).(imgur).(com)/(.*?).(png|jpg)$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"readLogs": {
"type": "boolean",
"description": "Whether or not the extension should be reading logs."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa[bsd] fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,257 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.5",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(_[A-Z]{2})?$"
},
"patternProperties": {
"^[a-z]{2}(_[A-Z]{2})?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?://(i).(imgur).(com)/(.*?).(png|jpe?g|gif)$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?://(i).(imgur).(com)/(.*?).(png|jpe?g)$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"readLogs": {
"type": "boolean",
"description": "Whether or not the extension should be reading logs."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa[bsd] fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,257 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.6",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$"
},
"patternProperties": {
"^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?://(i).(imgur).(com)/(.*?).(png|jpe?g|gif)$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?://(i).(imgur).(com)/(.*?).(png|jpe?g)$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"readLogs": {
"type": "boolean",
"description": "Whether or not the extension should be reading logs."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa[bsd] fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,257 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.7",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$"
},
"patternProperties": {
"^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?://(i).(imgur).(com)/(.*?).(png|jpe?g|gif)$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?://(i).(imgur).(com)/(.*?).(png|jpe?g)$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"readLogs": {
"type": "boolean",
"description": "Whether or not the extension should be reading logs."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa([bsdrlt]|([-](brands|solid|duotone|regular|light|thin))) fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,257 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.8",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$"
},
"patternProperties": {
"^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?://(cdn[.]rcd[.]gg|i[.]imgur[.]com)/(.*?)[.](png|jpe?g|gif|webp)$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?://(cdn[.]rcd[.]gg|i[.]imgur[.]com)/(.*?)[.](png|jpe?g|gif|webp)$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"readLogs": {
"type": "boolean",
"description": "Whether or not the extension should be reading logs."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa([bsdrlt]|([-](brands|solid|duotone|regular|light|thin))) fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,257 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://schemas.premid.app/metadata/1.9",
"title": "Metadata",
"type": "object",
"description": "Metadata that describes a presence.",
"definitions": {
"user": {
"type": "object",
"description": "User information.",
"properties": {
"name": {
"type": "string",
"description": "The name of the user."
},
"id": {
"type": "string",
"description": "The Discord snowflake of the user.",
"pattern": "^\\d+$"
}
},
"additionalProperties": false,
"required": ["name", "id"]
}
},
"properties": {
"$schema": {
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
"type": "string",
"description": "The metadata schema URL."
},
"author": {
"$ref": "#/definitions/user",
"description": "The author of this presence."
},
"contributors": {
"type": "array",
"description": "Any extra contributors to this presence.",
"items": {
"$ref": "#/definitions/user"
}
},
"service": {
"type": "string",
"description": "The service this presence is for."
},
"altnames": {
"type": "array",
"description": "Alternative names for the service.",
"items": {
"type": "string",
"description": "An alternative name."
},
"minItems": 1
},
"description": {
"type": "object",
"description": "A description of the presence in multiple languages.",
"propertyNames": {
"type": "string",
"description": "The language key. The key must be languagecode(_REGIONCODE).",
"pattern": "^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$"
},
"patternProperties": {
"^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$": {
"type": "string",
"description": "The description of the presence in the key's language."
}
},
"additionalProperties": false,
"required": ["en"]
},
"url": {
"type": ["string", "array"],
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
"items": {
"type": "string",
"description": "One of the service's website URLs.",
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
},
"minItems": 2
},
"version": {
"type": "string",
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"logo": {
"type": "string",
"description": "The logo of the service this presence is for.",
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
},
"thumbnail": {
"type": "string",
"description": "A thumbnail of the service this presence is for.",
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
},
"color": {
"type": "string",
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
},
"tags": {
"type": ["array"],
"description": "The tags for the presence.",
"items": {
"type": "string",
"description": "A tag.",
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
},
"minItems": 1
},
"category": {
"type": "string",
"description": "The category the presence falls under.",
"enum": ["anime", "games", "music", "socials", "videos", "other"]
},
"iframe": {
"type": "boolean",
"description": "Whether or not the presence should run in IFrames."
},
"readLogs": {
"type": "boolean",
"description": "Whether or not the extension should be reading logs."
},
"regExp": {
"type": "string",
"description": "A regular expression used to match URLs for the presence to inject into."
},
"iFrameRegExp": {
"type": "string",
"description": "A regular expression used to match IFrames for the presence to inject into."
},
"button": {
"type": "boolean",
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
},
"warning": {
"type": "boolean",
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
},
"settings": {
"type": "array",
"description": "An array of settings the user can change in the presence.",
"items": {
"type": "object",
"description": "A setting.",
"properties": {
"id": {
"type": "string",
"description": "The ID of the setting."
},
"title": {
"type": "string",
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
},
"icon": {
"type": "string",
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
"pattern": "^fa([bsdrlt]|([-](brands|solid|duotone|regular|light|thin))) fa-[0-9a-z-]+$"
},
"if": {
"type": "object",
"description": "Restrict showing this setting if another setting is the defined value.",
"propertyNames": {
"type": "string",
"description": "The ID of the setting."
},
"patternProperties": {
"": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"additionalProperties": false
},
"placeholder": {
"type": "string",
"description": "The placeholder for settings that require input. Shown when the input is empty."
},
"value": {
"type": ["string", "number", "boolean"],
"description": "The default value of the setting. Not compatible with `values`."
},
"values": {
"type": "array",
"description": "The default values of the setting. Not compatible with `value`.",
"items": {
"type": ["string", "number", "boolean"],
"description": "The value of the setting."
}
},
"multiLanguage": {
"type": ["string", "boolean", "array"],
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
"items": {
"type": "string",
"description": "The name of a file from the localization GitHub repository."
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
}

View File

@@ -0,0 +1,35 @@
# Metadata Schema
This schema is used to validate the `metadata.json` files in [PreMiD/Presences](https://github.com/PreMiD/Presences).
## 1.7
- Updates `icon` regex for compability with FontAwesome v6 icon labelling
## 1.6
- Adds support for language locales that end in numbers (e.g. es_419)
## 1.5
- Allows `.gif` extensions for service logos and `.jpeg` extensions for service logos and thumbnails.
## 1.4
- Adds regex for only allowing imgur links for `logo` & `thumbnail`.
## 1.3
- Fixes validation for icons.
## 1.2
- Adds "readLogs".
## 1.1
- Adds "altnames" and "multiLanguage".
## 1.0
- First iteration of the metadata file format.

View File

@@ -0,0 +1,37 @@
import { resolve } from "node:path";
import { describe, expect, test } from "vitest";
import { app } from "./index.js";
describe("schemas", () => {
test("/ should return date", async () => {
const result = await app.inject({ url: "/" });
expect(result.statusCode).toBe(200);
expect(result.json()).toEqual({ date: expect.any(String) });
});
test("/metadata/1.0 should return metadata schema", async () => {
const result = await app.inject({ url: "/metadata/1.0" }),
schema = await import(resolve(import.meta.dirname, "../schemas/metadata/1.0.json"));
expect(result.statusCode).toBe(200);
expect(result.json()).toEqual(schema.default);
});
test("/metadata/1.0 should return cached metadata schema", async () => {
const result = await app.inject({ url: "/metadata/1.0" }),
schema = await import(resolve(import.meta.dirname, "../schemas/metadata/1.0.json"));
expect(result.statusCode).toBe(200);
expect(result.json()).toEqual(schema.default);
});
test("/metadata/1 should return 404", async () => {
const result = await app.inject({ url: "/metadata/1" });
expect(result.statusCode).toBe(404);
expect(result.json()).toEqual({ error: "Schema not found." });
});
});

View File

@@ -0,0 +1,61 @@
import { extname, resolve } from "node:path";
import helmet from "@fastify/helmet";
import fastify, { RequestGenericInterface } from "fastify";
import { globby } from "globby";
export const app = fastify();
app.register(helmet);
app.get("/", (_, reply) => reply.send({ date: new Date() }));
interface versionRequest extends RequestGenericInterface {
Params: {
schemaName: string;
version: string;
};
}
const availableSchemas: Record<string, {
version: string;
content: Record<string, unknown>;
}[]> = {};
for (const schemaPath of await globby(resolve(import.meta.dirname, "../schemas/*/*.json"), { onlyFiles: true })) {
const [schemaName, version] = schemaPath.split("/").slice(-2) as [string, string];
let schemaVersions = availableSchemas[schemaName];
if (!schemaVersions)
schemaVersions = [];
const { default: content } = await import(schemaPath, { with: { type: "json" } });
schemaVersions.push({
content,
version: version.replace(extname(version), ""),
});
availableSchemas[schemaName] = schemaVersions;
}
app.get<versionRequest>("/:schemaName/:version", async (request, reply) => {
const { schemaName, version } = request.params;
if (!availableSchemas[schemaName]?.some(schema => schema.version === version))
return reply.status(404).send({ error: "Schema not found." });
return reply.send(availableSchemas[schemaName]?.find(schema => schema.version === version)?.content);
});
/* c8 ignore start */
if (process.env.NODE_ENV !== "test") {
const url = await app.listen({
host: process.env.HOST ?? "0.0.0.0",
port: Number.parseInt(process.env.PORT || "80"),
});
// eslint-disable-next-line no-console
console.log(`Listening on ${url}`);
}
/* c8 ignore stop */

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.app.json",
"include": ["environment.d.ts", "src"],
"compilerOptions": {
"noEmit": true,
"types": ["@types/node"]
}
}

2
commitlint.config.cjs Normal file
View File

@@ -0,0 +1,2 @@
/* eslint-disable @typescript-eslint/no-var-requires */
module.exports = require("@recodive/configs").default.commitlint;

View File

@@ -1,416 +0,0 @@
<project>
<shortName>PreMiD</shortName>
<fullName>PreMiD</fullName>
<version>VERSION</version>
<installerFilename>upgrader.${platform_exec_suffix}</installerFilename>
<debugLevel>0</debugLevel>
<licenseFile>../LICENSE</licenseFile>
<leftImage>leftSide.png</leftImage>
<logoImage>logo.png</logoImage>
<splashImage>logo.png</splashImage>
<componentList>
<component>
<name>default</name>
<description>Default Component</description>
<canBeEdited>1</canBeEdited>
<selected>1</selected>
<show>1</show>
<folderList>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfiles</name>
<platforms>all</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfileslinux</name>
<platforms>linux</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfileslinux64</name>
<platforms>linux-x64</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfileswindows</name>
<platforms>windows</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfileswindows64</name>
<platforms>windows-x64</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfilesosx</name>
<platforms>osx</platforms>
</folder>
</folderList>
<startMenuShortcutList>
<startMenuShortcut>
<comment>Uninstall ${product_fullname}</comment>
<name>Uninstall ${product_fullname}</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>${installdir}/${uninstallerName}.exe</windowsExec>
<windowsExecArgs></windowsExecArgs>
<windowsIcon></windowsIcon>
<windowsPath>${installdir}/</windowsPath>
</startMenuShortcut>
<startMenuShortcut>
<comment>Rich Presence for web services.</comment>
<name>PreMiD</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>${installdir}/PreMiD.${platform_exec_suffix}</windowsExec>
<windowsExecArgs></windowsExecArgs>
<windowsIcon></windowsIcon>
<windowsPath></windowsPath>
</startMenuShortcut>
</startMenuShortcutList>
</component>
</componentList>
<initializationActionList>
<setInstallerVariable>
<name>installdir</name>
<persist>1</persist>
<value>${windows_folder_appdata}/PreMiD</value>
<ruleList>
<platformTest>
<type>windows</type>
</platformTest>
</ruleList>
</setInstallerVariable>
<setInstallerVariable>
<name>installdir</name>
<persist>1</persist>
<value>${platform_install_prefix}/PreMiD</value>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
</ruleList>
</setInstallerVariable>
</initializationActionList>
<readyToInstallActionList>
<actionGroup>
<customErrorMessage>Couldn't download release... Try again later.</customErrorMessage>
<progressText>Downloading latest release...</progressText>
<actionList>
<showProgressDialog>
<title>Downloading latest release...</title>
<width>450</width>
<actionList>
<httpGet>
<customErrorMessage>${platform_name}</customErrorMessage>
<filename>${system_temp_directory}/PreMiD-release.zip</filename>
<url>https://github.com/PreMiD/PreMiD/releases/latest/download/PreMiD-win32-x64.zip</url>
</httpGet>
</actionList>
<ruleList>
<platformTest>
<type>windows-x64</type>
</platformTest>
</ruleList>
</showProgressDialog>
<showProgressDialog>
<title>Downloading latest release...</title>
<actionList>
<httpGet>
<customErrorMessage>${platform_name}</customErrorMessage>
<filename>${system_temp_directory}/PreMiD-release.zip</filename>
<url>https://github.com/PreMiD/PreMiD/releases/latest/download/PreMiD-win32-ia32.zip</url>
</httpGet>
</actionList>
<ruleList>
<platformTest>
<type>windows-x86</type>
</platformTest>
</ruleList>
</showProgressDialog>
<showProgressDialog>
<title>Downloading latest release...</title>
<actionList>
<httpGet>
<customErrorMessage>${platform_name}</customErrorMessage>
<filename>${system_temp_directory}/PreMiD-release.zip</filename>
<url>https://github.com/PreMiD/PreMiD/releases/latest/download/PreMiD-darwin-x64.zip</url>
</httpGet>
</actionList>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
</ruleList>
</showProgressDialog>
</actionList>
</actionGroup>
<actionGroup>
<actionList>
<!-- Remove the old ARP Entry
Get the old version -->
<registryGet>
<key>HKEY_LOCAL_MACHINE\Software\${project.windowsSoftwareRegistryPrefix}</key>
<name>Version</name>
<variable>oldVersion</variable>
</registryGet>
<!-- Delete the old ARP registry keys -->
<registryDelete>
<key>HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${project.fullName} ${oldVersion}</key>
<name></name>
</registryDelete>
<registryDelete>
<key>HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Management\ARPCache\${project.fullName} ${oldVersion}</key>
<name></name>
</registryDelete>
</actionList>
<ruleList>
<platformTest type="windows"/>
<isTrue value="${isUpgradeMode}"/>
</ruleList>
</actionGroup>
<actionGroup>
<progressText>Killing ${product_fullname}...</progressText>
<actionList>
<kill>
<abortOnError>0</abortOnError>
<name>${product_fullname}.exe</name>
<showMessageOnError>0</showMessageOnError>
<ruleList>
<platformTest>
<type>windows</type>
</platformTest>
</ruleList>
</kill>
<runProgram>
<program>pkill</program>
<programArguments>PreMiD</programArguments>
<runAs>${env(USER)}</runAs>
<useMSDOSPath>0</useMSDOSPath>
<workingDirectory>${installdir}/</workingDirectory>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
<processTest>
<logic>is_running</logic>
<name>PreMiD</name>
</processTest>
</ruleList>
</runProgram>
</actionList>
</actionGroup>
</readyToInstallActionList>
<postInstallationActionList>
<unzip>
<addToUninstaller>1</addToUninstaller>
<destinationDirectory>${installdir}</destinationDirectory>
<progressText>Extracting release...</progressText>
<zipFile>${system_temp_directory}/PreMiD-release.zip</zipFile>
</unzip>
<addDirectoriesToUninstaller>
<addContents>1</addContents>
<files>${installdir}/</files>
</addDirectoriesToUninstaller>
</postInstallationActionList>
<preUninstallationActionList>
<actionGroup>
<progressText>Killing ${product_fullname}...</progressText>
<actionList>
<kill>
<abortOnError>0</abortOnError>
<name>${product_fullname}.exe</name>
<showMessageOnError>0</showMessageOnError>
<ruleList>
<platformTest>
<type>windows</type>
</platformTest>
</ruleList>
</kill>
<runProgram>
<program>pkill</program>
<programArguments>PreMiD</programArguments>
<runAs>${env(USER)}</runAs>
<useMSDOSPath>0</useMSDOSPath>
<workingDirectory>${installdir}/</workingDirectory>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
<processTest>
<logic>is_running</logic>
<name>PreMiD</name>
</processTest>
</ruleList>
</runProgram>
</actionList>
</actionGroup>
</preUninstallationActionList>
<compressionAlgorithm>lzham-ultra</compressionAlgorithm>
<createOsxBundleZip>1</createOsxBundleZip>
<defaultInstallationMode>unattended</defaultInstallationMode>
<deleteOnExit>1</deleteOnExit>
<description>Rich Presence for web services.</description>
<disableSplashScreen>1</disableSplashScreen>
<enableRollback>0</enableRollback>
<enableSslSupport>1</enableSslSupport>
<enableTimestamp>1</enableTimestamp>
<installationScope>user</installationScope>
<installationType>upgrade</installationType>
<licenseFileEncoding>utf-8</licenseFileEncoding>
<osxApplicationBundleIcon>appIcon.icns</osxApplicationBundleIcon>
<osxApplicationBundleIdentifier>eu.Timeraa.PreMiD</osxApplicationBundleIdentifier>
<osxPlatforms>osx-intel osx-x86_64</osxPlatforms>
<osxUninstallerApplicationBundleIcon>appIcon.icns</osxUninstallerApplicationBundleIcon>
<outputDirectory>../dist/installer</outputDirectory>
<overwritePolicy>onlyIfNewer</overwritePolicy>
<productDisplayIcon>C:/Users/metzf/Documents/Development/PreMiD/PreMiD/installer_assets/appIcon.ico</productDisplayIcon>
<productUrlHelpLink>https://discord.premid.app</productUrlHelpLink>
<productUrlInfoAbout>https://premid.app</productUrlInfoAbout>
<readmeFileEncoding>utf-8</readmeFileEncoding>
<removeLogFile>1</removeLogFile>
<removeUninstallationLogFile>1</removeUninstallationLogFile>
<requestedExecutionLevel>asInvoker</requestedExecutionLevel>
<saveRelativePaths>1</saveRelativePaths>
<summary>Rich Presence for web services.</summary>
<unattendedModeUI>minimalWithDialogs</unattendedModeUI>
<vendor>Timeraa</vendor>
<width>625</width>
<windowsExecutableIcon>appIcon.ico</windowsExecutableIcon>
<windowsResourceFileDescription>Rich Presence for web services.</windowsResourceFileDescription>
<windowsResourceFileVersion>${product_version}</windowsResourceFileVersion>
<windowsUninstallerExecutableIcon>appIcon.ico</windowsUninstallerExecutableIcon>
<licenseFileList>
<licenseFile>
<code>en</code>
<encoding>utf-8</encoding>
<file>../LICENSE</file>
</licenseFile>
</licenseFileList>
<parameterList>
<parameterGroup>
<name>post_install_page</name>
<title>Installation Finished</title>
<explanation></explanation>
<value></value>
<default></default>
<insertAfter>installation</insertAfter>
<parameterList>
<labelParameter>
<name>general</name>
<description>General</description>
<explanation></explanation>
<image></image>
</labelParameter>
<booleanParameter>
<name>addDesktop</name>
<description>Create Desktop Icon</description>
<explanation></explanation>
<value>false</value>
<default>false</default>
<displayStyle>checkbox-left</displayStyle>
</booleanParameter>
<booleanParameter>
<name>launchApp</name>
<description>Launch App</description>
<explanation></explanation>
<value>true</value>
<default>true</default>
<displayStyle>checkbox-left</displayStyle>
</booleanParameter>
<labelParameter>
<name>extra</name>
<description>Extra</description>
<explanation></explanation>
<image></image>
</labelParameter>
<booleanParameter>
<name>openStore</name>
<description>Open Presence Store</description>
<explanation></explanation>
<value>true</value>
<default>true</default>
<displayStyle>checkbox-left</displayStyle>
</booleanParameter>
</parameterList>
<postShowPageActionList>
<createShortcuts>
<destination>${windows_folder_desktopdirectory}</destination>
<ruleList>
<isTrue>
<value>${addDesktop}</value>
</isTrue>
</ruleList>
<shortcutList>
<quickLaunchShortcut>
<comment>Rich Presence for web services.</comment>
<name>PreMiD</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>${installdir}/PreMiD.${platform_exec_suffix}</windowsExec>
<windowsExecArgs></windowsExecArgs>
<windowsIcon></windowsIcon>
<windowsPath></windowsPath>
</quickLaunchShortcut>
</shortcutList>
</createShortcuts>
<runProgram>
<abortOnError>0</abortOnError>
<program>PreMiD.exe</program>
<programArguments>&amp;</programArguments>
<progressText>Launching PreMiD...</progressText>
<showMessageOnError>0</showMessageOnError>
<workingDirectory>${installdir}</workingDirectory>
<ruleList>
<platformTest>
<type>windows</type>
</platformTest>
<isTrue>
<value>${launchApp}</value>
</isTrue>
</ruleList>
</runProgram>
<runProgram>
<abortOnError>0</abortOnError>
<program>open</program>
<programArguments>${installdir}/PreMiD.app</programArguments>
<progressText>Launching PreMiD...</progressText>
<runAs>${env(USER)}</runAs>
<showMessageOnError>0</showMessageOnError>
<useMSDOSPath>0</useMSDOSPath>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
<isTrue>
<value>${launchApp}</value>
</isTrue>
</ruleList>
</runProgram>
<launchBrowser>
<url>https://premid.app/store</url>
<ruleList>
<isTrue>
<value>${openStore}</value>
</isTrue>
</ruleList>
</launchBrowser>
<exit/>
</postShowPageActionList>
</parameterGroup>
</parameterList>
<platformOptionsList>
<platformOptions>
<platform>windows</platform>
</platformOptions>
</platformOptionsList>
</project>

View File

@@ -1,412 +0,0 @@
<project>
<shortName>PreMiD</shortName>
<fullName>PreMiD</fullName>
<version>latest</version>
<installerFilename>${product_shortname}-installer.${platform_exec_suffix}</installerFilename>
<debugLevel>0</debugLevel>
<licenseFile>../LICENSE</licenseFile>
<leftImage>leftSide.png</leftImage>
<logoImage>logo.png</logoImage>
<splashImage>logo.png</splashImage>
<componentList>
<component>
<name>default</name>
<description>Default Component</description>
<canBeEdited>1</canBeEdited>
<selected>1</selected>
<show>1</show>
<folderList>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfiles</name>
<platforms>all</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfileslinux</name>
<platforms>linux</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfileslinux64</name>
<platforms>linux-x64</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfileswindows</name>
<platforms>windows</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfileswindows64</name>
<platforms>windows-x64</platforms>
</folder>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfilesosx</name>
<platforms>osx</platforms>
</folder>
</folderList>
<startMenuShortcutList>
<startMenuShortcut>
<comment>Uninstall ${product_fullname}</comment>
<name>Uninstall ${product_fullname}</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>${installdir}/${uninstallerName}.exe</windowsExec>
<windowsExecArgs></windowsExecArgs>
<windowsIcon></windowsIcon>
<windowsPath>${installdir}/</windowsPath>
</startMenuShortcut>
<startMenuShortcut>
<comment>Rich Presence for web services.</comment>
<name>PreMiD</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>${installdir}/PreMiD.${platform_exec_suffix}</windowsExec>
<windowsExecArgs></windowsExecArgs>
<windowsIcon></windowsIcon>
<windowsPath></windowsPath>
</startMenuShortcut>
</startMenuShortcutList>
</component>
</componentList>
<initializationActionList>
<setInstallerVariable>
<name>installdir</name>
<persist>1</persist>
<value>${windows_folder_appdata}/PreMiD</value>
<ruleList>
<platformTest>
<type>windows</type>
</platformTest>
</ruleList>
</setInstallerVariable>
<setInstallerVariable>
<name>installdir</name>
<persist>1</persist>
<value>${platform_install_prefix}/PreMiD</value>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
</ruleList>
</setInstallerVariable>
</initializationActionList>
<readyToInstallActionList>
<actionGroup>
<customErrorMessage>Couldn't download release... Try again later.</customErrorMessage>
<progressText>Downloading latest release...</progressText>
<actionList>
<showProgressDialog>
<title>Downloading latest release...</title>
<width>450</width>
<actionList>
<httpGet>
<customErrorMessage>${platform_name}</customErrorMessage>
<filename>${system_temp_directory}/PreMiD-release.zip</filename>
<url>https://github.com/PreMiD/PreMiD/releases/latest/download/PreMiD-win32-x64.zip</url>
</httpGet>
</actionList>
<ruleList>
<platformTest>
<type>windows-x64</type>
</platformTest>
</ruleList>
</showProgressDialog>
<showProgressDialog>
<title>Downloading latest release...</title>
<actionList>
<httpGet>
<customErrorMessage>${platform_name}</customErrorMessage>
<filename>${system_temp_directory}/PreMiD-release.zip</filename>
<url>https://github.com/PreMiD/PreMiD/releases/latest/download/PreMiD-win32-ia32.zip</url>
</httpGet>
</actionList>
<ruleList>
<platformTest>
<type>windows-x86</type>
</platformTest>
</ruleList>
</showProgressDialog>
<showProgressDialog>
<title>Downloading latest release...</title>
<actionList>
<httpGet>
<customErrorMessage>${platform_name}</customErrorMessage>
<filename>${system_temp_directory}/PreMiD-release.zip</filename>
<url>https://github.com/PreMiD/PreMiD/releases/latest/download/PreMiD-darwin-x64.zip</url>
</httpGet>
</actionList>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
</ruleList>
</showProgressDialog>
</actionList>
</actionGroup>
<actionGroup>
<actionList>
<!-- Remove the old ARP Entry
Get the old version -->
<registryGet>
<key>HKEY_LOCAL_MACHINE\Software\${project.windowsSoftwareRegistryPrefix}</key>
<name>Version</name>
<variable>oldVersion</variable>
</registryGet>
<!-- Delete the old ARP registry keys -->
<registryDelete>
<key>HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${project.fullName} ${oldVersion}</key>
<name></name>
</registryDelete>
<registryDelete>
<key>HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Management\ARPCache\${project.fullName} ${oldVersion}</key>
<name></name>
</registryDelete>
</actionList>
<ruleList>
<platformTest type="windows"/>
<isTrue value="${isUpgradeMode}"/>
</ruleList>
</actionGroup>
<actionGroup>
<progressText>Killing ${product_fullname}...</progressText>
<actionList>
<kill>
<abortOnError>0</abortOnError>
<name>${product_fullname}.exe</name>
<showMessageOnError>0</showMessageOnError>
<ruleList>
<platformTest>
<type>windows</type>
</platformTest>
</ruleList>
</kill>
<runProgram>
<program>pkill</program>
<programArguments>PreMiD</programArguments>
<runAs>${env(USER)}</runAs>
<useMSDOSPath>0</useMSDOSPath>
<workingDirectory>${installdir}/</workingDirectory>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
<processTest>
<logic>is_running</logic>
<name>PreMiD</name>
</processTest>
</ruleList>
</runProgram>
</actionList>
</actionGroup>
</readyToInstallActionList>
<postInstallationActionList>
<unzip>
<addToUninstaller>1</addToUninstaller>
<destinationDirectory>${installdir}</destinationDirectory>
<progressText>Extracting release...</progressText>
<zipFile>${system_temp_directory}/PreMiD-release.zip</zipFile>
</unzip>
<addDirectoriesToUninstaller>
<addContents>1</addContents>
<files>${installdir}/</files>
</addDirectoriesToUninstaller>
</postInstallationActionList>
<preUninstallationActionList>
<actionGroup>
<progressText>Killing ${product_fullname}...</progressText>
<actionList>
<kill>
<abortOnError>0</abortOnError>
<name>${product_fullname}.exe</name>
<showMessageOnError>0</showMessageOnError>
<ruleList>
<platformTest>
<type>windows</type>
</platformTest>
</ruleList>
</kill>
<runProgram>
<program>pkill</program>
<programArguments>PreMiD</programArguments>
<runAs>${env(USER)}</runAs>
<useMSDOSPath>0</useMSDOSPath>
<workingDirectory>${installdir}/</workingDirectory>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
<processTest>
<logic>is_running</logic>
<name>PreMiD</name>
</processTest>
</ruleList>
</runProgram>
</actionList>
</actionGroup>
</preUninstallationActionList>
<compressionAlgorithm>lzham-ultra</compressionAlgorithm>
<createOsxBundleZip>1</createOsxBundleZip>
<deleteOnExit>1</deleteOnExit>
<description>Rich Presence for web services.</description>
<disableSplashScreen>1</disableSplashScreen>
<enableRollback>0</enableRollback>
<enableSslSupport>1</enableSslSupport>
<enableTimestamp>1</enableTimestamp>
<installationScope>user</installationScope>
<licenseFileEncoding>utf-8</licenseFileEncoding>
<osxApplicationBundleIcon>appIcon.icns</osxApplicationBundleIcon>
<osxApplicationBundleIdentifier>eu.Timeraa.PreMiD</osxApplicationBundleIdentifier>
<osxPlatforms>osx-intel osx-x86_64</osxPlatforms>
<osxUninstallerApplicationBundleIcon>appIcon.icns</osxUninstallerApplicationBundleIcon>
<outputDirectory>../dist/installer</outputDirectory>
<overwritePolicy>onlyIfNewer</overwritePolicy>
<productDisplayIcon>C:/Users/metzf/Documents/Development/PreMiD/PreMiD/installer_assets/appIcon.ico</productDisplayIcon>
<productUrlHelpLink>https://discord.premid.app</productUrlHelpLink>
<productUrlInfoAbout>https://premid.app</productUrlInfoAbout>
<readmeFileEncoding>utf-8</readmeFileEncoding>
<removeLogFile>1</removeLogFile>
<removeUninstallationLogFile>1</removeUninstallationLogFile>
<requestedExecutionLevel>asInvoker</requestedExecutionLevel>
<saveRelativePaths>1</saveRelativePaths>
<summary>Rich Presence for web services.</summary>
<vendor>Timeraa</vendor>
<windowsExecutableIcon>appIcon.ico</windowsExecutableIcon>
<windowsResourceFileDescription>Rich Presence for web services.</windowsResourceFileDescription>
<windowsResourceFileVersion>${product_version}</windowsResourceFileVersion>
<windowsUninstallerExecutableIcon>appIcon.ico</windowsUninstallerExecutableIcon>
<licenseFileList>
<licenseFile>
<code>en</code>
<encoding>utf-8</encoding>
<file>../LICENSE</file>
</licenseFile>
</licenseFileList>
<parameterList>
<parameterGroup>
<name>post_install_page</name>
<title>Installation Finished</title>
<explanation></explanation>
<value></value>
<default></default>
<insertAfter>installation</insertAfter>
<parameterList>
<labelParameter>
<name>general</name>
<description>General</description>
<explanation></explanation>
<image></image>
</labelParameter>
<booleanParameter>
<name>addDesktop</name>
<description>Create Desktop Icon</description>
<explanation></explanation>
<value>false</value>
<default>false</default>
<displayStyle>checkbox-left</displayStyle>
</booleanParameter>
<booleanParameter>
<name>launchApp</name>
<description>Launch App</description>
<explanation></explanation>
<value>true</value>
<default>true</default>
<displayStyle>checkbox-left</displayStyle>
</booleanParameter>
<labelParameter>
<name>extra</name>
<description>Extra</description>
<explanation></explanation>
<image></image>
</labelParameter>
<booleanParameter>
<name>openStore</name>
<description>Open Presence Store</description>
<explanation></explanation>
<value>true</value>
<default>true</default>
<displayStyle>checkbox-left</displayStyle>
</booleanParameter>
</parameterList>
<postShowPageActionList>
<createShortcuts>
<destination>${windows_folder_desktopdirectory}</destination>
<ruleList>
<isTrue>
<value>${addDesktop}</value>
</isTrue>
</ruleList>
<shortcutList>
<quickLaunchShortcut>
<comment>Rich Presence for web services.</comment>
<name>PreMiD</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>${installdir}/PreMiD.${platform_exec_suffix}</windowsExec>
<windowsExecArgs></windowsExecArgs>
<windowsIcon></windowsIcon>
<windowsPath></windowsPath>
</quickLaunchShortcut>
</shortcutList>
</createShortcuts>
<runProgram>
<abortOnError>0</abortOnError>
<program>PreMiD.exe</program>
<programArguments>&amp;</programArguments>
<progressText>Launching PreMiD...</progressText>
<showMessageOnError>0</showMessageOnError>
<workingDirectory>${installdir}</workingDirectory>
<ruleList>
<platformTest>
<type>windows</type>
</platformTest>
<isTrue>
<value>${launchApp}</value>
</isTrue>
</ruleList>
</runProgram>
<runProgram>
<abortOnError>0</abortOnError>
<program>open</program>
<programArguments>${installdir}/PreMiD.app</programArguments>
<progressText>Launching PreMiD...</progressText>
<runAs>${env(USER)}</runAs>
<showMessageOnError>0</showMessageOnError>
<useMSDOSPath>0</useMSDOSPath>
<ruleList>
<platformTest>
<type>osx</type>
</platformTest>
<isTrue>
<value>${launchApp}</value>
</isTrue>
</ruleList>
</runProgram>
<launchBrowser>
<url>https://premid.app/store</url>
<ruleList>
<isTrue>
<value>${openStore}</value>
</isTrue>
</ruleList>
</launchBrowser>
<exit/>
</postShowPageActionList>
</parameterGroup>
</parameterList>
<platformOptionsList>
<platformOptions>
<platform>windows</platform>
</platformOptions>
</platformOptionsList>
</project>

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More